mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #15015 from ahmedhamidawan/tool_client_search_bug
Tool Client Search bug fix and add client tests
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user