From a89e975a4ab7867b2e34962a01fe50a89e74f5b8 Mon Sep 17 00:00:00 2001 From: mm <25961416+mlmoravek@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:11:20 +0100 Subject: [PATCH] test(autocomplete): add autocomplete test cases (#1181) --- .../components/autocomplete/Autocomplete.vue | 3 +- .../autocomplete/examples/index.vue | 14 + .../autocomplete/examples/options.vue | 7 +- .../autocomplete/examples/slots.vue | 36 ++ .../tests/Autocomplete.spec.cy.ts | 10 - .../autocomplete/tests/SimpleAutocomplete.vue | 38 -- .../__snapshots__/autocomplete.test.ts.snap | 141 +++++++- .../autocomplete/tests/autocomplete.test.ts | 335 +++++++++++++++++- .../tests/__snapshots__/dropdown.test.ts.snap | 4 +- .../dropdown/tests/dropdown.axe.test.ts | 2 +- .../dropdown/tests/dropdown.test.ts | 40 ++- .../components/radio/tests/radio.axe.test.ts | 2 +- .../components/steps/tests/steps.axe.test.ts | 2 +- .../switch/tests/switch.axe.test.ts | 2 +- .../components/table/tests/table.axe.test.ts | 2 +- .../src/components/table/tests/table.test.ts | 2 - .../components/tabs/tests/tabs.axe.test.ts | 2 +- .../src/components/tabs/tests/tabs.test.ts | 1 - 18 files changed, 565 insertions(+), 78 deletions(-) create mode 100644 packages/oruga/src/components/autocomplete/examples/slots.vue delete mode 100644 packages/oruga/src/components/autocomplete/tests/Autocomplete.spec.cy.ts delete mode 100644 packages/oruga/src/components/autocomplete/tests/SimpleAutocomplete.vue diff --git a/packages/oruga/src/components/autocomplete/Autocomplete.vue b/packages/oruga/src/components/autocomplete/Autocomplete.vue index 3b54bce10..5bad84ab2 100644 --- a/packages/oruga/src/components/autocomplete/Autocomplete.vue +++ b/packages/oruga/src/components/autocomplete/Autocomplete.vue @@ -284,7 +284,8 @@ function setSelected(item: T | SpecialOption | undefined): void { inputValue.value = props.clearOnSelect ? "" : option?.label || ""; checkHtml5Validity(); - setFocus(); + if (props.keepOpen) setFocus(); + else isActive.value = false; } // --- Event Handler --- diff --git a/packages/oruga/src/components/autocomplete/examples/index.vue b/packages/oruga/src/components/autocomplete/examples/index.vue index 11cffea1e..8ced950b0 100644 --- a/packages/oruga/src/components/autocomplete/examples/index.vue +++ b/packages/oruga/src/components/autocomplete/examples/index.vue @@ -5,6 +5,9 @@ import BaseCode from "./base.vue?raw"; import Options from "./options.vue"; import OptionsCode from "./options.vue?raw"; +import Slots from "./slots.vue"; +import SlotsCode from "./slots.vue?raw"; + import Selection from "./selection.vue"; import SelectionCode from "./selection.vue?raw"; @@ -61,6 +64,17 @@ import ScrollCode from "./scroll.vue?raw"; +

Slots

+

+ A header and a footer can be added to the options list by using the + header and footer slots. The header and + footer can be made clickable by adding the + selectable-header and + selectable-footer props. Clicking them will clear the + input. +

+ +

Infinite Scroll

When check-scroll prop is set, the component will emits diff --git a/packages/oruga/src/components/autocomplete/examples/options.vue b/packages/oruga/src/components/autocomplete/examples/options.vue index 06344a048..641f06cfc 100644 --- a/packages/oruga/src/components/autocomplete/examples/options.vue +++ b/packages/oruga/src/components/autocomplete/examples/options.vue @@ -241,12 +241,7 @@ const groupSelected = ref(); v-model="selected" :options="options" placeholder="e.g. Anne" - open-on-focus - selectable-header - selectable-footer> - - - + open-on-focus>

Selected: {{ selected }}

diff --git a/packages/oruga/src/components/autocomplete/examples/slots.vue b/packages/oruga/src/components/autocomplete/examples/slots.vue new file mode 100644 index 000000000..bb7dda94e --- /dev/null +++ b/packages/oruga/src/components/autocomplete/examples/slots.vue @@ -0,0 +1,36 @@ + diff --git a/packages/oruga/src/components/autocomplete/tests/Autocomplete.spec.cy.ts b/packages/oruga/src/components/autocomplete/tests/Autocomplete.spec.cy.ts deleted file mode 100644 index abff1fedd..000000000 --- a/packages/oruga/src/components/autocomplete/tests/Autocomplete.spec.cy.ts +++ /dev/null @@ -1,10 +0,0 @@ -// import SimpleAutocomplete from "./SimpleAutocomplete.vue"; - -// describe("", () => { -// it("works", () => { -// cy.mount(SimpleAutocomplete); -// cy.get("input").click().type("j"); -// cy.get("input").type("{downarrow}{downarrow}{enter}"); -// cy.get("input").should("have.value", "Node.js"); -// }); -// }); diff --git a/packages/oruga/src/components/autocomplete/tests/SimpleAutocomplete.vue b/packages/oruga/src/components/autocomplete/tests/SimpleAutocomplete.vue deleted file mode 100644 index 113e4dc0a..000000000 --- a/packages/oruga/src/components/autocomplete/tests/SimpleAutocomplete.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - diff --git a/packages/oruga/src/components/autocomplete/tests/__snapshots__/autocomplete.test.ts.snap b/packages/oruga/src/components/autocomplete/tests/__snapshots__/autocomplete.test.ts.snap index 2dbf05fab..6906432b0 100644 --- a/packages/oruga/src/components/autocomplete/tests/__snapshots__/autocomplete.test.ts.snap +++ b/packages/oruga/src/components/autocomplete/tests/__snapshots__/autocomplete.test.ts.snap @@ -25,13 +25,142 @@ exports[`OAutocomplete tests > render correctly 1`] = ` @binding {number} focusedIndex - index of the focused element @binding {() => void} toggle - toggle dropdown active state --> - - + +
+ + Angular +
+
+ + Angular 2 +
+
+ + Aurelia +
+
+ + Backbone +
+
+ + Ember +
+
+ + jQuery +
+
+ + Meteor +
+
+ + Node.js +
+
+ + Polymer +
+
+ + React +
+
+ + RxJS +
+
+ + Vue.js +
+ + diff --git a/packages/oruga/src/components/autocomplete/tests/autocomplete.test.ts b/packages/oruga/src/components/autocomplete/tests/autocomplete.test.ts index 2522a5ea1..e43e52adf 100644 --- a/packages/oruga/src/components/autocomplete/tests/autocomplete.test.ts +++ b/packages/oruga/src/components/autocomplete/tests/autocomplete.test.ts @@ -1,21 +1,348 @@ -import { describe, test, expect, afterEach } from "vitest"; +import { describe, test, expect, afterEach, vi } from "vitest"; import { enableAutoUnmount, mount } from "@vue/test-utils"; - -import OAutocomplete from "@/components/autocomplete/Autocomplete.vue"; +import { nextTick } from "vue"; +import { setTimeout } from "timers/promises"; import type { OptionsGroupProp, OptionsItem, OptionsProp } from "@/composables"; +import OAutocomplete from "@/components/autocomplete/Autocomplete.vue"; + describe("OAutocomplete tests", () => { enableAutoUnmount(afterEach); + const OPTIONS = [ + "Angular", + "Angular 2", + "Aurelia", + "Backbone", + "Ember", + "jQuery", + "Meteor", + "Node.js", + "Polymer", + "React", + "RxJS", + "Vue.js", + ]; + test("render correctly", () => { - const wrapper = mount(OAutocomplete); + const wrapper = mount(OAutocomplete, { + props: { options: OPTIONS }, + }); expect(!!wrapper.vm).toBeTruthy(); expect(wrapper.exists()).toBeTruthy(); expect(wrapper.attributes("data-oruga")).toBe("autocomplete"); expect(wrapper.html()).toMatchSnapshot(); }); + test("has a dropdown menu hidden by default", () => { + const wrapper = mount(OAutocomplete, { attachTo: document.body }); + const dropdown = wrapper.find(".o-drop__menu"); + expect(dropdown.exists()).toBeTruthy(); + expect(dropdown.isVisible()).toBeFalsy(); + }); + + test("can emit input, focus and blur events", async () => { + const VALUE_TYPED = "test"; + + const wrapper = mount(OAutocomplete, { + props: { options: OPTIONS }, + }); + + const input = wrapper.find("input"); + expect(input.exists()).toBeTruthy(); + + // open menu + await input.trigger("focus"); + expect(wrapper.emitted("focus")).toHaveLength(1); + + await input.setValue(VALUE_TYPED); + await input.trigger("input"); + + expect(wrapper.emitted("update:input")).toHaveLength(1); + expect(wrapper.emitted("update:input")![0]).toContain(VALUE_TYPED); + + await input.trigger("blur"); + expect(wrapper.emitted("blur")).toBeDefined(); + }); + + test("can autocomplete with keydown", async () => { + const VALUE_TYPED = "Ang"; + + const wrapper = mount(OAutocomplete, { + props: { options: OPTIONS, openOnFocus: true, keepOpen: false }, + attachTo: document.body, + }); + + const input = wrapper.find("input"); + expect(input.exists()).toBeTruthy(); + + // open menu + await input.trigger("focus"); + await input.setValue(VALUE_TYPED); + + const dropdown = wrapper.find(".o-drop__menu"); + expect(dropdown.exists()).toBeTruthy(); + expect(dropdown.isVisible()).toBeTruthy(); + + await input.trigger("keydown", { key: "Down" }); + await input.trigger("keydown", { key: "Enter" }); + + expect(input.element.value).toBe(OPTIONS[0]); + expect(wrapper.emitted("select")).toStrictEqual([[OPTIONS[0]]]); + expect(wrapper.emitted("update:modelValue")).toStrictEqual([ + [OPTIONS[0]], + ]); + + expect(dropdown.exists()).toBeTruthy(); + expect(dropdown.isVisible()).toBeFalsy(); + }); + + test("close dropdown on esc", async () => { + const wrapper = mount(OAutocomplete, { + props: { options: OPTIONS, openOnFocus: true }, + attachTo: document.body, + }); + + const input = wrapper.find("input"); + expect(input.exists()).toBeTruthy(); + + // open menu + await input.trigger("focus"); + + const dropdown = wrapper.find(".o-drop__menu"); + expect(dropdown.exists()).toBeTruthy(); + expect(dropdown.isVisible()).toBeTruthy(); + + await input.trigger("keydown", { key: "Escape" }); + + expect(dropdown.isVisible()).toBeFalsy(); + }); + + test("close dropdown on click outside", async () => { + const wrapper = mount(OAutocomplete, { + props: { options: OPTIONS, openOnFocus: true }, + attachTo: document.body, + }); + + const input = wrapper.find("input"); + expect(input.exists()).toBeTruthy(); + + // open menu + await input.trigger("focus"); + await setTimeout(); // await event handler get set + + const dropdown = wrapper.find(".o-drop__menu"); + expect(dropdown.exists()).toBeTruthy(); + expect(dropdown.isVisible()).toBeTruthy(); + + // click outside + window.dispatchEvent(new Event("click")); + await nextTick(); // await dom update + + expect(dropdown.isVisible()).toBeFalsy(); + }); + + test("open dropdown on down key click", async () => { + const wrapper = mount(OAutocomplete, { + props: { options: OPTIONS }, + attachTo: document.body, + }); + + const dropdown = wrapper.find(".o-drop__menu"); + expect(dropdown.exists()).toBeTruthy(); + expect(dropdown.isVisible()).toBeFalsy(); + + const input = wrapper.find("input"); + expect(input.exists()).toBeTruthy(); + + await input.trigger("focus"); + await input.trigger("keydown", { key: "Down" }); + + expect(dropdown.isVisible()).toBeTruthy(); + }); + + test("manages tab pressed as expected", async () => { + const wrapper = mount(OAutocomplete, { + props: { options: OPTIONS, openOnFocus: true, keepFirst: true }, + attachTo: document.body, + }); + + const dropdown = wrapper.find(".o-drop__menu"); + expect(dropdown.exists()).toBeTruthy(); + expect(dropdown.isVisible()).toBeFalsy(); + + const input = wrapper.find("input"); + expect(input.exists()).toBeTruthy(); + + await input.trigger("keydown", { key: "Tab" }); + expect(dropdown.isVisible()).toBeFalsy(); + + await input.trigger("focus"); + + await input.trigger("keydown", { key: "Tab" }); + expect(input.element.value).toBe(""); + }); + + test("can openOnFocus and keepFirst", async () => { + const wrapper = mount(OAutocomplete, { + props: { options: OPTIONS, openOnFocus: true, keepFirst: true }, + attachTo: document.body, + }); + + const dropdown = wrapper.find(".o-drop__menu"); + expect(dropdown.exists()).toBeTruthy(); + expect(dropdown.isVisible()).toBeFalsy(); + + const input = wrapper.find("input"); + expect(input.exists()).toBeTruthy(); + + await input.trigger("focus"); + + expect(dropdown.isVisible()).toBeTruthy(); + }); + + test("reset events before destroy", async () => { + document.removeEventListener = vi.fn(); + window.removeEventListener = vi.fn(); + + const wrapper = mount(OAutocomplete, { + props: { options: OPTIONS }, + }); + await setTimeout(); // await event handler get set + + wrapper.unmount(); + + expect(document.removeEventListener).toBeCalledTimes(2); + // remove scroll listener + expect(document.removeEventListener).toBeCalledWith( + "scroll", + expect.any(Function), + ); + + expect(window.removeEventListener).toBeCalledTimes(2); + // remove position listener + expect(window.removeEventListener).toBeCalledWith( + "resize", + expect.any(Function), + ); + }); + + test("clear button does not exist when the search input is empty", async () => { + const wrapper = mount(OAutocomplete, { + props: { + options: OPTIONS, + modelValue: "", + clearable: true, + }, + }); + + const subject = wrapper.find(".o-icon"); + expect(subject.exists()).toBeFalsy(); + }); + + test("clears search input text when clear button gets clicked", async () => { + const wrapper = mount(OAutocomplete, { + props: { + options: OPTIONS, + modelValue: OPTIONS[5], + clearable: true, + }, + }); + + const input = wrapper.find("input"); + expect(input.exists()).toBeTruthy(); + expect(input.element.value).toBe(OPTIONS[5]); + + const icon = wrapper.find(".o-icon"); + expect(icon.exists()).toBeTruthy(); + await icon.trigger("click"); + + expect(input.element.value).toEqual(""); + }); + + test("clear button does not appear when clearable property is not set to true", () => { + const wrapper = mount(OAutocomplete, { + props: { options: OPTIONS, modelValue: OPTIONS[5] }, + }); + const subject = wrapper.find(".o-icon").exists(); + + expect(subject).toBeFalsy(); + }); + + test("can emit select-header by keyboard and click", async () => { + const wrapper = mount(OAutocomplete, { + props: { + openOnFocus: true, + keepOpen: true, + selectableHeader: true, + selectableFooter: true, + }, + slots: { + header: "

SLOT HEADER

", + footer: "

SLOT FOOTER

", + }, + }); + + const input = wrapper.find("input"); + expect(input.exists()).toBeTruthy(); + + // open menu + await input.trigger("focus"); + + // move to header and select by enter + await input.trigger("keydown", { key: "Down" }); + await input.trigger("keydown", { key: "Enter" }); + + expect(wrapper.emitted("select-header")).toHaveLength(1); + + const header = wrapper.find(".o-acp__item-header"); + expect(header.exists()).toBeTruthy(); + await header.trigger("click"); + + expect(wrapper.emitted("select-header")).toHaveLength(2); + }); + + test("can emit select-footer by keyboard and click", async () => { + const wrapper = mount(OAutocomplete, { + props: { + openOnFocus: true, + keepOpen: true, + selectableHeader: true, + selectableFooter: true, + }, + slots: { + header: "

SLOT HEADER

", + footer: "

SLOT FOOTER

", + }, + }); + const input = wrapper.find("input"); + + // open menu + await input.trigger("focus"); + + // move to footer and select by enter + await input.trigger("keydown", { key: "Down" }); + await input.trigger("keydown", { key: "Down" }); + await input.trigger("keydown", { key: "Enter" }); + + expect(wrapper.emitted("select-footer")).toHaveLength(1); + + const footer = wrapper.find(".o-acp__item-footer"); + expect(footer.exists()).toBeTruthy(); + await footer.trigger("click"); + + expect(wrapper.emitted("select-footer")).toHaveLength(2); + }); + + test("has configurable menu and item tags", () => { + const wrapper = mount(OAutocomplete, { + props: { options: OPTIONS, menuTag: "ul", itemTag: "li" }, + }); + expect(wrapper.find("ul.o-drop__menu").exists()).toBeTruthy(); + expect(wrapper.find("li.o-acp__item").exists()).toBeTruthy(); + }); + describe("render options props correctly", () => { test("handle options as primitves correctly", () => { const options: OptionsProp = ["Flint", "Silver", "Vane", 0, 1, 2]; diff --git a/packages/oruga/src/components/dropdown/tests/__snapshots__/dropdown.test.ts.snap b/packages/oruga/src/components/dropdown/tests/__snapshots__/dropdown.test.ts.snap index 25ec22b37..2a11ddb93 100644 --- a/packages/oruga/src/components/dropdown/tests/__snapshots__/dropdown.test.ts.snap +++ b/packages/oruga/src/components/dropdown/tests/__snapshots__/dropdown.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Dropdown tests > render correctly with items 1`] = ` +exports[`ODropdown tests > render correctly with items 1`] = ` "