Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create reusable FilterMenu with advanced options #16522

Merged
merged 12 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client/src/components/Common/DelayedInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
size="sm"
autocomplete="off"
:placeholder="placeholder"
data-description="filter text input"
@input="delayQuery"
@change="setQuery"
@keydown.esc="setQuery('')" />
Expand Down Expand Up @@ -80,6 +81,11 @@ export default {
this.setQuery(queryNew);
},
},
created() {
if (this.query) {
this.setQuery(this.query);
}
},
methods: {
clearTimer() {
if (this.queryTimer) {
Expand Down
293 changes: 293 additions & 0 deletions client/src/components/Common/FilterMenu.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
import { mount } from "@vue/test-utils";
import { HistoryFilters } from "components/History/HistoryFilters";
import { getLocalVue } from "tests/jest/helpers";
import Filtering, { compare, contains, equals, toBool, toDate } from "utils/filtering";

import FilterMenu from "./FilterMenu";

const localVue = getLocalVue();
const options = [
{ text: "Any", value: "any" },
{ text: "Yes", value: true },
{ text: "No", value: false },
];
const validTestFilters = {
/** A basic name filter (with same placeholder as the key) */
name: { placeholder: "name", type: String, handler: contains("name"), menuItem: true },
/** A filter with different key and placeholder */
filter_key: { placeholder: "item", type: String, handler: contains("filter_key"), menuItem: true },
/** A filter with help component */
has_help: {
placeholder: "value",
type: String,
handler: equals("has_help"),
helpInfo: "Some test help info",
menuItem: true,
},
/** A filter with datalist */
list_item: {
placeholder: "list item",
type: Number,
handler: equals("list_item"),
datalist: ["option1", "option2"],
menuItem: true,
},
/** A ranged date filter */
create_time: {
placeholder: "creation time",
type: Date,
handler: compare("create_time", "le", toDate),
isRangeInput: true,
menuItem: true,
},
/** A ranged number filter */
number: { placeholder: "index", type: Number, handler: equals("number"), isRangeInput: true, menuItem: true },
/** A boolean filter with default boolType */
bool_def: {
placeholder: "Filter by option (any/yes/no)",
type: Boolean,
handler: equals("bool_def", "bool_def", toBool),
menuItem: true,
},
/** A boolean filter with is:filter boolType */
bool_is: {
placeholder: "Filter by option (yes/no)",
type: Boolean,
handler: equals("bool_is", "bool_is", toBool),
menuItem: true,
boolType: "is",
},
/** A valid filter, just not included in menu */
not_included: { handler: contains("not_included"), menuItem: false },
};
const TestFilters = new Filtering(validTestFilters, false);

describe("FilterMenu", () => {
let wrapper;

function setUpWrapper(name, placeholder, filterClass) {
wrapper = mount(FilterMenu, {
propsData: {
name: name,
placeholder: placeholder,
filterClass: filterClass,
filterText: "",
showAdvanced: false,
},
localVue,
stubs: {
icon: { template: "<div></div>" },
},
});
}

async function performSearch() {
// Test: search button (should toggle the view out)
const searchButton = wrapper.find("[data-description='apply filters']");
await searchButton.trigger("click");
}

async function expectCorrectEmits(showAdvanced, filterText, filterClass) {
const filterEmit = wrapper.emitted()["update:filter-text"].length - 1;
const toggleEmit = wrapper.emitted()["update:show-advanced"].length - 1;
expect(wrapper.emitted()["update:show-advanced"][toggleEmit][0]).toEqual(showAdvanced);
await wrapper.setProps({ showAdvanced: wrapper.emitted()["update:show-advanced"][toggleEmit][0] });
const receivedText = wrapper.emitted()["update:filter-text"][filterEmit][0];
const receivedDict = filterClass.getQueryDict(receivedText);
const parsedDict = filterClass.getQueryDict(filterText);
expect(receivedDict).toEqual(parsedDict);
}

it("test generic test items filter panel search", async () => {
setUpWrapper("Test Items", "search test items", TestFilters);
const validFilters = wrapper.vm.$props.filterClass.validFilters;

await wrapper.setProps({ showAdvanced: true });

const expectedFilters = [
{
label: "Filter by name:",
placeholder: "any name",
value: "name-filter",
},
{
label: "Filter by item:",
placeholder: "any item",
value: "item-filter",
},
{
label: "Filter by value:",
placeholder: "any value",
value: "has-help-filter",
},
{
label: "Filter by list item:",
placeholder: "any list item",
value: "1234",
},
];

expect(Object.keys(validFilters).length).toBe(13);
// find all labels for the filters
const labels = wrapper.findAll("small");
expect(labels.length).toBe(8);
// 8 labels, but 15 valid filters
// more valid filters than labels because not all all `menuItem:true`

/**
* Now add filters in all input fields in the advanced menu
* and check that the correct query is emitted
* */

// First 4 filters are normal, non ranged input fields
expectedFilters.forEach((expectedFilter, i) => {
const label = labels.at(i);
expect(label.text()).toBe(expectedFilter.label);
if (i < 4) {
const filterInput = wrapper.find(`[placeholder='${expectedFilter.placeholder}']`);
expect(filterInput.exists()).toBe(true);
filterInput.setValue(expectedFilter.value);
}
});
// `has_help` filter should have help modal button
expect(wrapper.find("[title='Value Help']").classes().includes("btn")).toBe(true);
// ranged time field (has 2 datepickers)
const createdGtInput = wrapper.find("[placeholder='creation time after']");
const createdLtInput = wrapper.find("[placeholder='creation time before']");
createdGtInput.setValue("January 1, 2022");
createdLtInput.setValue("January 1, 2023");
expect(wrapper.findAll(".b-form-datepicker").length).toBe(2);
// ranged number field (has different placeholder: greater instead of after...)
const indexGtInput = wrapper.find("[placeholder='index greater']");
const indexLtInput = wrapper.find("[placeholder='index lower']");
indexGtInput.setValue("1234");
indexLtInput.setValue("5678");
// default bool filter
const radioBtnGrp = wrapper.find("[data-description='filter bool_def']").findAll(".btn-secondary");
expect(radioBtnGrp.length).toBe(options.length);
for (let i = 0; i < options.length; i++) {
expect(radioBtnGrp.at(i).text()).toBe(options[i].text);
expect(radioBtnGrp.at(i).props().value).toBe(options[i].value);
expect(radioBtnGrp.at(i).props().checked).toBe(null);
}
await radioBtnGrp.at(1).find("input").setChecked(); // click "Yes"
// boolean filter
const boolBtnGrp = wrapper.find("[data-description='filter bool_is']").findAll(".btn-secondary");
expect(boolBtnGrp.length).toBe(2);
expect(boolBtnGrp.at(0).text()).toBe("Yes");
expect(boolBtnGrp.at(0).props().value).toBe(true);
expect(boolBtnGrp.at(1).text()).toBe("No");
expect(boolBtnGrp.at(1).props().value).toBe("any");
await boolBtnGrp.at(1).find("input").setChecked(); // click "No"

// perform search
await performSearch();
await expectCorrectEmits(
false,
"create_time>'January 1, 2022' create_time<'January 1, 2023' " +
"filter_key:item-filter has_help:has-help-filter list_item:1234 " +
"number>1234 number<5678 name:name-filter radio:true bool_def:true",
TestFilters
);
});

it("test buttons that navigate menu and keyup.enter/esc events", async () => {
setUpWrapper("Test Items", "search test items", TestFilters);

expect(wrapper.find("[data-description='advanced filters']").exists()).toBe(false);
await wrapper.setProps({ showAdvanced: true });
expect(wrapper.find("[data-description='advanced filters']").exists()).toBe(true);

// only add name filter in the advanced menu
let filterName = wrapper.find("[placeholder='any name']");
if (filterName.vm && filterName.props().type == "text") {
await filterName.setValue("sample name");
}

// -------- Test keyup.enter key: ---------
// toggles view out and performs a search
await filterName.trigger("keyup.enter");
await expectCorrectEmits(false, "name:'sample name'", TestFilters);

// Test: clearing the filterText
const clearButton = wrapper.find("[data-description='reset query']");
await clearButton.trigger("click");
await expectCorrectEmits(false, "", TestFilters);

// Test: toggling view back in
const toggleButton = wrapper.find("[data-description='toggle advanced search']");
await toggleButton.trigger("click");
await expectCorrectEmits(true, "", TestFilters);

// -------- Test keyup.esc key: ---------
// toggles view out only (doesn't cause a new search / doesn't emulate enter)

// find name field again (destroyed because of toggling out) and set value
filterName = wrapper.find("[placeholder='any name']");
if (filterName.vm && filterName.props().type == "text") {
filterName.setValue("newnamefilter");
}

// press esc key from name field (should not change emitted filterText unlike enter key)
await filterName.trigger("keyup.esc");
await expectCorrectEmits(false, "", TestFilters);
});

/**
* Testing the default values of the filters defined in the HistoryFilters: Filtering
* class, ensuring the default values are reflected in the radio-group buttons
*/
it("test radio-group default filters on HistoryFilters", async () => {
setUpWrapper("History Items", "search datasets", HistoryFilters);
// -------- Testing deleted filter first: ---------

await wrapper.setProps({ showAdvanced: true });
const deletedFilterBtnGrp = wrapper.find("[data-description='filter deleted']");
const deletedFilterAnyBtn = deletedFilterBtnGrp.find(".btn-secondary");
expect(deletedFilterAnyBtn.text()).toBe("Any");

// current active button for deleted filter should be "No"
let deletedFilterActiveBtn = deletedFilterBtnGrp.find(".btn-secondary.active");
expect(deletedFilterActiveBtn.text()).toBe("No");

await deletedFilterAnyBtn.find("input").setChecked();

// now active button for deleted filter should be "Any"
deletedFilterActiveBtn = deletedFilterBtnGrp.find(".btn-secondary.active");
expect(deletedFilterActiveBtn.text()).toBe("Any");

// expect "deleted = any" filter to be applied
await performSearch();
await expectCorrectEmits(false, "visible:true", HistoryFilters);

// -------- Testing visible filter now: ---------

const toggleButton = wrapper.find("[data-description='toggle advanced search']");
await toggleButton.trigger("click");
await expectCorrectEmits(true, "visible:true", HistoryFilters);
const visibleFilterBtnGrp = wrapper.find("[data-description='filter visible']");
const visibleFilterAnyBtn = visibleFilterBtnGrp.find(".btn-secondary");
expect(visibleFilterAnyBtn.text()).toBe("Any");

// current active button for visible filter should be "Yes"
let visibleFilterActiveBtn = visibleFilterBtnGrp.find(".btn-secondary.active");
expect(visibleFilterActiveBtn.text()).toBe("Yes");

await visibleFilterAnyBtn.find("input").setChecked();

// now active button for visible filter should be "Any"
visibleFilterActiveBtn = visibleFilterBtnGrp.find(".btn-secondary.active");
expect(visibleFilterActiveBtn.text()).toBe("Any");

// expect "visible = any" filter to be applied
await performSearch();
await expectCorrectEmits(false, "deleted:any visible:any", HistoryFilters);

// -------- Testing repeated search if it prevents bug: ---------
// (bug reported here: https://github.com/galaxyproject/galaxy/issues/16211)
await toggleButton.trigger("click");
await expectCorrectEmits(true, "deleted:any visible:any", HistoryFilters);
await performSearch();
await expectCorrectEmits(false, "deleted:any visible:any", HistoryFilters);
});
});
Loading
Loading