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: '{{ icon }}', }; const VTooltipStub = { name: "VTooltip", props: ["text", "location"], template: '
', }; 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); }); }); });