From e03c6426151e8675306468649e0a02abf3c5be2f Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 10 Feb 2019 14:38:30 -0800 Subject: [PATCH] add Jest suite from vue-cli-3 branch --- tests/helpers.js | 28 ++++ tests/unit/.eslintrc.js | 8 ++ tests/unit/Ajax.spec.js | 89 ++++++++++++ tests/unit/Deselecting.spec.js | 87 +++++++++++ tests/unit/Dropdown.spec.js | 135 +++++++++++++++++ tests/unit/Filtering.spec.js | 75 ++++++++++ tests/unit/Labels.spec.js | 42 ++++++ tests/unit/Layout.spec.js | 43 ++++++ tests/unit/ObjectIndex.spec.js | 215 +++++++++++++++++++++++++++ tests/unit/ReactiveOptions.spec.js | 20 +++ tests/unit/Selecting.spec.js | 217 ++++++++++++++++++++++++++++ tests/unit/Tagging.spec.js | 224 +++++++++++++++++++++++++++++ tests/unit/TypeAhead.spec.js | 147 +++++++++++++++++++ 13 files changed, 1330 insertions(+) create mode 100755 tests/helpers.js create mode 100755 tests/unit/.eslintrc.js create mode 100755 tests/unit/Ajax.spec.js create mode 100755 tests/unit/Deselecting.spec.js create mode 100755 tests/unit/Dropdown.spec.js create mode 100755 tests/unit/Filtering.spec.js create mode 100755 tests/unit/Labels.spec.js create mode 100755 tests/unit/Layout.spec.js create mode 100755 tests/unit/ObjectIndex.spec.js create mode 100755 tests/unit/ReactiveOptions.spec.js create mode 100755 tests/unit/Selecting.spec.js create mode 100755 tests/unit/Tagging.spec.js create mode 100755 tests/unit/TypeAhead.spec.js diff --git a/tests/helpers.js b/tests/helpers.js new file mode 100755 index 0000000..c625289 --- /dev/null +++ b/tests/helpers.js @@ -0,0 +1,28 @@ +import { shallowMount } from "@vue/test-utils"; +import VueSelect from "../src/components/Select"; + +/** + * Trigger a submit event on the search + * input with a provided search text. + * + * @param Wrapper {Wrapper} + * @param searchText + */ +export const searchSubmit = (Wrapper, searchText = false) => { + if (searchText) { + Wrapper.vm.search = searchText; + } + Wrapper.find({ ref: "search" }).trigger("keydown", { + keyCode: 13 + }); +}; + +/** + * Create a new VueSelect instance with + * a provided set of props. + * @param propsData + * @returns {Wrapper} + */ +export const selectWithProps = (propsData = {}) => { + return shallowMount(VueSelect, { propsData }); +}; diff --git a/tests/unit/.eslintrc.js b/tests/unit/.eslintrc.js new file mode 100755 index 0000000..4e51c63 --- /dev/null +++ b/tests/unit/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + env: { + jest: true + }, + rules: { + 'import/no-extraneous-dependencies': 'off' + } +} \ No newline at end of file diff --git a/tests/unit/Ajax.spec.js b/tests/unit/Ajax.spec.js new file mode 100755 index 0000000..e9a7914 --- /dev/null +++ b/tests/unit/Ajax.spec.js @@ -0,0 +1,89 @@ +import { selectWithProps } from "../helpers"; + +describe("Asynchronous Loading", () => { + it("can toggle the loading class", () => { + const Select = selectWithProps(); + + Select.vm.toggleLoading(); + expect(Select.vm.mutableLoading).toEqual(true); + + Select.vm.toggleLoading(true); + expect(Select.vm.mutableLoading).toEqual(true); + }); + + it("should trigger the onSearch callback when the search text changes", () => { + const propsData = { onSearch: () => {} }; + const spy = jest.spyOn(propsData, "onSearch"); + const Select = selectWithProps(propsData); + + Select.vm.search = "foo"; + + expect(spy).toHaveBeenCalled(); + }); + + it("should not trigger the onSearch callback if the search text is empty", () => { + let calledWith = []; + const propsData = { + onSearch: search => { + calledWith.push(search); + } + }; + const spy = jest.spyOn(propsData, "onSearch"); + const Select = selectWithProps(propsData); + + Select.vm.search = "foo"; + Select.vm.search = ""; + + expect(spy).toHaveBeenCalledTimes(1); + expect(calledWith).toEqual(["foo"]); + }); + + it("should trigger the search event when the search text changes", () => { + const Select = selectWithProps(); + + Select.vm.search = "foo"; + + const events = Select.emitted("search"); + + expect(events).toContainEqual(["foo", Select.vm.toggleLoading]); + expect(events.length).toEqual(1); + }); + + it("should not trigger the search event if the search text is empty", () => { + const Select = selectWithProps(); + + Select.vm.search = "foo"; + Select.vm.search = ""; + + const events = Select.emitted("search"); + + expect(events).toContainEqual(["foo", Select.vm.toggleLoading]); + expect(events.length).toEqual(1); + }); + + it("can set loading to false from the onSearch callback", () => { + const Select = selectWithProps({ + onSearch: (search, loading) => loading(false) + }); + + Select.vm.search = "foo"; + + expect(Select.vm.mutableLoading).toEqual(false); + }); + + it("can set loading to true from the onSearch callback", () => { + const Select = selectWithProps({ + onSearch: (search, loading) => loading(true) + }); + + Select.vm.search = "foo"; + + expect(Select.vm.mutableLoading).toEqual(true); + }); + + it("will sync mutable loading with the loading prop", () => { + const Select = selectWithProps({ loading: false }); + Select.setProps({ loading: true }); + expect(Select.vm.mutableLoading).toEqual(true); + }); +}); diff --git a/tests/unit/Deselecting.spec.js b/tests/unit/Deselecting.spec.js new file mode 100755 index 0000000..1ffebec --- /dev/null +++ b/tests/unit/Deselecting.spec.js @@ -0,0 +1,87 @@ +import { selectWithProps } from "../helpers"; + +describe("Removing values", () => { + it("can remove the given tag when its close icon is clicked", () => { + const Select = selectWithProps({ multiple: true, value: ["foo"] }); + + Select.find(".close").trigger("click"); + expect(Select.vm.mutableValue).toEqual([]); + }); + + it("should not remove tag when close icon is clicked and component is disabled", () => { + const Select = selectWithProps({ + value: ["one"], + options: ["one", "two", "three"], + multiple: true, + disabled: true + }); + + Select.find(".close").trigger("click"); + expect(Select.vm.mutableValue).toEqual(["one"]); + }); + + it("should remove the last item in the value array on delete keypress when multiple is true", () => { + const Select = selectWithProps({ + multiple: true, + value: ["one", "two"], + options: ["one", "two", "three"] + }); + + Select.vm.maybeDeleteValue(); + expect(Select.vm.mutableValue).toEqual(["one"]); + }); + + it("should set value to null on delete keypress when multiple is false", () => { + const Select = selectWithProps({ + value: "one", + options: ["one", "two", "three"] + }); + + Select.vm.maybeDeleteValue(); + expect(Select.vm.mutableValue).toEqual(null); + }); + + describe("Clear button", () => { + it("should be displayed on single select when value is selected", () => { + const Select = selectWithProps({ + options: ["foo", "bar"], + value: "foo" + }); + + expect(Select.vm.showClearButton).toEqual(true); + }); + + it("should not be displayed on multiple select", () => { + const Select = selectWithProps({ + options: ["foo", "bar"], + value: "foo", + multiple: true + }); + + expect(Select.vm.showClearButton).toEqual(false); + }); + + it("should remove selected value when clicked", () => { + const Select = selectWithProps({ + options: ["foo", "bar"], + value: "foo" + }); + + expect(Select.vm.mutableValue).toEqual("foo"); + Select.find("button.clear").trigger("click"); + expect(Select.vm.mutableValue).toEqual(null); + }); + + it("should be disabled when component is disabled", () => { + const Select = selectWithProps({ + options: ["foo", "bar"], + value: "foo", + disabled: true + }); + + expect(Select.find("button.clear").attributes().disabled).toEqual( + "disabled" + ); + }); + }); +}); diff --git a/tests/unit/Dropdown.spec.js b/tests/unit/Dropdown.spec.js new file mode 100755 index 0000000..f28b059 --- /dev/null +++ b/tests/unit/Dropdown.spec.js @@ -0,0 +1,135 @@ +import { selectWithProps } from "../helpers"; + +describe("Toggling Dropdown", () => { + it("should not open the dropdown when the el is clicked but the component is disabled", () => { + const Select = selectWithProps({ disabled: true }); + Select.vm.toggleDropdown({ target: Select.vm.$refs.search }); + expect(Select.vm.open).toEqual(false); + }); + + it("should open the dropdown when the el is clicked", () => { + const Select = selectWithProps({ + value: [{ label: "one" }], + options: [{ label: "one" }] + }); + + Select.vm.toggleDropdown({ target: Select.vm.$refs.search }); + expect(Select.vm.open).toEqual(true); + }); + + it("should open the dropdown when the selected tag is clicked", () => { + const Select = selectWithProps({ + value: [{ label: "one" }], + options: [{ label: "one" }] + }); + + const selectedTag = Select.find(".selected-tag").element; + + Select.vm.toggleDropdown({ target: selectedTag }); + expect(Select.vm.open).toEqual(true); + }); + + it("can close the dropdown when the el is clicked", () => { + const Select = selectWithProps(); + const spy = jest.spyOn(Select.vm.$refs.search, "blur"); + + Select.vm.open = true; + Select.vm.toggleDropdown({ target: Select.vm.$el }); + + expect(spy).toHaveBeenCalled(); + }); + + it("closes the dropdown when an option is selected, multiple is true, and closeOnSelect option is true", () => { + const Select = selectWithProps({ + value: [], + options: ["one", "two", "three"], + multiple: true + }); + + Select.vm.open = true; + Select.vm.select("one"); + + expect(Select.vm.open).toEqual(false); + }); + + it("does not close the dropdown when the el is clicked, multiple is true, and closeOnSelect option is false", () => { + const Select = selectWithProps({ + value: [], + options: ["one", "two", "three"], + multiple: true, + closeOnSelect: false + }); + // const vm = new Vue({ + // template: + // '
', + // components: { vSelect }, + // }).$mount(); + + Select.vm.open = true; + Select.vm.select("one"); + + expect(Select.vm.open).toEqual(true); + }); + + it("should close the dropdown on search blur", () => { + const Select = selectWithProps({ + options: [{ label: "one" }] + }); + + Select.vm.open = true; + Select.find({ ref: "search" }).trigger("blur"); + + expect(Select.vm.open).toEqual(false); + }); + + it("will close the dropdown and emit the search:blur event from onSearchBlur", () => { + const Select = selectWithProps(); + const spy = jest.spyOn(Select.vm, "$emit"); + + Select.vm.open = true; + Select.vm.onSearchBlur(); + + expect(Select.vm.open).toEqual(false); + expect(spy).toHaveBeenCalledWith("search:blur"); + }); + + it("will open the dropdown and emit the search:focus event from onSearchFocus", () => { + const Select = selectWithProps(); + const spy = jest.spyOn(Select.vm, "$emit"); + + Select.vm.onSearchFocus(); + + expect(Select.vm.open).toEqual(true); + expect(spy).toHaveBeenCalledWith("search:focus"); + }); + + it("will close the dropdown on escape, if search is empty", () => { + const Select = selectWithProps(); + const spy = jest.spyOn(Select.vm.$refs.search, "blur"); + + Select.vm.open = true; + Select.vm.onEscape(); + + expect(spy).toHaveBeenCalled(); + }); + + it("should remove existing search text on escape keyup", () => { + const Select = selectWithProps({ + value: [{ label: "one" }], + options: [{ label: "one" }] + }); + + Select.vm.search = "foo"; + Select.vm.onEscape(); + expect(Select.vm.search).toEqual(""); + }); + + it("should have an open class when dropdown is active", () => { + const Select = selectWithProps(); + + expect(Select.vm.dropdownClasses.open).toEqual(false); + + Select.vm.open = true; + expect(Select.vm.dropdownClasses.open).toEqual(true); + }); +}); diff --git a/tests/unit/Filtering.spec.js b/tests/unit/Filtering.spec.js new file mode 100755 index 0000000..933fde8 --- /dev/null +++ b/tests/unit/Filtering.spec.js @@ -0,0 +1,75 @@ +import { shallowMount } from "@vue/test-utils"; +import VueSelect from "../../src/components/Select"; + +describe("Filtering Options", () => { + it("should filter an array of strings", () => { + const Select = shallowMount(VueSelect, { + propsData: { options: ["foo", "bar", "baz"] } + }); + Select.vm.search = "ba"; + expect(Select.vm.filteredOptions).toEqual(["bar", "baz"]); + }); + + it("should not filter the array of strings if filterable is false", () => { + const Select = shallowMount(VueSelect, { + propsData: { options: ["foo", "bar", "baz"], filterable: false } + }); + Select.vm.search = "ba"; + expect(Select.vm.filteredOptions).toEqual(["foo", "bar", "baz"]); + }); + + it("should filter without case-sensitivity", () => { + const Select = shallowMount(VueSelect, { + propsData: { options: ["Foo", "Bar", "Baz"] } + }); + Select.vm.search = "ba"; + expect(Select.vm.filteredOptions).toEqual(["Bar", "Baz"]); + }); + + it("can filter an array of objects based on the objects label key", () => { + const Select = shallowMount(VueSelect, { + propsData: { + options: [{ label: "Foo" }, { label: "Bar" }, { label: "Baz" }] + } + }); + Select.vm.search = "ba"; + expect(Select.vm.filteredOptions).toEqual([ + { label: "Bar" }, + { label: "Baz" } + ]); + }); + + it("can determine if a given option should match the current search text", () => { + const Select = shallowMount(VueSelect, { + propsData: { + options: [{ label: "Aoo" }, { label: "Bar" }, { label: "Baz" }], + filterBy: (option, label, search) => + label.match(new RegExp("^" + search, "i")) + } + }); + + Select.vm.search = "a"; + expect(Select.vm.filteredOptions).toEqual([{ label: "Aoo" }]); + }); + + it("can use a custom filtering method", () => { + const Select = shallowMount(VueSelect, { + propsData: { + options: ["foo", "bar", "baz"], + filterBy: (option, label) => label.includes("o") + } + }); + Select.vm.search = "a"; + expect(Select.vm.filteredOptions).toEqual(["foo"]); + }); + + it("can filter arrays of numbers", () => { + const Select = shallowMount(VueSelect, { + propsData: { + options: [1, 5, 10] + } + }); + Select.vm.search = "1"; + expect(Select.vm.filteredOptions).toEqual([1, 10]); + }); +}); diff --git a/tests/unit/Labels.spec.js b/tests/unit/Labels.spec.js new file mode 100755 index 0000000..0554be0 --- /dev/null +++ b/tests/unit/Labels.spec.js @@ -0,0 +1,42 @@ +import VueSelect from "../../src/components/Select"; +import { shallowMount } from "@vue/test-utils"; +import { selectWithProps } from "../helpers"; + +describe("Labels", () => { + it("can generate labels using a custom label key", () => { + const Select = selectWithProps({ + options: [{ name: "Foo" }], + label: "name", + value: { name: "Foo" } + }); + expect(Select.find(".selected-tag").text()).toBe("Foo"); + }); + + it("will console.warn when options contain objects without a valid label key", () => { + const spy = jest.spyOn(console, "warn").mockImplementation(() => {}); + const Select = selectWithProps({ + options: [{}] + }); + + Select.vm.open = true; + expect(spy).toHaveBeenCalledWith( + '[vue-select warn]: Label key "option.label" does not exist in options object {}.' + + "\nhttp://sagalbot.github.io/vue-select/#ex-labels" + ); + }); + + it("should display a placeholder if the value is empty", () => { + const Select = shallowMount(VueSelect, { + propsData: { + options: ["one"] + }, + attrs: { + placeholder: "foo" + } + }); + + expect(Select.vm.searchPlaceholder).toEqual("foo"); + Select.vm.mutableValue = "one"; + expect(Select.vm.searchPlaceholder).not.toBeDefined(); + }); +}); diff --git a/tests/unit/Layout.spec.js b/tests/unit/Layout.spec.js new file mode 100755 index 0000000..9fb84fb --- /dev/null +++ b/tests/unit/Layout.spec.js @@ -0,0 +1,43 @@ +import { shallowMount } from "@vue/test-utils"; +import VueSelect from "../../src/components/Select"; + +describe("Single value options", () => { + it("should reset the search input on focus lost", () => { + const Select = shallowMount(VueSelect); + Select.vm.open = true; + + Select.vm.search = "t"; + expect(Select.vm.search).toEqual("t"); + + Select.vm.onSearchBlur(); + expect(Select.vm.search).toEqual(""); + }); + + it('should apply the "hidden" class to the search input when a value is present', () => { + const Select = shallowMount(VueSelect, { propsData: { value: "foo" } }); + expect(Select.vm.inputClasses.hidden).toEqual(true); + }); + + it('should not apply the "hidden" class to the search input when a value is present, and the dropdown is open', () => { + const Select = shallowMount(VueSelect, { propsData: { value: "foo" } }); + Select.vm.toggleDropdown({ target: Select.vm.$refs.search }); + + expect(Select.vm.open).toEqual(true); + expect(Select.vm.inputClasses.hidden).toEqual(false); + }); + + it("should not reset the search input on focus lost when clearSearchOnSelect is false", () => { + const Select = shallowMount(VueSelect, { + propsData: { value: "foo", clearSearchOnSelect: false } + }); + + expect(Select.vm.clearSearchOnSelect).toEqual(false); + + Select.vm.open = true; + Select.vm.search = "t"; + expect(Select.vm.search).toEqual("t"); + + Select.vm.onSearchBlur(); + expect(Select.vm.search).toEqual("t"); + }); +}); diff --git a/tests/unit/ObjectIndex.spec.js b/tests/unit/ObjectIndex.spec.js new file mode 100755 index 0000000..530dc1a --- /dev/null +++ b/tests/unit/ObjectIndex.spec.js @@ -0,0 +1,215 @@ +import { mount, shallowMount } from "@vue/test-utils"; +import VueSelect from "../../src/components/Select"; + +describe("When index prop is defined", () => { + it("can accept an array of objects and pre-selected value (single)", () => { + const Select = shallowMount(VueSelect, { + propsData: { + index: "value", + value: "foo", + options: [{ label: "This is Foo", value: "foo" }] + } + }); + expect(Select.vm.mutableValue).toEqual("foo"); + }); + + it("can determine if an object is pre-selected", () => { + const Select = shallowMount(VueSelect, { + propsData: { + index: "id", + value: "foo", + options: [ + { + id: "foo", + label: "This is Foo" + } + ] + } + }); + + expect( + Select.vm.isOptionSelected({ + id: "foo", + label: "This is Foo" + }) + ).toEqual(true); + }); + + it("can determine if an object is selected after it has been chosen", () => { + const Select = shallowMount(VueSelect, { + propsData: { + index: "id", + options: [{ id: "foo", label: "FooBar" }] + } + }); + + Select.vm.select({ id: "foo", label: "FooBar" }); + + expect( + Select.vm.isOptionSelected({ + id: "foo", + label: "This is Foo" + }) + ).toEqual(true); + }); + + it("can accept an array of objects and pre-selected values (multiple)", () => { + const Select = shallowMount(VueSelect, { + propsData: { + multiple: true, + index: "value", + value: ["foo", "bar"], + options: [ + { label: "This is Foo", value: "foo" }, + { label: "This is Bar", value: "bar" } + ] + } + }); + + expect(Select.vm.mutableValue).toEqual(["foo", "bar"]); + }); + + it("can deselect a pre-selected object", () => { + const Select = shallowMount(VueSelect, { + propsData: { + multiple: true, + index: "value", + value: ["foo", "bar"], + options: [ + { label: "This is Foo", value: "foo" }, + { label: "This is Bar", value: "bar" } + ] + } + }); + + Select.vm.deselect("foo"); + expect(Select.vm.mutableValue.length).toEqual(1); + expect(Select.vm.mutableValue).toEqual(["bar"]); + }); + + it("can deselect an option when multiple is false", () => { + const Select = shallowMount(VueSelect, { + propsData: { + index: "value", + value: "foo", + options: [ + { label: "This is Foo", value: "foo" }, + { label: "This is Bar", value: "bar" } + ] + } + }); + + Select.vm.deselect("foo"); + expect(Select.vm.mutableValue).toEqual(null); + }); + + it("can use v-model syntax for a two way binding to a parent component", () => { + const Parent = mount({ + data: () => ({ + index: "value", + value: "foo", + options: [ + { label: "This is Foo", value: "foo" }, + { label: "This is Bar", value: "bar" }, + { label: "This is Baz", value: "baz" } + ] + }), + template: `
`, + components: { "v-select": VueSelect } + }); + const Select = Parent.find(VueSelect); + + expect(Select.vm.value).toEqual("foo"); + expect(Select.vm.mutableValue).toEqual("foo"); + + Select.vm.mutableValue = "bar"; + expect(Parent.vm.value).toEqual("bar"); + }); + + it("can generate labels using a custom label key", () => { + const Select = shallowMount(VueSelect, { + propsData: { + multiple: true, + index: "value", + value: ["baz"], + label: "name", + options: [{ value: "foo", name: "Foo" }, { value: "baz", name: "Baz" }] + } + }); + + expect(Select.find(".selected-tag").text()).toContain("Baz"); + }); + + it("will console.warn when attempting to select an option with an undefined index", () => { + const spy = jest.spyOn(console, "warn").mockImplementation(() => {}); + const Select = shallowMount(VueSelect, { + propsData: { + index: "value", + options: [{ label: "Foo" }] + } + }); + + Select.vm.select({ label: "Foo" }); + expect(spy).toHaveBeenCalledWith( + `[vue-select warn]: Index key "option.value" does not exist in options object {"label":"Foo"}.` + ); + }); + + it("can find the original option within this.options", () => { + const optionToFind = { id: 1, label: "Foo" }; + const Select = shallowMount(VueSelect, { + propsData: { + index: "id", + options: [optionToFind, { id: 2, label: "Bar" }] + } + }); + + expect(Select.vm.findOptionByIndexValue(1)).toEqual(optionToFind); + expect(Select.vm.findOptionByIndexValue(optionToFind)).toEqual( + optionToFind + ); + }); + + describe("And when option[index] is a nested object", () => { + it("can determine if an object is pre-selected", () => { + const nestedOption = { value: { nested: true }, label: "foo" }; + const Select = shallowMount(VueSelect, { + propsData: { + index: "value", + value: { + nested: true + }, + options: [nestedOption] + } + }); + + expect(Select.vm.isOptionSelected({ nested: true })).toEqual(true); + }); + + it("can determine if an object is selected after it is chosen", () => { + const nestedOption = { value: { nested: true }, label: "foo" }; + const Select = shallowMount(VueSelect, { + propsData: { + index: "value", + options: [nestedOption] + } + }); + + Select.vm.select(nestedOption); + expect(Select.vm.isOptionSelected(nestedOption)).toEqual(true); + }); + + it("can determine a selected values label", () => { + const nestedOption = { value: { nested: true }, label: "foo" }; + const Select = shallowMount(VueSelect, { + propsData: { + index: "value", + value: { nested: true }, + options: [nestedOption] + } + }); + + expect(Select.vm.getOptionLabel({ nested: true })).toEqual("foo"); + }); + }); +}); diff --git a/tests/unit/ReactiveOptions.spec.js b/tests/unit/ReactiveOptions.spec.js new file mode 100755 index 0000000..d73b72f --- /dev/null +++ b/tests/unit/ReactiveOptions.spec.js @@ -0,0 +1,20 @@ +import { shallowMount } from "@vue/test-utils"; +import VueSelect from "../../src/components/Select"; + +describe("Reset on options change", () => { + it("should not reset the selected value by default when the options property changes", () => { + const Select = shallowMount(VueSelect, { + propsData: { value: "one", options: ["one"] } + }); + Select.vm.mutableOptions = ["four", "five", "six"]; + expect(Select.vm.mutableValue).toEqual("one"); + }); + + it("should reset the selected value when the options property changes", () => { + const Select = shallowMount(VueSelect, { + propsData: { resetOnOptionsChange: true, value: "one", options: ["one"] } + }); + Select.vm.mutableOptions = ["four", "five", "six"]; + expect(Select.vm.mutableValue).toEqual(null); + }); +}); diff --git a/tests/unit/Selecting.spec.js b/tests/unit/Selecting.spec.js new file mode 100755 index 0000000..af49b9f --- /dev/null +++ b/tests/unit/Selecting.spec.js @@ -0,0 +1,217 @@ +import { mount, shallowMount } from "@vue/test-utils"; +import VueSelect from "@/components/Select.vue"; + +describe("VS - Selecting Values", () => { + let defaultProps; + + beforeEach(() => { + defaultProps = { + value: "one", + options: ["one", "two", "three"] + }; + }); + + it("can accept an array with pre-selected values", () => { + const Select = shallowMount(VueSelect, { + propsData: defaultProps + }); + expect(Select.mutableValue).toEqual(Select.value); + }); + + it("can accept an array of objects and pre-selected value (single)", () => { + const Select = shallowMount(VueSelect, { + propsData: { + value: { label: "This is Foo", value: "foo" }, + options: [ + { label: "This is Foo", value: "foo" }, + { label: "This is Bar", value: "bar" } + ] + } + }); + expect(Select.mutableValue).toEqual(Select.value); + }); + + it("can accept an array of objects and pre-selected values (multiple)", () => { + const Select = shallowMount(VueSelect, { + propsData: { + value: [ + { label: "This is Foo", value: "foo" }, + { label: "This is Bar", value: "bar" } + ], + options: [ + { label: "This is Foo", value: "foo" }, + { label: "This is Bar", value: "bar" } + ] + }, + multiple: true + }); + + expect(Select.mutableValue).toEqual(Select.value); + }); + + it("can select an option on tab", () => { + const Select = shallowMount(VueSelect, { + propsData: { + selectOnTab: true + } + }); + + const spy = jest.spyOn(Select.vm, "typeAheadSelect"); + + Select.find({ ref: "search" }).trigger("keydown", { + keyCode: 9 + }); + + expect(spy).toHaveBeenCalledWith(); + }); + + it("can deselect a pre-selected object", () => { + const Select = shallowMount(VueSelect, { + propsData: { + multiple: true, + value: [ + { label: "This is Foo", value: "foo" }, + { label: "This is Bar", value: "bar" } + ], + options: [ + { label: "This is Foo", value: "foo" }, + { label: "This is Bar", value: "bar" } + ] + } + }); + + Select.vm.deselect({ label: "This is Foo", value: "foo" }); + expect(Select.vm.mutableValue.length).toEqual(1); + }); + + it("can deselect a pre-selected string", () => { + const Select = shallowMount(VueSelect, { + propsData: { + multiple: true, + value: ["foo", "bar"], + options: ["foo", "bar"] + } + }); + Select.vm.deselect("foo"); + expect(Select.vm.mutableValue.length).toEqual(1); + }); + + it("can deselect an option when multiple is false", () => { + const Select = shallowMount(VueSelect, { + propsData: { + value: "foo" + } + }); + Select.vm.deselect("foo"); + expect(Select.vm.mutableValue).toEqual(null); + }); + + it("can determine if the value prop is empty", () => { + const Select = shallowMount(VueSelect, { + propsData: { + value: [], + options: ["one", "two", "three"] + } + }); + + const select = Select.vm; + expect(select.isValueEmpty).toEqual(true); + + select.select(["one"]); + expect(select.isValueEmpty).toEqual(false); + + select.select("one"); + expect(select.isValueEmpty).toEqual(false); + + select.select({ label: "foo", value: "foo" }); + expect(select.isValueEmpty).toEqual(false); + + select.select(""); + expect(select.isValueEmpty).toEqual(true); + + select.select(null); + expect(select.isValueEmpty).toEqual(true); + }); + + it("should reset the selected values when the multiple property changes", () => { + const Select = shallowMount(VueSelect, { + propsData: { + value: ["one"], + multiple: true, + options: ["one", "two", "three"] + } + }); + + expect(Select.vm.mutableValue).toEqual(["one"]); + + Select.setProps({ multiple: false }); + expect(Select.vm.mutableValue).toEqual(null); + + Select.setProps({ multiple: true }); + expect(Select.vm.mutableValue).toEqual([]); + }); + + it("can retain values present in a new array of options", () => { + const Select = shallowMount(VueSelect, { + propsData: { + value: ["one"], + options: ["one", "two", "three"] + } + }); + + Select.setProps({ options: ["one", "five", "six"] }); + expect(Select.vm.mutableValue).toEqual(["one"]); + }); + + it("can determine if an object is already selected", () => { + const Select = shallowMount(VueSelect, { + propsData: { + value: [{ label: "one" }], + options: [{ label: "one" }] + } + }); + + expect(Select.vm.isOptionSelected({ label: "one" })).toEqual(true); + }); + + it("can use v-model syntax for a two way binding to a parent component", () => { + const Parent = mount({ + data: () => ({ value: "foo", options: ["foo", "bar", "baz"] }), + template: `
`, + components: { "v-select": VueSelect } + }); + const Select = Parent.find(VueSelect); + + expect(Select.vm.value).toEqual("foo"); + expect(Select.vm.mutableValue).toEqual("foo"); + + Select.vm.mutableValue = "bar"; + expect(Parent.vm.value).toEqual("bar"); + }); + + it("can check if a string value is selected when the value is an object and multiple is true", () => { + const Select = shallowMount(VueSelect, { + propsData: { + multiple: true, + value: [{ label: "foo", value: "bar" }] + } + }); + expect(Select.vm.isOptionSelected("foo")).toEqual(true); + }); + + describe("change Event", () => { + it("will trigger the input event when the selection changes", () => { + const Select = shallowMount(VueSelect); + Select.vm.$data.mutableValue = "bar"; + expect(Select.emitted("input")[0]).toEqual(["bar"]); + }); + + it("should run change when multiple is true and the value changes", () => { + const Select = shallowMount(VueSelect, { + propsData: { multiple: true, value: ["foo"], options: ["foo", "bar"] } + }); + Select.vm.$data.mutableValue = ["bar"]; + expect(Select.emitted("input")[0]).toEqual([["bar"]]); + }); + }); +}); diff --git a/tests/unit/Tagging.spec.js b/tests/unit/Tagging.spec.js new file mode 100755 index 0000000..e8b2d88 --- /dev/null +++ b/tests/unit/Tagging.spec.js @@ -0,0 +1,224 @@ +import { searchSubmit, selectWithProps } from "../helpers"; + +describe("When Tagging Is Enabled", () => { + it("can determine if a given option string already exists", () => { + const Select = selectWithProps({ taggable: true, options: ["one", "two"] }); + expect(Select.vm.optionExists("one")).toEqual(true); + expect(Select.vm.optionExists("three")).toEqual(false); + }); + + it("can determine if a given option object already exists", () => { + const Select = selectWithProps({ + taggable: true, + options: [{ label: "one" }, { label: "two" }] + }); + + expect(Select.vm.optionExists("one")).toEqual(true); + expect(Select.vm.optionExists("three")).toEqual(false); + }); + + it("can determine if a given option object already exists when using custom labels", () => { + const Select = selectWithProps({ + taggable: true, + options: [{ foo: "one" }, { foo: "two" }], + label: "foo" + }); + + expect(Select.vm.optionExists("one")).toEqual(true); + expect(Select.vm.optionExists("three")).toEqual(false); + }); + + it("can add the current search text as the first item in the options list", () => { + const Select = selectWithProps({ + taggable: true, + multiple: true, + value: ["one"], + options: ["one", "two"] + }); + + Select.vm.search = "three"; + expect(Select.vm.filteredOptions).toEqual(["three"]); + }); + + it("can select the current search text as a string", () => { + const Select = selectWithProps({ + taggable: true, + multiple: true, + value: ["one"], + options: ["one", "two"] + }); + + searchSubmit(Select, "three"); + expect(Select.vm.mutableValue).toEqual(["one", "three"]); + }); + + it("can select the current search text as an object", () => { + const Select = selectWithProps({ + taggable: true, + multiple: true, + value: [{ label: "one" }], + options: [{ label: "one" }] + }); + + searchSubmit(Select, "two"); + expect(Select.vm.mutableValue).toEqual([ + { label: "one" }, + { label: "two" } + ]); + }); + + it("should add a freshly created option/tag to the options list when pushTags is true", () => { + const Select = selectWithProps({ + pushTags: true, + taggable: true, + multiple: true, + value: ["one"], + options: ["one", "two"] + }); + + searchSubmit(Select, "three"); + expect(Select.vm.mutableOptions).toEqual(["one", "two", "three"]); + }); + + it("should add a freshly created option/tag to the options list when pushTags is true and filterable is false", () => { + const Select = selectWithProps({ + filterable: false, + pushTags: true, + taggable: true, + multiple: true, + value: ["one"], + options: ["one", "two"] + }); + + searchSubmit(Select, "three"); + expect(Select.vm.mutableOptions).toEqual(["one", "two", "three"]); + expect(Select.vm.filteredOptions).toEqual(["one", "two", "three"]); + }); + + it("wont add a freshly created option/tag to the options list when pushTags is false", () => { + const Select = selectWithProps({ + pushTags: false, + taggable: true, + multiple: true, + value: ["one"], + options: ["one", "two"] + }); + + searchSubmit(Select, "three"); + expect(Select.vm.mutableOptions).toEqual(["one", "two"]); + }); + + it("wont add a freshly created option/tag to the options list when pushTags is false and filterable is false", () => { + const Select = selectWithProps({ + filterable: false, + pushTags: false, + taggable: true, + multiple: true, + value: ["one"], + options: ["one", "two"] + }); + + searchSubmit(Select, "three"); + expect(Select.vm.mutableOptions).toEqual(["one", "two"]); + expect(Select.vm.filteredOptions).toEqual(["one", "two"]); + }); + + it("should select an existing option if the search string matches a string from options", () => { + let two = "two"; + const Select = selectWithProps({ + taggable: true, + multiple: true, + value: null, + options: ["one", two] + }); + + Select.vm.search = "two"; + + searchSubmit(Select); + + expect(Select.vm.mutableValue[0]).toBe(two); + }); + + it("should select an existing option if the search string matches an objects label from options", () => { + let two = { label: "two" }; + const Select = selectWithProps({ + taggable: true, + options: [{ label: "one" }, two] + }); + + Select.vm.search = "two"; + + searchSubmit(Select); + expect(Select.vm.mutableValue.label).toBe(two.label); + }); + + it("should select an existing option if the search string matches an objects label from options when filter-options is false", () => { + let two = { label: "two" }; + const Select = selectWithProps({ + taggable: true, + filterable: false, + options: [{ label: "one" }, two] + }); + + Select.vm.search = "two"; + + searchSubmit(Select); + expect(Select.vm.mutableValue.label).toBe(two.label); + }); + + it("should not reset the selected value when the options property changes", () => { + const Select = selectWithProps({ + taggable: true, + multiple: true, + value: [{ label: "one" }], + options: [{ label: "one" }] + }); + + Select.vm.mutableOptions = [{ label: "two" }]; + expect(Select.vm.mutableValue).toEqual([{ label: "one" }]); + }); + + it("should not reset the selected value when the options property changes when filterable is false", () => { + const Select = selectWithProps({ + taggable: true, + multiple: true, + filterable: false, + value: [{ label: "one" }], + options: [{ label: "one" }] + }); + + Select.vm.mutableOptions = [{ label: "two" }]; + expect(Select.vm.mutableValue).toEqual([{ label: "one" }]); + }); + + it("should not allow duplicate tags when using string options", () => { + const Select = selectWithProps({ + taggable: true, + multiple: true + }); + + searchSubmit(Select, "one"); + expect(Select.vm.mutableValue).toEqual(["one"]); + expect(Select.vm.search).toEqual(""); + + searchSubmit(Select, "one"); + expect(Select.vm.mutableValue).toEqual(["one"]); + expect(Select.vm.search).toEqual(""); + }); + + it("should not allow duplicate tags when using object options", () => { + const Select = selectWithProps({ + taggable: true, + multiple: true, + options: [{ label: "two" }] + }); + + searchSubmit(Select, "one"); + expect(Select.vm.mutableValue).toEqual([{ label: "one" }]); + expect(Select.vm.search).toEqual(""); + + searchSubmit(Select, "one"); + expect(Select.vm.mutableValue).toEqual([{ label: "one" }]); + expect(Select.vm.search).toEqual(""); + }); +}); diff --git a/tests/unit/TypeAhead.spec.js b/tests/unit/TypeAhead.spec.js new file mode 100755 index 0000000..9d4a1c6 --- /dev/null +++ b/tests/unit/TypeAhead.spec.js @@ -0,0 +1,147 @@ +import { shallowMount } from "@vue/test-utils"; +import VueSelect from "../../src/components/Select"; + +describe("Moving the Typeahead Pointer", () => { + const mountDefault = () => + shallowMount(VueSelect, { + propsData: { options: ["one", "two", "three"] } + }); + + it("should set the pointer to zero when the filteredOptions change", () => { + const Select = mountDefault(); + Select.vm.search = "two"; + expect(Select.vm.typeAheadPointer).toEqual(0); + }); + + it("should move the pointer visually up the list on up arrow keyDown", () => { + const Select = mountDefault(); + + Select.vm.typeAheadPointer = 1; + + Select.find({ ref: "search" }).trigger("keydown", { keyCode: 38 }); + + expect(Select.vm.typeAheadPointer).toEqual(0); + }); + + it("should move the pointer visually down the list on down arrow keyDown", () => { + const Select = mountDefault(); + + Select.vm.typeAheadPointer = 1; + + Select.find({ ref: "search" }).trigger("keydown", { keyCode: 40 }); + + expect(Select.vm.typeAheadPointer).toEqual(2); + }); + + it("should not move the pointer past the end of the list", () => { + const Select = mountDefault(); + + Select.vm.typeAheadPointer = 2; + Select.vm.typeAheadDown(); + expect(Select.vm.typeAheadPointer).toEqual(2); + }); + + describe("Automatic Scrolling", () => { + it("should check if the scroll position needs to be adjusted on up arrow keyDown", () => { + const Select = mountDefault(); + const spy = jest.spyOn(Select.vm, "maybeAdjustScroll"); + + Select.vm.typeAheadPointer = 1; + + Select.find({ ref: "search" }).trigger("keydown", { keyCode: 38 }); + expect(spy).toHaveBeenCalled(); + }); + + it("should check if the scroll position needs to be adjusted on down arrow keyDown", () => { + const Select = mountDefault(); + const spy = jest.spyOn(Select.vm, "maybeAdjustScroll"); + + Select.vm.typeAheadPointer = 1; + + Select.find({ ref: "search" }).trigger("keydown", { keyCode: 40 }); + expect(spy).toHaveBeenCalled(); + }); + + it("should check if the scroll position needs to be adjusted when filtered options changes", () => { + const Select = mountDefault(); + const spy = jest.spyOn(Select.vm, "maybeAdjustScroll"); + + Select.vm.search = "two"; + + expect(spy).toHaveBeenCalled(); + }); + + it("should scroll up if the pointer is above the current viewport bounds", () => { + const Select = mountDefault(); + const spy = jest.spyOn(Select.vm, "scrollTo"); + + Select.setMethods({ + pixelsToPointerTop() { + return 1; + }, + viewport() { + return { top: 2, bottom: 0 }; + } + }); + + Select.vm.maybeAdjustScroll(); + + expect(spy).toHaveBeenCalledWith(1); + }); + + it("should scroll down if the pointer is below the current viewport bounds", () => { + const Select = mountDefault(); + const spy = jest.spyOn(Select.vm, "scrollTo"); + + Select.setMethods({ + pixelsToPointerBottom() { + return 2; + }, + viewport() { + return { top: 0, bottom: 1 }; + } + }); + + Select.vm.maybeAdjustScroll(); + expect(spy).toHaveBeenCalledWith( + Select.vm.viewport().top + Select.vm.pointerHeight() + ); + }); + }); + + describe("Measuring pixel distances", () => { + it("should calculate pointerHeight as the offsetHeight of the pointer element if it exists", () => { + const Select = mountDefault(); + + // Drop down must be open for $refs to exist + Select.vm.open = true; + + /** + * Since JSDom doesn't render layouts, set the offsetHeight explicitly + * to 25px for each list item. + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty + */ + let i = 0; + for (let option of Select.vm.$refs.dropdownMenu.children) { + Object.defineProperty(option, "offsetHeight", { + value: 1 + i + }); + i++; + } + + // Fresh instances start with the pointer at -1 + Select.vm.typeAheadPointer = -1; + expect(Select.vm.pointerHeight()).toEqual(0); + + Select.vm.typeAheadPointer = 0; + expect(Select.vm.pointerHeight()).toEqual(1); + + Select.vm.typeAheadPointer = 1; + expect(Select.vm.pointerHeight()).toEqual(2); + + Select.vm.typeAheadPointer = 2; + expect(Select.vm.pointerHeight()).toEqual(3); + }); + }); +});