diff --git a/__tests__/e2e/local-search/local-search.test.ts b/__tests__/e2e/local-search/local-search.test.ts index 3788ff54..8248b46a 100644 --- a/__tests__/e2e/local-search/local-search.test.ts +++ b/__tests__/e2e/local-search/local-search.test.ts @@ -16,7 +16,7 @@ describe('local search', () => { }) try { - await page.locator('.VPNavBarSearchButton').click() + await openSearch() const loading = page.locator('.search-loading') const results = page.locator('.results') @@ -49,22 +49,10 @@ describe('local search', () => { ) test('exclude content from search results', async () => { - await page.locator('.VPNavBarSearchButton').click() - - const input = await page.waitForSelector('input#localsearch-input') - await input.type('local') + await searchFor('local') + await waitForSearchResults({ text: 'Local search included', count: 1 }) const searchResults = page.locator('#localsearch-list') - await page.waitForFunction(() => { - const options = [ - ...document.querySelectorAll('#localsearch-list li[role=option]') - ] - - return ( - options.length === 1 && - options[0].textContent?.includes('Local search included') - ) - }) expect(await searchResults.locator('li[role=option]').count()).toBe(1) @@ -84,21 +72,10 @@ describe('local search', () => { }) test('resolves $frontmatter expressions in search results', async () => { - await page.locator('.VPNavBarSearchButton').click() - - const input = await page.waitForSelector('input#localsearch-input') - await input.fill('Frontmatter Title Resolved') + await searchFor('Frontmatter Title Resolved') + await waitForSearchResults({ text: 'Frontmatter Title Resolved' }) const searchResults = page.locator('#localsearch-list') - await page.waitForFunction(() => { - const options = [ - ...document.querySelectorAll('#localsearch-list li[role=option]') - ] - - return options.some((option) => - option.textContent?.includes('Frontmatter Title Resolved') - ) - }) expect( await searchResults @@ -111,25 +88,11 @@ describe('local search', () => { }) test('custom tokenize function reaches the client', async () => { - await page.locator('.VPNavBarSearchButton').click() - - const input = await page.waitForSelector('input#localsearch-input') - // '#hash-probe' survives as one token only under the custom tokenizer — // MiniSearch's default one would degrade the query to 'hash'/'probe' // and miss the index built with the custom tokenizer - await input.type('#hash-probe') - - await page.waitForFunction(() => { - const options = [ - ...document.querySelectorAll('#localsearch-list li[role=option]') - ] - - return ( - options.length === 1 && - options[0].textContent?.includes('Local search included') - ) - }) + const input = await searchFor('#hash-probe') + await waitForSearchResults({ text: 'Local search included', count: 1 }) // a fragment of a kept-whole token must not match anything await input.fill('linked-words') @@ -145,8 +108,7 @@ describe('local search', () => { ]) { await page.setViewportSize({ width, height: 600 }) await goto('/') - await page.locator('.VPNavBarSearchButton').click() - await page.waitForSelector('input#localsearch-input') + await openSearch() expect(await page.locator('.VPNavBarHamburger').isVisible()).toBe( !isDesktop @@ -162,17 +124,9 @@ describe('local search', () => { test('navigate results with macOS Ctrl shortcuts', async () => { await page.evaluate(() => document.documentElement.classList.add('mac')) - await page.locator('.VPNavBarSearchButton').click() - const input = await page.waitForSelector('input#localsearch-input') - await input.type('lorem') - - await page.waitForFunction(() => { - return ( - document.querySelectorAll('#localsearch-list li[role=option]').length > - 1 - ) - }) + const input = await searchFor('lorem') + await waitForSearchResults({ minCount: 2 }) expect(await input.getAttribute('aria-activedescendant')).toBe( 'localsearch-item-0' @@ -190,6 +144,43 @@ describe('local search', () => { }) }) +async function openSearch() { + await page.locator('.VPNavBarSearchButton').click() + return page.waitForSelector('input#localsearch-input') +} + +// fills the query in one step, so exactly one search runs and the result +// list settles into the state for this query and nothing else +async function searchFor(query: string) { + const input = await openSearch() + await input.fill(query) + return input +} + +// waits until the result list matches, so assertions never run against the +// results of an earlier query +function waitForSearchResults(condition: { + /** some result must contain this text */ + text?: string + /** exactly this many results */ + count?: number + /** at least this many results */ + minCount?: number +}) { + return page.waitForFunction(({ text, count, minCount }) => { + const options = [ + ...document.querySelectorAll('#localsearch-list li[role=option]') + ] + + return ( + (count === undefined || options.length === count) && + (minCount === undefined || options.length >= minCount) && + (text === undefined || + options.some((option) => option.textContent?.includes(text))) + ) + }, condition) +} + function pressMacCtrl(key: string) { return page.evaluate((key) => { window.dispatchEvent(