mirror of
https://github.com/photoprism/photoprism.git
synced 2025-10-05 16:57:17 +08:00
263 lines
8.5 KiB
JavaScript
263 lines
8.5 KiB
JavaScript
import { describe, it, expect, beforeEach } from "vitest";
|
|
import { mount } from "@vue/test-utils";
|
|
import { nextTick } from "vue";
|
|
import ChipSelector from "component/file/chip-selector.vue";
|
|
|
|
describe("component/file/chip-selector", () => {
|
|
let wrapper;
|
|
|
|
const mockItems = [
|
|
{ value: "album1", title: "Album 1", mixed: false, action: "none" },
|
|
{ value: "album2", title: "Album 2", mixed: true, action: "add" },
|
|
{ value: "album3", title: "Album 3", mixed: false, action: "remove" },
|
|
];
|
|
|
|
const mockAvailableItems = [
|
|
{ value: "album1", title: "Album 1" },
|
|
{ value: "album2", title: "Album 2" },
|
|
{ value: "album3", title: "Album 3" },
|
|
{ value: "album4", title: "Album 4" },
|
|
];
|
|
|
|
beforeEach(() => {
|
|
const VIconStub = {
|
|
name: "VIcon",
|
|
props: ["icon"],
|
|
template: '<i class="chip__icon"><slot />{{ icon }}</i>',
|
|
};
|
|
|
|
const VTooltipStub = {
|
|
name: "VTooltip",
|
|
props: ["text", "location"],
|
|
template: '<div class="v-tooltip-stub"><slot name="activator" :props="{}"></slot><slot /></div>',
|
|
};
|
|
|
|
wrapper = mount(ChipSelector, {
|
|
props: {
|
|
items: mockItems,
|
|
availableItems: mockAvailableItems,
|
|
allowCreate: true,
|
|
emptyText: "No items",
|
|
inputPlaceholder: "Enter item name...",
|
|
},
|
|
global: {
|
|
stubs: {
|
|
VIcon: VIconStub,
|
|
VTooltip: VTooltipStub,
|
|
},
|
|
mocks: {
|
|
$gettext: (s) => s,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
describe("Component Rendering", () => {
|
|
it("should show empty text and hide input when allowCreate is false and no items", async () => {
|
|
await wrapper.setProps({ items: [], allowCreate: false });
|
|
|
|
const emptyDiv = wrapper.find(".chip-selector__empty");
|
|
expect(emptyDiv.exists()).toBe(true);
|
|
expect(emptyDiv.text()).toBe("No items");
|
|
expect(wrapper.find(".chip-selector__input-container").exists()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Chip Icons", () => {
|
|
it.each([
|
|
{ idx: 0, expectedClass: "chip--gray", expectedIcon: null },
|
|
{ idx: 1, expectedClass: "chip--green-light", expectedIcon: "mdi-plus" },
|
|
{ idx: 2, expectedClass: "chip--red", expectedIcon: "mdi-minus" },
|
|
])("should render expected style/icon for chip at index $idx", ({ idx, expectedClass, expectedIcon }) => {
|
|
const chips = wrapper.findAll(".chip");
|
|
const chip = chips[idx];
|
|
expect(chip.find(".chip__text").exists()).toBe(true);
|
|
expect(chip.classes()).toContain(expectedClass);
|
|
|
|
const icon = chip.find(".chip__icon");
|
|
if (expectedIcon) {
|
|
expect(icon.exists()).toBe(true);
|
|
expect(icon.text()).toBe(expectedIcon);
|
|
} else {
|
|
expect(icon.exists()).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("should show half-circle icon for mixed state without action", async () => {
|
|
const mixedItem = { value: "mixed1", title: "Mixed Item", mixed: true, action: "none" };
|
|
await wrapper.setProps({ items: [mixedItem] });
|
|
|
|
const chip = wrapper.find(".chip");
|
|
const icon = chip.find(".chip__icon");
|
|
expect(icon.exists()).toBe(true);
|
|
expect(icon.text()).toBe("mdi-circle-half-full");
|
|
});
|
|
});
|
|
|
|
describe("Chip Interactions", () => {
|
|
it("should emit update:items when chip is clicked", async () => {
|
|
const chip = wrapper.findAll(".chip")[0]; // First chip (action: none)
|
|
await chip.trigger("click");
|
|
|
|
const emitted = wrapper.emitted("update:items");
|
|
expect(emitted).toBeTruthy();
|
|
expect(emitted[0][0]).toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ value: "album1", action: "remove" })])
|
|
);
|
|
});
|
|
|
|
it.each(["keydown.enter", "keydown.space"])("should handle keyboard interactions (%s)", async (evt) => {
|
|
const chip = wrapper.findAll(".chip")[0];
|
|
await chip.trigger(evt);
|
|
const emitted = wrapper.emitted("update:items");
|
|
expect(emitted).toBeTruthy();
|
|
});
|
|
|
|
it.each([
|
|
{ prop: "loading", value: true },
|
|
{ prop: "disabled", value: true },
|
|
])("should not respond to clicks when %s", async ({ prop, value }) => {
|
|
await wrapper.setProps({ [prop]: value });
|
|
const chip = wrapper.findAll(".chip")[0];
|
|
await chip.trigger("click");
|
|
expect(wrapper.emitted("update:items")).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe("Chip Action Cycling", () => {
|
|
it("should cycle through actions correctly for mixed items", async () => {
|
|
const mixedItem = { value: "mixed1", title: "Mixed Item", mixed: true, action: "none" };
|
|
await wrapper.setProps({ items: [mixedItem] });
|
|
|
|
const chip = wrapper.find(".chip");
|
|
|
|
// First click: none -> add
|
|
await chip.trigger("click");
|
|
let emitted = wrapper.emitted("update:items");
|
|
expect(emitted[0][0][0].action).toBe("add");
|
|
|
|
// Update props to simulate the new state
|
|
await wrapper.setProps({ items: [{ ...mixedItem, action: "add" }] });
|
|
|
|
// Second click: add -> remove
|
|
await chip.trigger("click");
|
|
emitted = wrapper.emitted("update:items");
|
|
expect(emitted[1][0][0].action).toBe("remove");
|
|
|
|
// Update props again
|
|
await wrapper.setProps({ items: [{ ...mixedItem, action: "remove" }] });
|
|
|
|
// Third click: remove -> none
|
|
await chip.trigger("click");
|
|
emitted = wrapper.emitted("update:items");
|
|
expect(emitted[2][0][0].action).toBe("none");
|
|
});
|
|
|
|
it("should handle new item removal correctly", async () => {
|
|
const newItem = { value: "", title: "New Item", mixed: false, action: "add", isNew: true };
|
|
await wrapper.setProps({ items: [newItem] });
|
|
|
|
const chip = wrapper.find(".chip");
|
|
await chip.trigger("click");
|
|
|
|
const emitted = wrapper.emitted("update:items");
|
|
expect(emitted[0][0]).toEqual([]); // Item should be completely removed
|
|
});
|
|
});
|
|
|
|
describe("Input Functionality", () => {
|
|
it("should add new item when Enter is pressed with text input", async () => {
|
|
const combobox = wrapper.findComponent({ name: "VCombobox" });
|
|
|
|
// Set the input value
|
|
wrapper.vm.newItemTitle = "New Album";
|
|
await nextTick();
|
|
|
|
// Trigger enter key
|
|
await combobox.trigger("keydown.enter");
|
|
|
|
const emitted = wrapper.emitted("update:items");
|
|
expect(emitted).toBeTruthy();
|
|
expect(emitted[0][0]).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
title: "New Album",
|
|
action: "add",
|
|
isNew: true,
|
|
mixed: false,
|
|
value: "",
|
|
}),
|
|
])
|
|
);
|
|
});
|
|
|
|
it("should handle combobox selection change", async () => {
|
|
const combobox = wrapper.findComponent({ name: "VCombobox" });
|
|
const selectedItem = { value: "album4", title: "Album 4" };
|
|
|
|
await combobox.vm.$emit("update:model-value", selectedItem);
|
|
|
|
const emitted = wrapper.emitted("update:items");
|
|
expect(emitted).toBeTruthy();
|
|
expect(emitted[0][0]).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
value: "album4",
|
|
title: "Album 4",
|
|
action: "add",
|
|
isNew: true,
|
|
}),
|
|
])
|
|
);
|
|
});
|
|
|
|
it("should not add duplicate items", async () => {
|
|
const combobox = wrapper.findComponent({ name: "VCombobox" });
|
|
|
|
// Try to add an existing item
|
|
wrapper.vm.newItemTitle = "Album 1"; // This already exists
|
|
await combobox.trigger("keydown.enter");
|
|
|
|
// Should not emit update:items for duplicate
|
|
expect(wrapper.emitted("update:items")).toBeFalsy();
|
|
});
|
|
|
|
it("should not add empty items", async () => {
|
|
const combobox = wrapper.findComponent({ name: "VCombobox" });
|
|
|
|
wrapper.vm.newItemTitle = " "; // Empty/whitespace string
|
|
await combobox.trigger("keydown.enter");
|
|
|
|
expect(wrapper.emitted("update:items")).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe("Computed Properties", () => {
|
|
it("should process items correctly", () => {
|
|
const processed = wrapper.vm.processedItems;
|
|
|
|
expect(processed).toHaveLength(3);
|
|
expect(processed[0]).toMatchObject({
|
|
value: "album1",
|
|
title: "Album 1",
|
|
action: "none",
|
|
selected: false,
|
|
});
|
|
expect(processed[1]).toMatchObject({
|
|
value: "album2",
|
|
title: "Album 2",
|
|
action: "add",
|
|
selected: true,
|
|
});
|
|
});
|
|
|
|
it("should determine when to render chips correctly", async () => {
|
|
expect(wrapper.vm.shouldRenderChips).toBe(true);
|
|
|
|
// When no items and input is shown, should not render chips container
|
|
await wrapper.setProps({ items: [] });
|
|
expect(wrapper.vm.shouldRenderChips).toBe(false);
|
|
});
|
|
});
|
|
});
|