Skip to content

Commit

Permalink
Merge pull request galaxyproject#17059 from ElectronicBlueberry/many-…
Browse files Browse the repository at this point in the history
…select

New select component for selecting a large amount of options
  • Loading branch information
davelopez authored Nov 24, 2023
2 parents 34d4e39 + 1ef360c commit 61b7575
Show file tree
Hide file tree
Showing 13 changed files with 1,417 additions and 12 deletions.
4 changes: 4 additions & 0 deletions client/src/components/Form/Elements/FormSelect.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createTestingPinia } from "@pinia/testing";
import { mount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";

Expand All @@ -6,9 +7,12 @@ import MountTarget from "./FormSelection.vue";
const localVue = getLocalVue(true);

function createTarget(propsData) {
const pinia = createTestingPinia();

return mount(MountTarget, {
localVue,
propsData,
pinia,
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
import "./worker/__mocks__/selectMany";

import { createTestingPinia } from "@pinia/testing";
import { getLocalVue } from "@tests/jest/helpers";
import { mount } from "@vue/test-utils";
import { PropType } from "vue";

import type { SelectOption } from "./worker/selectMany";

import FormSelectMany from "./FormSelectMany.vue";

const pinia = createTestingPinia();
const localVue = getLocalVue();

jest.mock("@/components/Form/Elements/FormSelectMany/worker/selectMany");

function mountSelectMany(props: Partial<PropType<typeof FormSelectMany>>) {
return mount(FormSelectMany as any, {
propsData: { options: [], value: [], ...props },
pinia,
localVue,
});
}

const selectors = {
unselectedOptions: ".options-list.unselected > button",
unselectedHighlighted: ".options-list.unselected > button.highlighted",
selectedOptions: ".options-list:not(.unselected) > button",
selectedHighlighted: ".options-list:not(.unselected) > button.highlighted",
selectAll: ".selection-button.select",
deselectAll: ".selection-button.deselect",
selectedCount: ".selected-count",
unselectedCount: ".unselected-count",
search: "input[type=search]",
caseSensitivity: ".toggle-button.case-sensitivity",
useRegex: ".toggle-button.use-regex",
} as const;

function generateOptionsFromArrays(matrix: Array<Array<string>>): SelectOption[] {
const combineTwo = (a: string[], b: string[]) => {
const combined = [] as string[];

a.forEach((aValue) => {
b.forEach((bValue) => {
combined.push(`${aValue}${bValue}`);
});
});

return combined;
};

const combined = matrix.reduce((accumulator, current) => combineTwo(accumulator, current), [""]);

return combined.map((v) => ({ label: v, value: v }));
}

/** gets the latest input event value and reflects it to props */
async function emittedInput(wrapper: ReturnType<typeof mountSelectMany>) {
const emittedEvents = wrapper.emitted()?.["input"];

if (!emittedEvents) {
return undefined;
}

const latestValue = emittedEvents[emittedEvents.length - 1]?.[0];

if (latestValue === undefined) {
return undefined;
}

await wrapper.setProps({ ...wrapper.props(), value: latestValue });
return latestValue;
}

// circumvent input debounce
jest.useFakeTimers();

async function search(wrapper: ReturnType<typeof mountSelectMany>, value: string) {
const searchInput = wrapper.find(selectors.search);
await searchInput.setValue(value);
jest.runAllTimers();
}

describe("FormSelectMany", () => {
it("displays all options", async () => {
const options = generateOptionsFromArrays([["foo", "bar", "baz"], ["@"], ["galaxy"], [".com", ".org"]]);
const wrapper = mountSelectMany({ options });

const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);
expect(unselectedOptions.length).toBe(6);

options.forEach((option, i) => {
expect(unselectedOptions.at(i).text()).toBe(option.label);
});
});

it("emits selected options", async () => {
const options = generateOptionsFromArrays([["foo", "bar", "baz"], ["@"], ["galaxy"], [".com", ".org"]]);

const wrapper = mountSelectMany({ options });

{
const firstOption = wrapper.findAll(selectors.unselectedOptions).at(0);
await firstOption.trigger("click");

const emitted = await emittedInput(wrapper);
expect(emitted).toEqual(["[email protected]"]);
}

{
const firstOption = wrapper.findAll(selectors.unselectedOptions).at(0);
await firstOption.trigger("click");

const emitted = await emittedInput(wrapper);
expect(emitted).toEqual(["[email protected]", "[email protected]"]);
}
});

it("displays selected values in the selected column", async () => {
const options = generateOptionsFromArrays([["foo", "bar", "baz"], ["@"], ["galaxy"], [".com", ".org"]]);
const wrapper = mountSelectMany({ options, value: ["[email protected]", "[email protected]"] });

{
const selectedOptions = wrapper.findAll(selectors.selectedOptions);
expect(selectedOptions.length).toBe(2);
expect(selectedOptions.at(0).text()).toBe("[email protected]");
expect(selectedOptions.at(1).text()).toBe("[email protected]");

const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);
unselectedOptions.wrappers.forEach((unselectedOption) => {
expect(unselectedOption.text()).not.toBe("[email protected]");
expect(unselectedOption.text()).not.toBe("[email protected]");
});
}

const firstOption = wrapper.findAll(selectors.unselectedOptions).at(0);
await firstOption.trigger("click");
const emitted = await emittedInput(wrapper);

{
const selectedOptions = wrapper.findAll(selectors.selectedOptions);
expect(selectedOptions.length).toBe(3);
expect(selectedOptions.at(2).text()).toBe(emitted[2]);

const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);
unselectedOptions.wrappers.forEach((unselectedOption) => {
expect(unselectedOption.text()).not.toBe(emitted[2]);
});
}
});

it("shows the amount of selected options", async () => {
const options = generateOptionsFromArrays([["foo", "bar", "baz"], ["@"], ["galaxy"], [".com", ".org"]]);
const wrapper = mountSelectMany({ options, value: ["[email protected]", "[email protected]"] });

{
const selectedCount = wrapper.find(selectors.selectedCount);
const unselectedCount = wrapper.find(selectors.unselectedCount);

expect(selectedCount.text()).toBe("(2)");
expect(unselectedCount.text()).toBe("(4)");
}

const firstOption = wrapper.findAll(selectors.unselectedOptions).at(0);
await firstOption.trigger("click");
await emittedInput(wrapper);

{
const selectedCount = wrapper.find(selectors.selectedCount);
const unselectedCount = wrapper.find(selectors.unselectedCount);

expect(selectedCount.text()).toBe("(3)");
expect(unselectedCount.text()).toBe("(3)");
}
});

it("selects all options", async () => {
const options = generateOptionsFromArrays([["foo", "bar", "baz"], ["@"], ["galaxy"], [".com", ".org"]]);
const wrapper = mountSelectMany({ options, value: ["[email protected]", "[email protected]"] });

const selectAllButton = wrapper.find(selectors.selectAll);
await selectAllButton.trigger("click");
await emittedInput(wrapper);

{
const selectedOptions = wrapper.findAll(selectors.selectedOptions);
const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);

expect(unselectedOptions.length).toBe(0);
expect(selectedOptions.length).toBe(6);
}

const deselectAllButton = wrapper.find(selectors.deselectAll);
await deselectAllButton.trigger("click");
await emittedInput(wrapper);

{
const selectedOptions = wrapper.findAll(selectors.selectedOptions);
const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);

expect(unselectedOptions.length).toBe(6);
expect(selectedOptions.length).toBe(0);
}
});

it("filters options", async () => {
const options = generateOptionsFromArrays([["foo", "BAR", "baz"], ["@"], ["galaxy"], [".com", ".org"]]);
const wrapper = mountSelectMany({ options });

await search(wrapper, "bar");

{
const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);
expect(unselectedOptions.length).toBe(2);

const unselectedCount = wrapper.find(selectors.unselectedCount);
expect(unselectedCount.text()).toBe("(2)");
}

const caseSensitivityButton = wrapper.find(selectors.caseSensitivity);
await caseSensitivityButton.trigger("click");

{
const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);
expect(unselectedOptions.length).toBe(0);
}

await search(wrapper, "BAR");

{
const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);
expect(unselectedOptions.length).toBe(2);
}

