Merge pull request #15015 from ahmedhamidawan/tool_client_search_bug

Tool Client Search bug fix and add client tests
This commit is contained in:
Aysam Guerler
2022-11-24 12:52:32 -05:00
committed by GitHub
6 changed files with 218 additions and 85 deletions
@@ -1,65 +0,0 @@
import MockAdapter from "axios-mock-adapter";
import axios from "axios";
import { filterToolSections, filterTools } from "../utilities";
describe("ToolBox", () => {
const toolsMock = [
{
model_class: "ToolSection",
id: "group_1",
name: "Group 1",
elems: [
{
model_class: "Tool1",
id: "tool1",
name: "Tool 1",
link: "/tool_runner?tool_id=tool1",
panel_section_id: "group_1",
panel_section_name: "Group 1",
},
{
model_class: "Tool2",
id: "tool2",
name: "Tool 2",
link: "/tool_runner?tool_id=tool2",
panel_section_id: "group_1",
panel_section_name: "Group 1",
},
],
},
{
model_class: "ToolSection",
id: "group_2",
name: "Group 2",
elems: [
{
model_class: "Tool3",
id: "tool3",
name: "Tool 3",
link: "/tool_runner?tool_id=tool3",
panel_section_id: "group_2",
panel_section_name: "Group 2",
},
],
},
];
const resultsMock = ["tool1", "tool3"];
let axiosMock;
beforeEach(async () => {
axiosMock = new MockAdapter(axios);
});
it("test filter functions correctly matching: (1) Tools store array-of-objects with (2) Results array", async () => {
axiosMock
.onGet(`/api/tools`)
.replyOnce(200, toolsMock)
.onGet(/api\/tools?.*/)
.replyOnce(200, resultsMock);
const toolsResults = filterTools(toolsMock, resultsMock);
const toolsResultsSection = filterToolSections(toolsMock, resultsMock);
expect(toolsResults.length).toBe(2);
expect(toolsResultsSection.length).toBe(2);
});
});
@@ -21,7 +21,7 @@
size="sm"
:placeholder="`any ${sectionLabel}`"
list="sectionSelect" />
<b-form-datalist id="sectionSelect" :options="sections"></b-form-datalist>
<b-form-datalist id="sectionSelect" :options="sectionNames"></b-form-datalist>
<small class="mt-1">Filter by id:</small>
<b-form-input v-model="filterSettings['id']" size="sm" placeholder="any id" />
<small class="mt-1">Filter by help text:</small>
@@ -43,7 +43,7 @@
<script>
import { getGalaxyInstance } from "app";
import DelayedInput from "components/Common/DelayedInput";
import { searchToolsByKeys } from "../utilities.js";
import { normalizeTools, searchToolsByKeys } from "../utilities.js";
export default {
name: "ToolSearch",
@@ -88,7 +88,7 @@ export default {
const Galaxy = getGalaxyInstance();
return Galaxy.user.getFavorites().tools;
},
sections() {
sectionNames() {
return this.toolbox.map((section) =>
section.name !== undefined && section.name !== "Uncategorized" ? section.name : ""
);
@@ -96,6 +96,9 @@ export default {
sectionLabel() {
return this.currentPanelView === "default" ? "section" : "ontology";
},
toolsList() {
return normalizeTools(this.toolbox);
},
},
methods: {
checkQuery(q) {
@@ -105,8 +108,9 @@ export default {
if (this.favorites.includes(q)) {
this.$emit("onResults", this.favoritesResults);
} else {
const keys = ["name", "description"];
this.$emit("onResults", searchToolsByKeys(this.toolbox, keys, q));
// keys with sorting order
const keys = { exact: 2, name: 1, description: 0 };
this.$emit("onResults", searchToolsByKeys(this.toolsList, keys, q));
}
} else {
this.$emit("onResults", null);
@@ -0,0 +1,86 @@
import Vuex from "vuex";
import { mount } from "@vue/test-utils";
import { getLocalVue } from "jest/helpers";
import MockAdapter from "axios-mock-adapter";
import axios from "axios";
import { filterToolSections, filterTools } from "./utilities";
import ToolBox from "./ToolBox";
import toolsList from "./testToolsList";
import { userStore } from "store/userStore";
import { useConfig } from "composables/config";
jest.mock("composables/config");
useConfig.mockReturnValue({
config: {
toolbox_auto_sort: true,
},
isLoaded: true,
});
const localVue = getLocalVue();
describe("ToolBox", () => {
const toolsMock = toolsList;
const resultsMock = ["join1", "join_collections", "find1"];
let axiosMock;
const searches = {
join: ["join1", "join_collections", "find1"],
find: ["find1"],
remove: ["remove1", "remove_duplicate"],
fi: null,
};
let wrapper;
let store;
let state;
beforeEach(async () => {
axiosMock = new MockAdapter(axios);
store = new Vuex.Store({
modules: {
user: {
state,
actions: {
loadUser: jest.fn(),
},
getters: userStore.getters,
namespaced: true,
},
},
});
wrapper = mount(ToolBox, {
propsData: {
toolbox: toolsList,
currentPanelView: "default",
storedWorkflowMenuEntries: [],
},
store,
localVue,
stubs: {
icon: { template: "<div></div>" },
},
});
});
it("test filter functions correctly matching: (1) Tools store array-of-objects with (2) Results array", async () => {
axiosMock
.onGet(`/api/tools`)
.replyOnce(200, toolsMock)
.onGet(/api\/tools?.*/)
.replyOnce(200, resultsMock);
const toolsResults = filterTools(toolsMock, resultsMock);
const toolsResultsSection = filterToolSections(toolsMock, resultsMock);
expect(toolsResults.length).toBe(3);
expect(toolsResultsSection.length).toBe(2);
});
it("test toolbox client search", async () => {
for (const [query, results] of Object.entries(searches)) {
await wrapper.setData({ query: query });
expect(wrapper.vm.results).toEqual(results);
}
});
});
@@ -0,0 +1,72 @@
[
{
"model_class": "ToolSection",
"id": "group_1",
"name": "Group 1",
"elems": [
{
"model_class": "Tool",
"name": "Join",
"id": "join1",
"description": "two files",
"panel_section_id": "group_1",
"panel_section_name": "Group 1"
},
{
"model_class": "Tool",
"name": "Remove",
"id": "remove1",
"description": "lines",
"panel_section_id": "group_1",
"panel_section_name": "Group 1"
},
{
"model_class": "Tool",
"name": "Cut",
"id": "cut1",
"description": "lines",
"panel_section_id": "group_1",
"panel_section_name": "Group 1"
},
{
"model_class": "Tool",
"name": "Find",
"id": "find1",
"description": "joined dataset",
"panel_section_id": "group_1",
"panel_section_name": "Group 1"
}
]
},
{
"model_class": "ToolSection",
"id": "group_2",
"name": "Group 2",
"elems": [
{
"model_class": "Tool",
"name": "Join Collections",
"id": "join_collections",
"description": "",
"panel_section_id": "group_2",
"panel_section_name": "Group 2"
},
{
"model_class": "Tool",
"name": "Create Collections",
"id": "create_collections",
"description": "from datasets",
"panel_section_id": "group_2",
"panel_section_name": "Group 2"
}
]
},
{
"model_class": "Tool",
"name": "Remove duplicate",
"id": "remove_duplicate",
"description": "lines from dataset",
"panel_section_id": "remove_duplicate",
"panel_section_name": "Remove duplicate"
}
]
+27 -14
View File
@@ -1,11 +1,21 @@
/**
* Utilities file for Tool Search (panel/client search + advanced/backend search)
*/
import { orderBy } from "lodash";
const TOOLS_RESULTS_SORT_LABEL = "apiSort";
const TOOLS_RESULTS_SECTIONS_HIDE = ["Expression Tools"];
// - Takes filterSettings = {"name": "Tool Name", "section": "Collection", ...}
// - Takes panelView (if not 'default', does ontology search at backend)
// - Takes toolbox (to find ontology id if given ontology name)
// - Returns parsed Whoosh query
// e.g. fn call: createWhooshQuery(filterSettings, 'ontology:edam_topics', toolbox)
// can return:
// query = "(name:(skew) name_exact:(skew) description:(skew)) AND (edam_topics:(topic_0797) AND )"
export function createWhooshQuery(filterSettings, panelView, toolbox) {
let query = "(";
// add description+name_exact fields = name, to do a combined AndGroup at backend
// add description+name_exact fields = name, to do a combined OrGroup at backend
const name = filterSettings["name"];
if (name) {
query += "name:(" + name + ") ";
@@ -38,6 +48,7 @@ export function createWhooshQuery(filterSettings, panelView, toolbox) {
return query;
}
// Given toolbox and search results, returns filtered tool results
export function filterTools(tools, results) {
let toolsResults = [];
tools = normalizeTools(tools);
@@ -47,6 +58,7 @@ export function filterTools(tools, results) {
return toolsResults;
}
// Given toolbox and search results, returns filtered tool results by sections
export function filterToolSections(tools, results) {
let toolsResults = [];
let toolsResultsSection = [];
@@ -71,26 +83,27 @@ export function hasResults(results) {
return Array.isArray(results) && results.length > 0;
}
// Given toolbox, keys to sort/search results by and a search query,
// Returns tool ids sorted by order of keys that are being searched
export function searchToolsByKeys(tools, keys, query) {
const returnedTools = [];
for (const section of tools) {
if (section.elems) {
for (const tool of section.elems) {
for (const key of keys) {
const actualValue = tool[key];
if (actualValue && actualValue.toLowerCase().match(query.toLowerCase())) {
returnedTools.push({ id: tool.id, key: key });
break;
}
}
for (const tool of tools) {
for (const key of Object.keys(keys)) {
const actualValue = tool[key] ? tool[key].toLowerCase() : "";
const queryLowerCase = query.toLowerCase();
if (actualValue.match(queryLowerCase)) {
// do we care for exact matches && is it an exact match ?
const order = keys.exact && actualValue === queryLowerCase ? keys.exact : keys[key];
returnedTools.push({ id: tool.id, order });
break;
}
}
}
// sorting results by indexed order of key in keys
return orderBy(returnedTools, ["key"], ["desc"]).map((tool) => tool.id);
// sorting results by indexed order of keys
return orderBy(returnedTools, ["order"], ["desc"]).map((tool) => tool.id);
}
function normalizeTools(tools) {
export function normalizeTools(tools) {
tools = hideToolsSection(tools);
tools = flattenTools(tools);
return tools;
+24 -1
View File
@@ -1,5 +1,5 @@
import toolsList from "components/ToolsView/testData/toolsList";
import { createWhooshQuery, filterTools, filterToolSections } from "./utilities";
import { createWhooshQuery, filterTools, filterToolSections, normalizeTools, searchToolsByKeys } from "./utilities";
describe("test helpers in tool searching utilities", () => {
it("test parsing helper that converts settings to whoosh query", async () => {
@@ -16,6 +16,29 @@ describe("test helpers in tool searching utilities", () => {
expect(q).toContain("id_exact:(__FILTER_FAILED_DATASETS__) AND help:(downstream)");
});
it("test tool search helper that searches for tools given keys", async () => {
const q = "collection";
let expectedResults = [
"__FILTER_FAILED_DATASETS__",
"__FILTER_EMPTY_DATASETS__",
"__UNZIP_COLLECTION__",
"__ZIP_COLLECTION__",
];
let keys = { description: 1, name: 0 };
let results = searchToolsByKeys(normalizeTools(toolsList), keys, q);
expect(results).toEqual(expectedResults);
expectedResults = [
"__UNZIP_COLLECTION__",
"__ZIP_COLLECTION__",
"__FILTER_FAILED_DATASETS__",
"__FILTER_EMPTY_DATASETS__",
];
keys = { description: 0, name: 1 };
results = searchToolsByKeys(normalizeTools(toolsList), keys, q);
expect(results).toEqual(expectedResults);
});
it("test tool filtering helpers on toolsList given list of ids", async () => {
const ids = ["__FILTER_FAILED_DATASETS__", "liftOver1"];
// check length of first section from imported const toolsList