diff --git a/__tests__/e2e/local-search/local-search.test.ts b/__tests__/e2e/local-search/local-search.test.ts index 71915d4a..feff9126 100644 --- a/__tests__/e2e/local-search/local-search.test.ts +++ b/__tests__/e2e/local-search/local-search.test.ts @@ -3,6 +3,51 @@ describe('local search', () => { await goto('/') }) + test.runIf(!process.env.VITE_TEST_BUILD)( + 'shows progress while loading search index', + async () => { + const indexRoute = /@localSearchIndexroot/ + let delayedIndex = false + + await page.route(indexRoute, async (route) => { + delayedIndex = true + await new Promise((resolve) => setTimeout(resolve, 800)) + await route.continue() + }) + + try { + await page.locator('.VPNavBarSearchButton').click() + + const loading = page.locator('.search-loading') + const results = page.locator('.results') + + await page.waitForFunction(() => + document + .querySelector('.search-loading') + ?.classList.contains('active') + ) + + expect(delayedIndex).toBe(true) + expect(await loading.getAttribute('role')).toBe('status') + expect(await loading.getAttribute('aria-label')).toBe( + 'Loading search results' + ) + expect(await results.getAttribute('aria-busy')).toBe('true') + + await page.waitForFunction( + () => + !document + .querySelector('.search-loading') + ?.classList.contains('active') + ) + + expect(await results.getAttribute('aria-busy')).toBe('false') + } finally { + await page.unroute(indexRoute) + } + } + ) + test('exclude content from search results', async () => { await page.locator('.VPNavBarSearchButton').click() diff --git a/src/client/theme-default/components/VPLocalSearchBox.vue b/src/client/theme-default/components/VPLocalSearchBox.vue index 81e7a324..709ca137 100644 --- a/src/client/theme-default/components/VPLocalSearchBox.vue +++ b/src/client/theme-default/components/VPLocalSearchBox.vue @@ -67,25 +67,34 @@ const { activate } = useFocusTrap(el, { escapeDeactivates: true }) const { localeIndex, theme } = vitePressData -const searchIndex = computedAsync(async () => - markRaw( - MiniSearch.loadJSON( - (await searchIndexData.value[localeIndex.value]?.())?.default, - { - fields: ['title', 'titles', 'text'], - storeFields: ['title', 'titles'], - searchOptions: { - fuzzy: 0.2, - prefix: true, - boost: { title: 4, text: 2, titles: 1 }, +const isSearchIndexLoading = ref(false) +const isSearching = ref(false) +const showSearchSpinner = computed(() => { + return isSearchIndexLoading.value || isSearching.value +}) + +const searchIndex = computedAsync( + async () => + markRaw( + MiniSearch.loadJSON( + (await searchIndexData.value[localeIndex.value]?.())?.default, + { + fields: ['title', 'titles', 'text'], + storeFields: ['title', 'titles'], + searchOptions: { + fuzzy: 0.2, + prefix: true, + boost: { title: 4, text: 2, titles: 1 }, + ...(theme.value.search?.provider === 'local' && + theme.value.search.options?.miniSearch?.searchOptions) + }, ...(theme.value.search?.provider === 'local' && - theme.value.search.options?.miniSearch?.searchOptions) - }, - ...(theme.value.search?.provider === 'local' && - theme.value.search.options?.miniSearch?.options) - } - ) - ) + theme.value.search.options?.miniSearch?.options) + } + ) + ), + undefined, + isSearchIndexLoading ) const disableQueryPersistence = computed(() => { @@ -145,9 +154,15 @@ watchDebounced( let canceled = false onCleanup(() => { canceled = true + isSearching.value = false }) - if (!index) return + if (!index) { + results.value = [] + return + } + + isSearching.value = true // Search results.value = index @@ -232,6 +247,7 @@ watchDebounced( } // FIXME: without this whole page scrolls to the bottom resultsEl.value?.firstElementChild?.scrollIntoView({ block: 'start' }) + isSearching.value = false }, { debounce: 200, immediate: true } ) @@ -487,6 +503,14 @@ function onMouseMove(e: MouseEvent) { type="search" />
+ +