const useRegexButton = wrapper.find(selectors.useRegex);
await useRegexButton.trigger("click");

{
const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);
expect(unselectedOptions.length).toBe(2);
}

await search(wrapper, "^[a-z]+@");

{
const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);
expect(unselectedOptions.length).toBe(4);
}

await caseSensitivityButton.trigger("click");

{
const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);
expect(unselectedOptions.length).toBe(6);
}
});

it("selects filtered", async () => {
const options = generateOptionsFromArrays([["foo", "BAR", "baz"], ["@"], ["galaxy"], [".com", ".org"]]);
const wrapper = mountSelectMany({ options });

await search(wrapper, "bar");

const selectAllButton = wrapper.find(selectors.selectAll);
await selectAllButton.trigger("click");
await emittedInput(wrapper);

await search(wrapper, "");

{
const selectedOptions = wrapper.findAll(selectors.selectedOptions);
expect(selectedOptions.length).toBe(2);
}

await search(wrapper, ".org");

const deselectAllButton = wrapper.find(selectors.deselectAll);
await deselectAllButton.trigger("click");
await emittedInput(wrapper);

await search(wrapper, "");

{
const selectedOptions = wrapper.findAll(selectors.selectedOptions);
expect(selectedOptions.length).toBe(1);
}
});

it("allows for highlighting ranges", async () => {
const options = generateOptionsFromArrays([["foo", "BAR", "baz", "bar"], ["@"], ["galaxy"], [".com", ".org"]]);
const wrapper = mountSelectMany({ options });

{
const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);
await unselectedOptions.at(0).trigger("click", { shiftKey: true });
await unselectedOptions.at(7).trigger("click", { shiftKey: true });

{
const highlightedOptions = wrapper.findAll(selectors.unselectedHighlighted);
expect(highlightedOptions.length).toBe(8);
}

await unselectedOptions.at(1).trigger("click", { ctrlKey: true });
await unselectedOptions.at(2).trigger("click", { ctrlKey: true });

{
const highlightedOptions = wrapper.findAll(selectors.unselectedHighlighted);
expect(highlightedOptions.length).toBe(6);
}
}

const selectAllButton = wrapper.find(selectors.selectAll);
await selectAllButton.trigger("click");
await emittedInput(wrapper);

{
const selectedOptions = wrapper.findAll(selectors.selectedOptions);
expect(selectedOptions.length).toBe(6);

await selectedOptions.at(0).trigger("click", { shiftKey: true });
await selectedOptions.at(5).trigger("click", { shiftKey: true });

{
const highlightedOptions = wrapper.findAll(selectors.selectedHighlighted);
expect(highlightedOptions.length).toBe(6);
}

await selectedOptions.at(2).trigger("click", { shiftKey: true, ctrlKey: true });
await selectedOptions.at(5).trigger("click", { shiftKey: true, ctrlKey: true });

{
const highlightedOptions = wrapper.findAll(selectors.selectedHighlighted);
expect(highlightedOptions.length).toBe(2);
}
}

const deselectAllButton = wrapper.find(selectors.deselectAll);
await deselectAllButton.trigger("click");
await emittedInput(wrapper);

{
const unselectedOptions = wrapper.findAll(selectors.unselectedOptions);
expect(unselectedOptions.length).toBe(4);

const selectedOptions = wrapper.findAll(selectors.selectedOptions);
expect(selectedOptions.length).toBe(4);
}
});
});
Loading

0 comments on commit 61b7575

Please sign in to comment.