From 3509eacdec27f8659ffa1457422004ce78eb2ff8 Mon Sep 17 00:00:00 2001 From: Scott Bell Date: Wed, 15 Feb 2023 21:50:03 +0100 Subject: [PATCH] Debounce search results (#6259) * throttle search results to one a second * changed to use custom debounce due to async * attempt to fix flakey test * attempt to fix flakey test * revert test changes * reduce debounce time and add e2e test * allow canceling of timeout * removed debug * make search result e2e tests more stable * make drop down selector a constant * address pr comments --- e2e/tests/functional/search.e2e.spec.js | 100 ++++++++++-------- src/ui/layout/search/GrandSearch.vue | 20 ++++ .../layout/search/SearchResultsDropDown.vue | 5 +- 3 files changed, 82 insertions(+), 43 deletions(-) diff --git a/e2e/tests/functional/search.e2e.spec.js b/e2e/tests/functional/search.e2e.spec.js index 906290e1a8..1914810375 100644 --- a/e2e/tests/functional/search.e2e.spec.js +++ b/e2e/tests/functional/search.e2e.spec.js @@ -28,6 +28,14 @@ const { createDomainObjectWithDefaults } = require('../../appActions'); const { v4: uuid } = require('uuid'); test.describe('Grand Search', () => { + const searchResultSelector = '.c-gsearch-result__title'; + const searchResultDropDownSelector = '.c-gsearch__results'; + + test.beforeEach(async ({ page }) => { + // Go to baseURL + await page.goto("./", { waitUntil: "networkidle" }); + }); + test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => { const { myItemsFolderName } = openmctConfig; @@ -89,15 +97,8 @@ test.describe('Grand Search', () => { await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`); await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`); }); -}); - -test.describe("Search Tests @unstable", () => { - const searchResultSelector = '.c-gsearch-result__title'; test('Validate empty search result', async ({ page }) => { - // Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); - // Invalid search for objects await page.type("input[type=search]", 'not found'); @@ -105,7 +106,7 @@ test.describe("Search Tests @unstable", () => { await waitForSearchCompletion(page); // Get the search results - const searchResults = await page.locator(searchResultSelector); + const searchResults = page.locator(searchResultSelector); // Verify that no results are found expect(await searchResults.count()).toBe(0); @@ -115,9 +116,6 @@ test.describe("Search Tests @unstable", () => { }); test('Validate single object in search result @couchdb', async ({ page }) => { - //Go to baseURL - await page.goto("./", { waitUntil: "networkidle" }); - // Create a folder object const folderName = uuid(); await createDomainObjectWithDefaults(page, { @@ -139,21 +137,56 @@ test.describe("Search Tests @unstable", () => { await expect(searchResults).toHaveText(folderName); }); + test('Search results are debounced @couchdb', async ({ page }) => { + test.info().annotations.push({ + type: 'issue', + description: 'https://github.com/nasa/openmct/issues/6179' + }); + await createObjectsForSearch(page); + + let networkRequests = []; + page.on('request', (request) => { + const searchRequest = request.url().endsWith('_find'); + const fetchRequest = request.resourceType() === 'fetch'; + if (searchRequest && fetchRequest) { + networkRequests.push(request); + } + }); + + // Full search for object + await page.type("input[type=search]", 'Clock', { delay: 100 }); + + // Wait for search to finish + await waitForSearchCompletion(page); + + // Network requests for the composite telemetry with multiple items should be: + // 1. batched request for latest telemetry using the bulk API + expect(networkRequests.length).toBe(1); + + const searchResultDropDown = await page.locator(searchResultDropDownSelector); + + await expect(searchResultDropDown).toContainText('Clock A'); + }); + test("Validate multiple objects in search results return partial matches", async ({ page }) => { test.info().annotations.push({ type: 'issue', description: 'https://github.com/nasa/openmct/issues/4667' }); - // Go to baseURL - await page.goto("/", { waitUntil: "networkidle" }); - // Create folder objects - const folderName = "e928a26e-e924-4ea0"; + const folderName1 = "e928a26e-e924-4ea0"; const folderName2 = "e928a26e-e924-4001"; - await createFolderObject(page, folderName); - await createFolderObject(page, folderName2); + await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: folderName1 + }); + + await createDomainObjectWithDefaults(page, { + type: 'Folder', + name: folderName2 + }); // Partial search for objects await page.type("input[type=search]", 'e928a26e'); @@ -161,36 +194,22 @@ test.describe("Search Tests @unstable", () => { // Wait for search to finish await waitForSearchCompletion(page); - // Get the search results - const searchResults = await page.locator(searchResultSelector); + const searchResultDropDown = await page.locator(searchResultDropDownSelector); // Verify that the search result/s correctly match the search query + await expect(searchResultDropDown).toContainText(folderName1); + await expect(searchResultDropDown).toContainText(folderName2); + + // Get the search results + const searchResults = page.locator(searchResultSelector); + // Verify that two results are found expect(await searchResults.count()).toBe(2); - await expect(await searchResults.first()).toHaveText(folderName); - await expect(await searchResults.last()).toHaveText(folderName2); }); }); -async function createFolderObject(page, folderName) { - // Open Create menu - await page.locator('button:has-text("Create")').click(); - - // Select Folder object - await page.locator('text=Folder').nth(1).click(); - - // Click folder title to enter edit mode - await page.locator('text=Properties Title Notes >> input[type="text"]').click(); - - // Enter folder name - await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folderName); - - // Create folder object - await page.locator('button:has-text("OK")').click(); -} - async function waitForSearchCompletion(page) { // Wait loading spinner to disappear - await page.waitForSelector('.c-tree-and-search__loading', { state: 'detached' }); + await page.waitForSelector('.search-finished'); } /** @@ -198,9 +217,6 @@ async function waitForSearchCompletion(page) { * @param {import('@playwright/test').Page} page */ async function createObjectsForSearch(page) { - //Go to baseURL - await page.goto('./', { waitUntil: 'networkidle' }); - const redFolder = await createDomainObjectWithDefaults(page, { type: 'Folder', name: 'Red Folder' diff --git a/src/ui/layout/search/GrandSearch.vue b/src/ui/layout/search/GrandSearch.vue index f9e34f6d32..efaa914617 100644 --- a/src/ui/layout/search/GrandSearch.vue +++ b/src/ui/layout/search/GrandSearch.vue @@ -47,6 +47,8 @@ import search from '../../components/search.vue'; import SearchResultsDropDown from './SearchResultsDropDown.vue'; +const SEARCH_DEBOUNCE_TIME = 200; + export default { name: 'GrandSearch', components: { @@ -59,11 +61,15 @@ export default { data() { return { searchValue: '', + debouncedSearchTimeoutID: null, searchLoading: false, annotationSearchResults: [], objectSearchResults: [] }; }, + mounted() { + this.getSearchResults = this.debounceAsyncFunction(this.getSearchResults, SEARCH_DEBOUNCE_TIME); + }, destroyed() { document.body.removeEventListener('click', this.handleOutsideClick); }, @@ -84,6 +90,7 @@ export default { if (this.searchValue) { await this.getSearchResults(); } else { + clearTimeout(this.debouncedSearchTimeoutID); const dropdownOptions = { searchLoading: this.searchLoading, searchValue: this.searchValue, @@ -93,6 +100,19 @@ export default { this.$refs.searchResultsDropDown.showResults(dropdownOptions); } }, + debounceAsyncFunction(functionToDebounce, debounceTime) { + return (...args) => { + clearTimeout(this.debouncedSearchTimeoutID); + + return new Promise((resolve, reject) => { + this.debouncedSearchTimeoutID = setTimeout(() => { + functionToDebounce(...args) + .then(resolve) + .catch(reject); + }, debounceTime); + }); + }; + }, getPathsForObjects(objectsNeedingPaths) { return Promise.all(objectsNeedingPaths.map(async (domainObject) => { if (!domainObject) { diff --git a/src/ui/layout/search/SearchResultsDropDown.vue b/src/ui/layout/search/SearchResultsDropDown.vue index 8815764090..0b42cd8310 100644 --- a/src/ui/layout/search/SearchResultsDropDown.vue +++ b/src/ui/layout/search/SearchResultsDropDown.vue @@ -28,7 +28,10 @@ v-show="resultsShown" class="c-gsearch__results-wrapper" > -
+