[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7D9HDFQHMTCPMACXKZPWMJD":3,"skill-same:01M7D9HDFQHMTCPMACXKZPWMJD":80},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":58,"commitSha":59,"skillDirectory":60,"forksCount":14,"repositoryHtmlUrl":61,"repositoryHomepageUrl":6,"ownerHtmlUrl":62,"ownerBlogUrl":63,"ownerDescription":64,"updatedAt":65,"install":66,"resources":79},"01M7D9HDFQHMTCPMACXKZPWMJD","Search Quality Tester",null,"Evaluate search functionality quality including relevance ranking, typo tolerance, faceted filtering, autocomplete accuracy, and zero-results handling","GrupoANDevelopment-m\u002Falberto-ai","GrupoANDevelopment-m","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F257455179?v=4","01M7D69X2N2KEVN0SJF6WGM6SH","Antônio  Ndombe","01M7D69X3P5JTT8H8S904N3888",0,"MIT","https:\u002F\u002Fgithub.com\u002FGrupoANDevelopment-m\u002Falberto-ai\u002Fblob\u002Fmain\u002Fskills\u002Fqa\u002Fsearch-quality-tester\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: Search Quality Tester\ndescription: Evaluate search functionality quality including relevance ranking, typo tolerance, faceted filtering, autocomplete accuracy, and zero-results handling\nversion: 1.0.0\nauthor: Pramod\nlicense: MIT\ntags: [search-testing, search-relevance, autocomplete, faceted-search, fuzzy-search, search-ranking, zero-results]\ntestingTypes: [e2e, integration]\nframeworks: [playwright]\nlanguages: [typescript, javascript]\ndomains: [web, api]\nagents: [claude-code, cursor, github-copilot, windsurf, codex, aider, continue, cline, zed, bolt, gemini-cli, amp]\n---\n\n# Search Quality Tester Skill\n\nYou are an expert QA engineer specializing in search functionality testing. When the user asks you to write, review, or debug search quality tests, follow these detailed instructions to evaluate relevance ranking, typo tolerance, faceted filtering, autocomplete accuracy, zero-results handling, and search performance.\n\n## Core Principles\n\n1. **Relevance is king** -- Search results must be ordered by relevance to the query, not by insertion order or random criteria. Every search test suite must include explicit relevance ranking assertions.\n2. **Typo tolerance is expected** -- Modern users expect search to handle misspellings, transpositions, and phonetic similarities. Test fuzzy matching systematically with controlled edit distances.\n3. **Faceted filtering must compose** -- Facet filters (category, price range, date, tags) must work individually and in combination without producing contradictory or empty results when data exists.\n4. **Autocomplete guides discovery** -- Autocomplete suggestions shape user behavior. They must appear quickly, reflect actual content, and gracefully handle partial input and special characters.\n5. **Zero results is a UX moment** -- A blank screen with no guidance is a failure. Zero-results pages must offer helpful alternatives, spelling corrections, or navigation paths.\n6. **Performance under load matters** -- Search latency directly impacts user satisfaction. Measure response times under realistic concurrent loads and set strict budgets.\n7. **Special characters must not break search** -- Queries containing quotes, ampersands, angle brackets, Unicode, or SQL-like syntax must never cause errors or security vulnerabilities.\n8. **Search analytics drive improvement** -- Every search interaction should emit trackable events. Validate that analytics capture query terms, result counts, click-through positions, and refinements.\n\n## Project Structure\n\n```\ntests\u002F\n  search\u002F\n    relevance\u002F\n      keyword-ranking.spec.ts\n      phrase-matching.spec.ts\n      boost-rules.spec.ts\n    typo-tolerance\u002F\n      edit-distance.spec.ts\n      phonetic-matching.spec.ts\n      transposition.spec.ts\n    autocomplete\u002F\n      suggestion-accuracy.spec.ts\n      suggestion-speed.spec.ts\n      partial-input.spec.ts\n    facets\u002F\n      single-facet.spec.ts\n      combined-facets.spec.ts\n      facet-counts.spec.ts\n    zero-results\u002F\n      empty-state.spec.ts\n      suggestions.spec.ts\n      spelling-correction.spec.ts\n    pagination\u002F\n      result-pages.spec.ts\n      deep-pagination.spec.ts\n      sort-order-persistence.spec.ts\n    performance\u002F\n      latency.spec.ts\n      concurrent-search.spec.ts\n    security\u002F\n      special-characters.spec.ts\n      injection-prevention.spec.ts\n    analytics\u002F\n      event-tracking.spec.ts\n      click-tracking.spec.ts\n    fixtures\u002F\n      search-test-data.ts\n      expected-rankings.ts\n    utils\u002F\n      search-helpers.ts\n      relevance-scorer.ts\nplaywright.config.ts\n```\n\n## Configuration\n\n```typescript\n\u002F\u002F playwright.config.ts\nimport { defineConfig, devices } from '@playwright\u002Ftest';\n\nexport default defineConfig({\n  testDir: '.\u002Ftests\u002Fsearch',\n  fullyParallel: true,\n  retries: 1,\n  workers: process.env.CI ? 2 : undefined,\n  reporter: [\n    ['html', { open: 'never' }],\n    ['json', { outputFile: 'search-test-results.json' }],\n  ],\n  use: {\n    baseURL: process.env.BASE_URL || 'http:\u002F\u002Flocalhost:3000',\n    trace: 'retain-on-failure',\n    screenshot: 'only-on-failure',\n  },\n  projects: [\n    {\n      name: 'search-chromium',\n      use: { ...devices['Desktop Chrome'] },\n    },\n    {\n      name: 'search-mobile',\n      use: { ...devices['Pixel 5'] },\n    },\n  ],\n});\n```\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Ffixtures\u002Fsearch-test-data.ts\nexport interface SearchTestCase {\n  query: string;\n  expectedTopResults: string[];\n  expectedMinCount: number;\n  category?: string;\n  description: string;\n}\n\nexport const relevanceTestCases: SearchTestCase[] = [\n  {\n    query: 'wireless bluetooth headphones',\n    expectedTopResults: ['Sony WH-1000XM5', 'Bose QuietComfort Ultra'],\n    expectedMinCount: 10,\n    description: 'Exact product category match should rank top brands first',\n  },\n  {\n    query: 'headphones',\n    expectedTopResults: ['Sony WH-1000XM5', 'AirPods Max', 'Bose QuietComfort Ultra'],\n    expectedMinCount: 20,\n    description: 'Broad category term should return all headphone products',\n  },\n  {\n    query: '\"noise cancelling\"',\n    expectedTopResults: ['Sony WH-1000XM5', 'Bose QuietComfort Ultra'],\n    expectedMinCount: 5,\n    description: 'Quoted phrase should match exact phrase in descriptions',\n  },\n];\n\nexport const typoTestCases = [\n  { typo: 'headphoens', corrected: 'headphones', editDistance: 1 },\n  { typo: 'bluetooh', corrected: 'bluetooth', editDistance: 1 },\n  { typo: 'wireles', corrected: 'wireless', editDistance: 1 },\n  { typo: 'heeadphones', corrected: 'headphones', editDistance: 2 },\n  { typo: 'blutooth', corrected: 'bluetooth', editDistance: 2 },\n];\n\nexport const specialCharacterQueries = [\n  'C++ programming',\n  'rock & roll',\n  '\u003Cscript>alert(\"xss\")\u003C\u002Fscript>',\n  \"O'Reilly books\",\n  'price: $50-$100',\n  'SELECT * FROM products',\n  'search term\\x00null byte',\n  'emoji search 🎧',\n  '   leading trailing spaces   ',\n  '',\n];\n```\n\n## How-To Guides\n\n### Testing Search Relevance Ranking\n\nRelevance ranking is the most critical aspect of search quality. The following pattern establishes a framework for asserting that the most relevant results appear at the top of the result list.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Frelevance\u002Fkeyword-ranking.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\nimport { relevanceTestCases } from '..\u002Ffixtures\u002Fsearch-test-data';\n\ntest.describe('Search Relevance Ranking', () => {\n  for (const testCase of relevanceTestCases) {\n    test(`relevance: ${testCase.description}`, async ({ page }) => {\n      await page.goto('\u002Fsearch');\n      const searchInput = page.getByRole('searchbox');\n      await searchInput.fill(testCase.query);\n      await searchInput.press('Enter');\n\n      \u002F\u002F Wait for results to load\n      await page.waitForSelector('[data-testid=\"search-results\"]');\n\n      \u002F\u002F Verify minimum result count\n      const resultCount = await page\n        .getByTestId('result-count')\n        .textContent();\n      const count = parseInt(resultCount || '0', 10);\n      expect(count).toBeGreaterThanOrEqual(testCase.expectedMinCount);\n\n      \u002F\u002F Verify top results contain expected items\n      const topResults = await page\n        .getByTestId('search-result-title')\n        .allTextContents();\n      const topN = topResults.slice(0, testCase.expectedTopResults.length);\n\n      for (const expectedResult of testCase.expectedTopResults) {\n        expect(topN).toContain(expectedResult);\n      }\n    });\n  }\n\n  test('exact match ranks above partial match', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('Playwright Testing Guide');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const firstResult = await page\n      .getByTestId('search-result-title')\n      .first()\n      .textContent();\n\n    \u002F\u002F Exact title match must be the first result\n    expect(firstResult).toBe('Playwright Testing Guide');\n  });\n\n  test('boosted fields rank higher than body text', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('performance optimization');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Items with \"performance optimization\" in the title should\n    \u002F\u002F rank above items that only mention it in the description\n    const results = await page.getByTestId('search-result').all();\n    expect(results.length).toBeGreaterThan(2);\n\n    const firstTitle = await results[0]\n      .getByTestId('search-result-title')\n      .textContent();\n    expect(firstTitle?.toLowerCase()).toContain('performance');\n  });\n});\n```\n\n### Testing Typo Tolerance and Fuzzy Matching\n\nUsers frequently misspell queries. A robust search system corrects these errors transparently or presents a \"Did you mean?\" prompt.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Ftypo-tolerance\u002Fedit-distance.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\nimport { typoTestCases } from '..\u002Ffixtures\u002Fsearch-test-data';\n\ntest.describe('Typo Tolerance', () => {\n  for (const { typo, corrected, editDistance } of typoTestCases) {\n    test(`corrects \"${typo}\" (edit distance ${editDistance})`, async ({ page }) => {\n      await page.goto('\u002Fsearch');\n      const searchInput = page.getByRole('searchbox');\n      await searchInput.fill(typo);\n      await searchInput.press('Enter');\n\n      await page.waitForSelector('[data-testid=\"search-results\"]');\n\n      \u002F\u002F Option A: Search auto-corrects and shows results\n      const resultCount = await page.getByTestId('result-count').textContent();\n      const count = parseInt(resultCount || '0', 10);\n\n      if (count > 0) {\n        \u002F\u002F Verify results are relevant to the corrected term\n        const didYouMean = page.getByTestId('did-you-mean');\n        if (await didYouMean.isVisible()) {\n          const suggestion = await didYouMean.textContent();\n          expect(suggestion?.toLowerCase()).toContain(corrected);\n        }\n\n        \u002F\u002F Results should match what the corrected query would return\n        const titles = await page\n          .getByTestId('search-result-title')\n          .allTextContents();\n        const hasRelevantResult = titles.some(\n          (t) =>\n            t.toLowerCase().includes(corrected) ||\n            t.toLowerCase().includes(corrected.split(' ')[0])\n        );\n        expect(hasRelevantResult).toBe(true);\n      } else {\n        \u002F\u002F Option B: Zero results but shows spelling suggestion\n        const didYouMean = page.getByTestId('did-you-mean');\n        await expect(didYouMean).toBeVisible();\n      }\n    });\n  }\n\n  test('preserves user query while showing correction', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('javscript');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F The input should still show the original query\n    await expect(searchInput).toHaveValue('javscript');\n\n    \u002F\u002F But results should be for \"javascript\"\n    const didYouMean = page.getByTestId('did-you-mean');\n    if (await didYouMean.isVisible()) {\n      await expect(didYouMean).toContainText('javascript');\n    }\n  });\n});\n```\n\n### Testing Autocomplete Suggestions\n\nAutocomplete must respond quickly and provide accurate, helpful suggestions as users type.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fautocomplete\u002Fsuggestion-accuracy.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Autocomplete Suggestions', () => {\n  test('shows suggestions after minimum character threshold', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    \u002F\u002F Type one character -- should not trigger autocomplete\n    await searchInput.fill('p');\n    await page.waitForTimeout(300);\n    const suggestionsAfterOne = page.getByTestId('autocomplete-dropdown');\n    await expect(suggestionsAfterOne).not.toBeVisible();\n\n    \u002F\u002F Type second character -- should trigger autocomplete\n    await searchInput.fill('pl');\n    await page.waitForTimeout(300);\n    await expect(suggestionsAfterOne).toBeVisible();\n\n    const suggestions = await page\n      .getByTestId('autocomplete-suggestion')\n      .allTextContents();\n    expect(suggestions.length).toBeGreaterThan(0);\n    expect(suggestions.length).toBeLessThanOrEqual(10);\n  });\n\n  test('suggestions respond within 200ms', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    const startTime = Date.now();\n    await searchInput.fill('test');\n\n    await page.waitForSelector('[data-testid=\"autocomplete-dropdown\"]');\n    const elapsed = Date.now() - startTime;\n\n    \u002F\u002F Autocomplete must appear within 200ms for good UX\n    expect(elapsed).toBeLessThan(200);\n  });\n\n  test('suggestions update as user continues typing', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    await searchInput.fill('play');\n    await page.waitForSelector('[data-testid=\"autocomplete-dropdown\"]');\n    const initialSuggestions = await page\n      .getByTestId('autocomplete-suggestion')\n      .allTextContents();\n\n    await searchInput.fill('playwright');\n    await page.waitForTimeout(300);\n    const refinedSuggestions = await page\n      .getByTestId('autocomplete-suggestion')\n      .allTextContents();\n\n    \u002F\u002F Refined suggestions should be a subset or more specific\n    expect(refinedSuggestions.length).toBeLessThanOrEqual(\n      initialSuggestions.length\n    );\n  });\n\n  test('keyboard navigation works in autocomplete', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    await searchInput.fill('test');\n    await page.waitForSelector('[data-testid=\"autocomplete-dropdown\"]');\n\n    \u002F\u002F Arrow down highlights the first suggestion\n    await searchInput.press('ArrowDown');\n    const firstSuggestion = page\n      .getByTestId('autocomplete-suggestion')\n      .first();\n    await expect(firstSuggestion).toHaveAttribute('aria-selected', 'true');\n\n    \u002F\u002F Enter selects the highlighted suggestion\n    const suggestionText = await firstSuggestion.textContent();\n    await searchInput.press('Enter');\n    await expect(searchInput).toHaveValue(suggestionText || '');\n  });\n\n  test('escape closes autocomplete dropdown', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    await searchInput.fill('test');\n    await page.waitForSelector('[data-testid=\"autocomplete-dropdown\"]');\n\n    await searchInput.press('Escape');\n    await expect(\n      page.getByTestId('autocomplete-dropdown')\n    ).not.toBeVisible();\n  });\n\n  test('highlighted terms match query prefix', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    await searchInput.fill('play');\n    await page.waitForSelector('[data-testid=\"autocomplete-dropdown\"]');\n\n    \u002F\u002F Each suggestion should have the matching portion highlighted\n    const highlights = await page\n      .locator('[data-testid=\"autocomplete-suggestion\"] mark')\n      .allTextContents();\n\n    for (const highlight of highlights) {\n      expect(highlight.toLowerCase()).toContain('play');\n    }\n  });\n});\n```\n\n### Testing Faceted Filter Combinations\n\nFaceted search allows users to narrow results by multiple dimensions. Filters must compose correctly and display accurate counts.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Ffacets\u002Fcombined-facets.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Faceted Filter Combinations', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('testing');\n    await searchInput.press('Enter');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n  });\n\n  test('single facet reduces result count', async ({ page }) => {\n    const initialCount = await getResultCount(page);\n\n    await page.getByTestId('facet-category').getByText('E2E Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const filteredCount = await getResultCount(page);\n    expect(filteredCount).toBeLessThanOrEqual(initialCount);\n    expect(filteredCount).toBeGreaterThan(0);\n  });\n\n  test('combining facets applies AND logic', async ({ page }) => {\n    \u002F\u002F Apply first facet\n    await page.getByTestId('facet-category').getByText('E2E Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n    const afterFirstFacet = await getResultCount(page);\n\n    \u002F\u002F Apply second facet\n    await page.getByTestId('facet-language').getByText('TypeScript').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n    const afterSecondFacet = await getResultCount(page);\n\n    expect(afterSecondFacet).toBeLessThanOrEqual(afterFirstFacet);\n  });\n\n  test('facet counts update after filtering', async ({ page }) => {\n    \u002F\u002F Get initial count for a category facet\n    const initialFacetCount = await page\n      .getByTestId('facet-category')\n      .getByText('E2E Testing')\n      .locator('..')\n      .getByTestId('facet-count')\n      .textContent();\n\n    \u002F\u002F Apply a different facet\n    await page.getByTestId('facet-language').getByText('Python').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Category facet counts should update\n    const updatedFacetCount = await page\n      .getByTestId('facet-category')\n      .getByText('E2E Testing')\n      .locator('..')\n      .getByTestId('facet-count')\n      .textContent();\n\n    const initial = parseInt(initialFacetCount || '0', 10);\n    const updated = parseInt(updatedFacetCount || '0', 10);\n    expect(updated).toBeLessThanOrEqual(initial);\n  });\n\n  test('removing a facet restores previous result set', async ({ page }) => {\n    const initialCount = await getResultCount(page);\n\n    \u002F\u002F Apply facet\n    await page.getByTestId('facet-category').getByText('E2E Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Remove facet\n    await page.getByTestId('active-filter-E2E Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const restoredCount = await getResultCount(page);\n    expect(restoredCount).toBe(initialCount);\n  });\n\n  test('clear all filters resets to original results', async ({ page }) => {\n    const initialCount = await getResultCount(page);\n\n    \u002F\u002F Apply multiple facets\n    await page.getByTestId('facet-category').getByText('E2E Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n    await page.getByTestId('facet-language').getByText('TypeScript').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Clear all\n    await page.getByTestId('clear-all-filters').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const clearedCount = await getResultCount(page);\n    expect(clearedCount).toBe(initialCount);\n  });\n});\n\nasync function getResultCount(page: import('@playwright\u002Ftest').Page): Promise\u003Cnumber> {\n  const text = await page.getByTestId('result-count').textContent();\n  return parseInt(text || '0', 10);\n}\n```\n\n### Testing Zero-Results Pages\n\nWhen search returns no results, the application must provide a helpful experience rather than a dead end.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fzero-results\u002Fempty-state.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Zero Results Handling', () => {\n  test('displays helpful message for no results', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('xyznonexistentqueryzyx');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"zero-results\"]');\n\n    \u002F\u002F Must show a clear \"no results\" message\n    await expect(page.getByTestId('zero-results-message')).toContainText(\n      'No results found'\n    );\n\n    \u002F\u002F Must include the user's query in the message\n    await expect(page.getByTestId('zero-results-message')).toContainText(\n      'xyznonexistentqueryzyx'\n    );\n  });\n\n  test('offers spelling suggestions on zero results', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('tesitng framwork');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"zero-results\"]');\n\n    const suggestion = page.getByTestId('spelling-suggestion');\n    await expect(suggestion).toBeVisible();\n    await expect(suggestion).toContainText('testing framework');\n  });\n\n  test('shows popular searches on zero results', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('asdfjklzxcv');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"zero-results\"]');\n\n    const popularSearches = page.getByTestId('popular-searches');\n    await expect(popularSearches).toBeVisible();\n\n    const links = await popularSearches.getByRole('link').all();\n    expect(links.length).toBeGreaterThan(0);\n  });\n\n  test('zero results page has navigation to browse categories', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('zzz_nothing_matches_this');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"zero-results\"]');\n\n    const browseLink = page.getByTestId('browse-categories-link');\n    await expect(browseLink).toBeVisible();\n    await browseLink.click();\n\n    await expect(page).toHaveURL(\u002F\\\u002Fcategories\u002F);\n  });\n});\n```\n\n### Testing Search Result Pagination\n\nPagination must maintain sort order, correctly handle deep page navigation, and preserve applied filters.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fpagination\u002Fresult-pages.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Search Result Pagination', () => {\n  test('pagination controls appear when results exceed page size', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=testing');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const resultCount = await page.getByTestId('result-count').textContent();\n    const count = parseInt(resultCount || '0', 10);\n\n    if (count > 20) {\n      await expect(page.getByTestId('pagination')).toBeVisible();\n      await expect(page.getByTestId('page-next')).toBeEnabled();\n    }\n  });\n\n  test('navigating pages shows different results', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=testing');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const firstPageResults = await page\n      .getByTestId('search-result-title')\n      .allTextContents();\n\n    await page.getByTestId('page-next').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const secondPageResults = await page\n      .getByTestId('search-result-title')\n      .allTextContents();\n\n    \u002F\u002F Pages must show different results\n    expect(firstPageResults).not.toEqual(secondPageResults);\n  });\n\n  test('sort order persists across pages', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=testing&sort=date-desc');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Get date of last result on page 1\n    const lastDatePage1 = await page\n      .getByTestId('search-result-date')\n      .last()\n      .textContent();\n\n    await page.getByTestId('page-next').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Get date of first result on page 2\n    const firstDatePage2 = await page\n      .getByTestId('search-result-date')\n      .first()\n      .textContent();\n\n    \u002F\u002F Dates should be in descending order across the page boundary\n    const date1 = new Date(lastDatePage1 || '');\n    const date2 = new Date(firstDatePage2 || '');\n    expect(date1.getTime()).toBeGreaterThanOrEqual(date2.getTime());\n  });\n\n  test('filters persist across page navigation', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=testing');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Apply a filter\n    await page.getByTestId('facet-category').getByText('Unit Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Navigate to page 2\n    await page.getByTestId('page-next').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Filter should still be active\n    await expect(\n      page.getByTestId('active-filter-Unit Testing')\n    ).toBeVisible();\n  });\n});\n```\n\n### Testing Special Character Handling and Security\n\nSearch inputs must be sanitized to prevent injection attacks while still supporting legitimate special-character queries.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fsecurity\u002Fspecial-characters.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\nimport { specialCharacterQueries } from '..\u002Ffixtures\u002Fsearch-test-data';\n\ntest.describe('Special Character Handling', () => {\n  for (const query of specialCharacterQueries) {\n    test(`handles special characters: \"${query.substring(0, 30)}...\"`, async ({ page }) => {\n      await page.goto('\u002Fsearch');\n      const searchInput = page.getByRole('searchbox');\n      await searchInput.fill(query);\n      await searchInput.press('Enter');\n\n      \u002F\u002F Must not throw a 500 error\n      const response = await page.waitForResponse(\n        (resp) => resp.url().includes('\u002Fapi\u002Fsearch') || resp.url().includes('\u002Fsearch'),\n        { timeout: 5000 }\n      );\n      expect(response.status()).toBeLessThan(500);\n\n      \u002F\u002F Page must still be functional\n      await expect(page.getByRole('searchbox')).toBeVisible();\n    });\n  }\n\n  test('XSS payloads are sanitized in search results', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('\u003Cimg src=x onerror=alert(1)>');\n    await searchInput.press('Enter');\n\n    \u002F\u002F Wait for results or zero-results state\n    await page.waitForSelector(\n      '[data-testid=\"search-results\"], [data-testid=\"zero-results\"]'\n    );\n\n    \u002F\u002F Verify no script execution occurred\n    const alerts: string[] = [];\n    page.on('dialog', (dialog) => {\n      alerts.push(dialog.message());\n      dialog.dismiss();\n    });\n    await page.waitForTimeout(1000);\n    expect(alerts.length).toBe(0);\n\n    \u002F\u002F Verify the payload is not rendered as HTML\n    const pageContent = await page.content();\n    expect(pageContent).not.toContain('\u003Cimg src=x onerror');\n  });\n\n  test('SQL injection payloads return safe responses', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill(\"'; DROP TABLE products; --\");\n    await searchInput.press('Enter');\n\n    \u002F\u002F Should return zero results or matching results, not an error\n    await page.waitForSelector(\n      '[data-testid=\"search-results\"], [data-testid=\"zero-results\"]'\n    );\n\n    \u002F\u002F Page should be fully functional after the injection attempt\n    await searchInput.clear();\n    await searchInput.fill('headphones');\n    await searchInput.press('Enter');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const resultCount = await page.getByTestId('result-count').textContent();\n    const count = parseInt(resultCount || '0', 10);\n    expect(count).toBeGreaterThan(0);\n  });\n});\n```\n\n### Testing Search Performance Under Load\n\nSearch latency degrades user experience quickly. Measure response times and validate they remain within budget under realistic conditions.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fperformance\u002Flatency.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Search Performance', () => {\n  const LATENCY_BUDGET_MS = 500;\n\n  test('search responds within latency budget', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    const queries = ['testing', 'playwright automation', 'performance testing tools'];\n\n    for (const query of queries) {\n      const startTime = Date.now();\n      await searchInput.fill(query);\n      await searchInput.press('Enter');\n      await page.waitForSelector('[data-testid=\"search-results\"]');\n      const elapsed = Date.now() - startTime;\n\n      expect(elapsed).toBeLessThan(LATENCY_BUDGET_MS);\n      await searchInput.clear();\n    }\n  });\n\n  test('API search endpoint responds within budget', async ({ request }) => {\n    const queries = ['unit testing', 'api testing', 'selenium'];\n\n    for (const query of queries) {\n      const startTime = Date.now();\n      const response = await request.get(`\u002Fapi\u002Fsearch?q=${encodeURIComponent(query)}`);\n      const elapsed = Date.now() - startTime;\n\n      expect(response.status()).toBe(200);\n      expect(elapsed).toBeLessThan(300); \u002F\u002F API should be faster than UI\n\n      const body = await response.json();\n      expect(body.results).toBeDefined();\n    }\n  });\n});\n```\n\n### Testing Search Analytics Event Tracking\n\nEvery search action should emit analytics events for monitoring and improvement.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fanalytics\u002Fevent-tracking.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Search Analytics Tracking', () => {\n  test('search query emits analytics event', async ({ page }) => {\n    const analyticsEvents: any[] = [];\n\n    \u002F\u002F Intercept analytics calls\n    await page.route('**\u002Fapi\u002Ftelemetry\u002F**', async (route) => {\n      const request = route.request();\n      if (request.method() === 'POST') {\n        const body = request.postDataJSON();\n        analyticsEvents.push(body);\n      }\n      await route.fulfill({ status: 200, body: '{}' });\n    });\n\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('playwright');\n    await searchInput.press('Enter');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Allow time for async analytics to fire\n    await page.waitForTimeout(1000);\n\n    const searchEvent = analyticsEvents.find(\n      (e) => e.event === 'search' || e.event === 'search_query'\n    );\n    expect(searchEvent).toBeDefined();\n    expect(searchEvent.properties?.query).toBe('playwright');\n  });\n\n  test('result click emits click-through event with position', async ({ page }) => {\n    const analyticsEvents: any[] = [];\n\n    await page.route('**\u002Fapi\u002Ftelemetry\u002F**', async (route) => {\n      const request = route.request();\n      if (request.method() === 'POST') {\n        const body = request.postDataJSON();\n        analyticsEvents.push(body);\n      }\n      await route.fulfill({ status: 200, body: '{}' });\n    });\n\n    await page.goto('\u002Fsearch?q=testing');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Click the second result\n    await page.getByTestId('search-result').nth(1).click();\n    await page.waitForTimeout(1000);\n\n    const clickEvent = analyticsEvents.find(\n      (e) => e.event === 'search_result_click'\n    );\n    expect(clickEvent).toBeDefined();\n    expect(clickEvent.properties?.position).toBe(2);\n    expect(clickEvent.properties?.query).toBe('testing');\n  });\n});\n```\n\n### Testing Highlighted\u002FBolded Term Matching\n\nSearch result snippets should highlight the matching query terms to help users quickly assess relevance.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Frelevance\u002Fhighlight-matching.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Search Term Highlighting', () => {\n  test('query terms are highlighted in result titles', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=playwright');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const highlights = await page\n      .getByTestId('search-result-title')\n      .first()\n      .locator('mark, strong, em.highlight')\n      .allTextContents();\n\n    expect(highlights.length).toBeGreaterThan(0);\n    expect(highlights.some((h) => h.toLowerCase().includes('playwright'))).toBe(true);\n  });\n\n  test('multi-word queries highlight each term', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=playwright+testing');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const allHighlights = await page\n      .locator('[data-testid=\"search-result\"] mark')\n      .allTextContents();\n\n    const highlightedText = allHighlights.join(' ').toLowerCase();\n    expect(highlightedText).toContain('playwright');\n    expect(highlightedText).toContain('testing');\n  });\n\n  test('highlights do not break HTML structure', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=\u003Cb>test\u003C\u002Fb>');\n    await page.waitForSelector(\n      '[data-testid=\"search-results\"], [data-testid=\"zero-results\"]'\n    );\n\n    \u002F\u002F Ensure no raw HTML tags appear in rendered text\n    const resultArea = page.getByTestId('search-results');\n    if (await resultArea.isVisible()) {\n      const text = await resultArea.textContent();\n      expect(text).not.toContain('\u003Cb>');\n      expect(text).not.toContain('\u003C\u002Fb>');\n    }\n  });\n});\n```\n\n## Best Practices\n\n1. **Use data-driven test cases** -- Define expected search results in fixture files rather than hardcoding them in tests. This makes it easy to update expectations when the search index changes and enables non-developers to maintain test data.\n\n2. **Test relevance with ranked assertions** -- Do not just check that a result appears somewhere in the list. Assert that it appears in the top N positions. Relevance regression often manifests as correct results dropping below the fold.\n\n3. **Separate relevance tests from functional tests** -- Relevance tests are inherently more fragile because they depend on indexed content. Keep them in a dedicated suite with their own fixtures so they can be run independently.\n\n4. **Use stable test data** -- Seed a known dataset before running search tests rather than relying on production data. This eliminates flakiness caused by content changes and ensures consistent relevance rankings.\n\n5. **Test both the API and the UI** -- Search API tests are faster and more precise for verifying ranking logic. UI tests are necessary for verifying autocomplete interactions, highlighting, and facet controls. Use both layers strategically.\n\n6. **Measure search latency in CI** -- Add performance assertions to your CI pipeline. Search latency regressions are subtle and accumulate over time. A 500ms budget that fails the build prevents gradual degradation.\n\n7. **Test facets with realistic combinations** -- Users apply multiple filters simultaneously. Test the most common two-facet and three-facet combinations, not just individual facets in isolation.\n\n8. **Verify facet counts match actual results** -- A facet showing \"TypeScript (15)\" must produce exactly 15 results when clicked. Mismatched counts erode user trust in the search interface.\n\n9. **Test autocomplete debouncing** -- Autocomplete should debounce rapid keystrokes to avoid overwhelming the server. Verify that typing quickly does not produce stale or out-of-order suggestions.\n\n10. **Handle empty queries gracefully** -- Submitting an empty search should either show trending\u002Fpopular results or display a helpful message, never an error or completely blank page.\n\n11. **Test search URL state** -- Search queries, filters, sort order, and page number should be reflected in the URL. Users expect to share search URLs and use the browser back button to return to previous searches.\n\n12. **Validate accessibility of search components** -- The search input must have proper ARIA labels. Autocomplete dropdowns must support keyboard navigation with correct `aria-expanded`, `aria-activedescendant`, and `role=\"listbox\"` attributes.\n\n## Anti-Patterns to Avoid\n\n1. **Asserting exact result counts** -- Search indices change frequently. Asserting `expect(count).toBe(47)` will break whenever content is added or removed. Use range assertions like `toBeGreaterThan(10)` or `toBeLessThan(100)` instead.\n\n2. **Ignoring search debounce in tests** -- Failing to account for autocomplete debounce timing leads to tests that pass locally but fail in CI. Always wait for the debounce period or intercept the underlying API call rather than using fixed timeouts.\n\n3. **Testing relevance against production data** -- Production data changes constantly. A test that passes today will fail tomorrow when new content is indexed. Always seed a controlled dataset for relevance tests.\n\n4. **Hardcoding page sizes** -- If the application changes its default page size from 20 to 25, hardcoded assertions will break. Read the page size from configuration or infer it from the results.\n\n5. **Skipping zero-results scenarios** -- The zero-results page is often the most neglected UX surface. Users who see a blank dead end will leave. Always test what happens when no results match.\n\n6. **Testing only happy-path queries** -- Real users type misspelled words, paste URLs into search boxes, enter single characters, and submit empty forms. Test the full spectrum of realistic and adversarial inputs.\n\n7. **Ignoring search result snippet quality** -- Search results often show a snippet or excerpt. If the snippet does not contain the query terms, the result appears irrelevant even when it is not. Verify snippet content alongside title matching.\n\n## Debugging Tips\n\n- **Stale search index**: If relevance tests fail unexpectedly, check whether the search index has been rebuilt after recent data changes. Most search engines (Typesense, Elasticsearch, Algolia) have a reindexing delay.\n\n- **Autocomplete timing failures**: If autocomplete tests fail intermittently, increase the debounce wait or switch to intercepting the API response with `page.waitForResponse()` instead of using `waitForTimeout()`.\n\n- **Facet count mismatches**: When facet counts do not match actual results, check whether the search engine is using cached facet counts from a previous index state. Force a cache invalidation or reindex before running facet tests.\n\n- **Encoding issues in URL state**: If search queries with special characters break when loaded from URLs, verify that the application properly encodes and decodes query parameters using `encodeURIComponent` \u002F `decodeURIComponent`.\n\n- **Flaky relevance order**: If the same query sometimes returns results in different orders, check whether the search engine uses a tie-breaking strategy for equally scored results. Add a secondary sort by ID or date to ensure deterministic ordering.\n\n- **Performance test variance**: Search latency measurements can vary due to cold starts, garbage collection, and network conditions. Run performance tests multiple times and use the median rather than a single measurement. Consider warming up the search engine with a few queries before measuring.\n\n- **Highlighting breaks with special regex characters**: If highlight markup is missing for queries containing regex metacharacters (`.`, `*`, `+`, `?`), verify that the search engine escapes these characters before applying highlight patterns.\n\n- **Mobile autocomplete differences**: On mobile devices, the virtual keyboard may obscure the autocomplete dropdown. Test autocomplete visibility and interaction on mobile viewports using Playwright's device emulation.\n","# Search Quality Tester Skill\n\nYou are an expert QA engineer specializing in search functionality testing. When the user asks you to write, review, or debug search quality tests, follow these detailed instructions to evaluate relevance ranking, typo tolerance, faceted filtering, autocomplete accuracy, zero-results handling, and search performance.\n\n## Core Principles\n\n1. **Relevance is king** -- Search results must be ordered by relevance to the query, not by insertion order or random criteria. Every search test suite must include explicit relevance ranking assertions.\n2. **Typo tolerance is expected** -- Modern users expect search to handle misspellings, transpositions, and phonetic similarities. Test fuzzy matching systematically with controlled edit distances.\n3. **Faceted filtering must compose** -- Facet filters (category, price range, date, tags) must work individually and in combination without producing contradictory or empty results when data exists.\n4. **Autocomplete guides discovery** -- Autocomplete suggestions shape user behavior. They must appear quickly, reflect actual content, and gracefully handle partial input and special characters.\n5. **Zero results is a UX moment** -- A blank screen with no guidance is a failure. Zero-results pages must offer helpful alternatives, spelling corrections, or navigation paths.\n6. **Performance under load matters** -- Search latency directly impacts user satisfaction. Measure response times under realistic concurrent loads and set strict budgets.\n7. **Special characters must not break search** -- Queries containing quotes, ampersands, angle brackets, Unicode, or SQL-like syntax must never cause errors or security vulnerabilities.\n8. **Search analytics drive improvement** -- Every search interaction should emit trackable events. Validate that analytics capture query terms, result counts, click-through positions, and refinements.\n\n## Project Structure\n\n```\ntests\u002F\n  search\u002F\n    relevance\u002F\n      keyword-ranking.spec.ts\n      phrase-matching.spec.ts\n      boost-rules.spec.ts\n    typo-tolerance\u002F\n      edit-distance.spec.ts\n      phonetic-matching.spec.ts\n      transposition.spec.ts\n    autocomplete\u002F\n      suggestion-accuracy.spec.ts\n      suggestion-speed.spec.ts\n      partial-input.spec.ts\n    facets\u002F\n      single-facet.spec.ts\n      combined-facets.spec.ts\n      facet-counts.spec.ts\n    zero-results\u002F\n      empty-state.spec.ts\n      suggestions.spec.ts\n      spelling-correction.spec.ts\n    pagination\u002F\n      result-pages.spec.ts\n      deep-pagination.spec.ts\n      sort-order-persistence.spec.ts\n    performance\u002F\n      latency.spec.ts\n      concurrent-search.spec.ts\n    security\u002F\n      special-characters.spec.ts\n      injection-prevention.spec.ts\n    analytics\u002F\n      event-tracking.spec.ts\n      click-tracking.spec.ts\n    fixtures\u002F\n      search-test-data.ts\n      expected-rankings.ts\n    utils\u002F\n      search-helpers.ts\n      relevance-scorer.ts\nplaywright.config.ts\n```\n\n## Configuration\n\n```typescript\n\u002F\u002F playwright.config.ts\nimport { defineConfig, devices } from '@playwright\u002Ftest';\n\nexport default defineConfig({\n  testDir: '.\u002Ftests\u002Fsearch',\n  fullyParallel: true,\n  retries: 1,\n  workers: process.env.CI ? 2 : undefined,\n  reporter: [\n    ['html', { open: 'never' }],\n    ['json', { outputFile: 'search-test-results.json' }],\n  ],\n  use: {\n    baseURL: process.env.BASE_URL || 'http:\u002F\u002Flocalhost:3000',\n    trace: 'retain-on-failure',\n    screenshot: 'only-on-failure',\n  },\n  projects: [\n    {\n      name: 'search-chromium',\n      use: { ...devices['Desktop Chrome'] },\n    },\n    {\n      name: 'search-mobile',\n      use: { ...devices['Pixel 5'] },\n    },\n  ],\n});\n```\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Ffixtures\u002Fsearch-test-data.ts\nexport interface SearchTestCase {\n  query: string;\n  expectedTopResults: string[];\n  expectedMinCount: number;\n  category?: string;\n  description: string;\n}\n\nexport const relevanceTestCases: SearchTestCase[] = [\n  {\n    query: 'wireless bluetooth headphones',\n    expectedTopResults: ['Sony WH-1000XM5', 'Bose QuietComfort Ultra'],\n    expectedMinCount: 10,\n    description: 'Exact product category match should rank top brands first',\n  },\n  {\n    query: 'headphones',\n    expectedTopResults: ['Sony WH-1000XM5', 'AirPods Max', 'Bose QuietComfort Ultra'],\n    expectedMinCount: 20,\n    description: 'Broad category term should return all headphone products',\n  },\n  {\n    query: '\"noise cancelling\"',\n    expectedTopResults: ['Sony WH-1000XM5', 'Bose QuietComfort Ultra'],\n    expectedMinCount: 5,\n    description: 'Quoted phrase should match exact phrase in descriptions',\n  },\n];\n\nexport const typoTestCases = [\n  { typo: 'headphoens', corrected: 'headphones', editDistance: 1 },\n  { typo: 'bluetooh', corrected: 'bluetooth', editDistance: 1 },\n  { typo: 'wireles', corrected: 'wireless', editDistance: 1 },\n  { typo: 'heeadphones', corrected: 'headphones', editDistance: 2 },\n  { typo: 'blutooth', corrected: 'bluetooth', editDistance: 2 },\n];\n\nexport const specialCharacterQueries = [\n  'C++ programming',\n  'rock & roll',\n  '\u003Cscript>alert(\"xss\")\u003C\u002Fscript>',\n  \"O'Reilly books\",\n  'price: $50-$100',\n  'SELECT * FROM products',\n  'search term\\x00null byte',\n  'emoji search 🎧',\n  '   leading trailing spaces   ',\n  '',\n];\n```\n\n## How-To Guides\n\n### Testing Search Relevance Ranking\n\nRelevance ranking is the most critical aspect of search quality. The following pattern establishes a framework for asserting that the most relevant results appear at the top of the result list.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Frelevance\u002Fkeyword-ranking.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\nimport { relevanceTestCases } from '..\u002Ffixtures\u002Fsearch-test-data';\n\ntest.describe('Search Relevance Ranking', () => {\n  for (const testCase of relevanceTestCases) {\n    test(`relevance: ${testCase.description}`, async ({ page }) => {\n      await page.goto('\u002Fsearch');\n      const searchInput = page.getByRole('searchbox');\n      await searchInput.fill(testCase.query);\n      await searchInput.press('Enter');\n\n      \u002F\u002F Wait for results to load\n      await page.waitForSelector('[data-testid=\"search-results\"]');\n\n      \u002F\u002F Verify minimum result count\n      const resultCount = await page\n        .getByTestId('result-count')\n        .textContent();\n      const count = parseInt(resultCount || '0', 10);\n      expect(count).toBeGreaterThanOrEqual(testCase.expectedMinCount);\n\n      \u002F\u002F Verify top results contain expected items\n      const topResults = await page\n        .getByTestId('search-result-title')\n        .allTextContents();\n      const topN = topResults.slice(0, testCase.expectedTopResults.length);\n\n      for (const expectedResult of testCase.expectedTopResults) {\n        expect(topN).toContain(expectedResult);\n      }\n    });\n  }\n\n  test('exact match ranks above partial match', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('Playwright Testing Guide');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const firstResult = await page\n      .getByTestId('search-result-title')\n      .first()\n      .textContent();\n\n    \u002F\u002F Exact title match must be the first result\n    expect(firstResult).toBe('Playwright Testing Guide');\n  });\n\n  test('boosted fields rank higher than body text', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('performance optimization');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Items with \"performance optimization\" in the title should\n    \u002F\u002F rank above items that only mention it in the description\n    const results = await page.getByTestId('search-result').all();\n    expect(results.length).toBeGreaterThan(2);\n\n    const firstTitle = await results[0]\n      .getByTestId('search-result-title')\n      .textContent();\n    expect(firstTitle?.toLowerCase()).toContain('performance');\n  });\n});\n```\n\n### Testing Typo Tolerance and Fuzzy Matching\n\nUsers frequently misspell queries. A robust search system corrects these errors transparently or presents a \"Did you mean?\" prompt.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Ftypo-tolerance\u002Fedit-distance.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\nimport { typoTestCases } from '..\u002Ffixtures\u002Fsearch-test-data';\n\ntest.describe('Typo Tolerance', () => {\n  for (const { typo, corrected, editDistance } of typoTestCases) {\n    test(`corrects \"${typo}\" (edit distance ${editDistance})`, async ({ page }) => {\n      await page.goto('\u002Fsearch');\n      const searchInput = page.getByRole('searchbox');\n      await searchInput.fill(typo);\n      await searchInput.press('Enter');\n\n      await page.waitForSelector('[data-testid=\"search-results\"]');\n\n      \u002F\u002F Option A: Search auto-corrects and shows results\n      const resultCount = await page.getByTestId('result-count').textContent();\n      const count = parseInt(resultCount || '0', 10);\n\n      if (count > 0) {\n        \u002F\u002F Verify results are relevant to the corrected term\n        const didYouMean = page.getByTestId('did-you-mean');\n        if (await didYouMean.isVisible()) {\n          const suggestion = await didYouMean.textContent();\n          expect(suggestion?.toLowerCase()).toContain(corrected);\n        }\n\n        \u002F\u002F Results should match what the corrected query would return\n        const titles = await page\n          .getByTestId('search-result-title')\n          .allTextContents();\n        const hasRelevantResult = titles.some(\n          (t) =>\n            t.toLowerCase().includes(corrected) ||\n            t.toLowerCase().includes(corrected.split(' ')[0])\n        );\n        expect(hasRelevantResult).toBe(true);\n      } else {\n        \u002F\u002F Option B: Zero results but shows spelling suggestion\n        const didYouMean = page.getByTestId('did-you-mean');\n        await expect(didYouMean).toBeVisible();\n      }\n    });\n  }\n\n  test('preserves user query while showing correction', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('javscript');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F The input should still show the original query\n    await expect(searchInput).toHaveValue('javscript');\n\n    \u002F\u002F But results should be for \"javascript\"\n    const didYouMean = page.getByTestId('did-you-mean');\n    if (await didYouMean.isVisible()) {\n      await expect(didYouMean).toContainText('javascript');\n    }\n  });\n});\n```\n\n### Testing Autocomplete Suggestions\n\nAutocomplete must respond quickly and provide accurate, helpful suggestions as users type.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fautocomplete\u002Fsuggestion-accuracy.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Autocomplete Suggestions', () => {\n  test('shows suggestions after minimum character threshold', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    \u002F\u002F Type one character -- should not trigger autocomplete\n    await searchInput.fill('p');\n    await page.waitForTimeout(300);\n    const suggestionsAfterOne = page.getByTestId('autocomplete-dropdown');\n    await expect(suggestionsAfterOne).not.toBeVisible();\n\n    \u002F\u002F Type second character -- should trigger autocomplete\n    await searchInput.fill('pl');\n    await page.waitForTimeout(300);\n    await expect(suggestionsAfterOne).toBeVisible();\n\n    const suggestions = await page\n      .getByTestId('autocomplete-suggestion')\n      .allTextContents();\n    expect(suggestions.length).toBeGreaterThan(0);\n    expect(suggestions.length).toBeLessThanOrEqual(10);\n  });\n\n  test('suggestions respond within 200ms', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    const startTime = Date.now();\n    await searchInput.fill('test');\n\n    await page.waitForSelector('[data-testid=\"autocomplete-dropdown\"]');\n    const elapsed = Date.now() - startTime;\n\n    \u002F\u002F Autocomplete must appear within 200ms for good UX\n    expect(elapsed).toBeLessThan(200);\n  });\n\n  test('suggestions update as user continues typing', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    await searchInput.fill('play');\n    await page.waitForSelector('[data-testid=\"autocomplete-dropdown\"]');\n    const initialSuggestions = await page\n      .getByTestId('autocomplete-suggestion')\n      .allTextContents();\n\n    await searchInput.fill('playwright');\n    await page.waitForTimeout(300);\n    const refinedSuggestions = await page\n      .getByTestId('autocomplete-suggestion')\n      .allTextContents();\n\n    \u002F\u002F Refined suggestions should be a subset or more specific\n    expect(refinedSuggestions.length).toBeLessThanOrEqual(\n      initialSuggestions.length\n    );\n  });\n\n  test('keyboard navigation works in autocomplete', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    await searchInput.fill('test');\n    await page.waitForSelector('[data-testid=\"autocomplete-dropdown\"]');\n\n    \u002F\u002F Arrow down highlights the first suggestion\n    await searchInput.press('ArrowDown');\n    const firstSuggestion = page\n      .getByTestId('autocomplete-suggestion')\n      .first();\n    await expect(firstSuggestion).toHaveAttribute('aria-selected', 'true');\n\n    \u002F\u002F Enter selects the highlighted suggestion\n    const suggestionText = await firstSuggestion.textContent();\n    await searchInput.press('Enter');\n    await expect(searchInput).toHaveValue(suggestionText || '');\n  });\n\n  test('escape closes autocomplete dropdown', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    await searchInput.fill('test');\n    await page.waitForSelector('[data-testid=\"autocomplete-dropdown\"]');\n\n    await searchInput.press('Escape');\n    await expect(\n      page.getByTestId('autocomplete-dropdown')\n    ).not.toBeVisible();\n  });\n\n  test('highlighted terms match query prefix', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    await searchInput.fill('play');\n    await page.waitForSelector('[data-testid=\"autocomplete-dropdown\"]');\n\n    \u002F\u002F Each suggestion should have the matching portion highlighted\n    const highlights = await page\n      .locator('[data-testid=\"autocomplete-suggestion\"] mark')\n      .allTextContents();\n\n    for (const highlight of highlights) {\n      expect(highlight.toLowerCase()).toContain('play');\n    }\n  });\n});\n```\n\n### Testing Faceted Filter Combinations\n\nFaceted search allows users to narrow results by multiple dimensions. Filters must compose correctly and display accurate counts.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Ffacets\u002Fcombined-facets.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Faceted Filter Combinations', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('testing');\n    await searchInput.press('Enter');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n  });\n\n  test('single facet reduces result count', async ({ page }) => {\n    const initialCount = await getResultCount(page);\n\n    await page.getByTestId('facet-category').getByText('E2E Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const filteredCount = await getResultCount(page);\n    expect(filteredCount).toBeLessThanOrEqual(initialCount);\n    expect(filteredCount).toBeGreaterThan(0);\n  });\n\n  test('combining facets applies AND logic', async ({ page }) => {\n    \u002F\u002F Apply first facet\n    await page.getByTestId('facet-category').getByText('E2E Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n    const afterFirstFacet = await getResultCount(page);\n\n    \u002F\u002F Apply second facet\n    await page.getByTestId('facet-language').getByText('TypeScript').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n    const afterSecondFacet = await getResultCount(page);\n\n    expect(afterSecondFacet).toBeLessThanOrEqual(afterFirstFacet);\n  });\n\n  test('facet counts update after filtering', async ({ page }) => {\n    \u002F\u002F Get initial count for a category facet\n    const initialFacetCount = await page\n      .getByTestId('facet-category')\n      .getByText('E2E Testing')\n      .locator('..')\n      .getByTestId('facet-count')\n      .textContent();\n\n    \u002F\u002F Apply a different facet\n    await page.getByTestId('facet-language').getByText('Python').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Category facet counts should update\n    const updatedFacetCount = await page\n      .getByTestId('facet-category')\n      .getByText('E2E Testing')\n      .locator('..')\n      .getByTestId('facet-count')\n      .textContent();\n\n    const initial = parseInt(initialFacetCount || '0', 10);\n    const updated = parseInt(updatedFacetCount || '0', 10);\n    expect(updated).toBeLessThanOrEqual(initial);\n  });\n\n  test('removing a facet restores previous result set', async ({ page }) => {\n    const initialCount = await getResultCount(page);\n\n    \u002F\u002F Apply facet\n    await page.getByTestId('facet-category').getByText('E2E Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Remove facet\n    await page.getByTestId('active-filter-E2E Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const restoredCount = await getResultCount(page);\n    expect(restoredCount).toBe(initialCount);\n  });\n\n  test('clear all filters resets to original results', async ({ page }) => {\n    const initialCount = await getResultCount(page);\n\n    \u002F\u002F Apply multiple facets\n    await page.getByTestId('facet-category').getByText('E2E Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n    await page.getByTestId('facet-language').getByText('TypeScript').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Clear all\n    await page.getByTestId('clear-all-filters').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const clearedCount = await getResultCount(page);\n    expect(clearedCount).toBe(initialCount);\n  });\n});\n\nasync function getResultCount(page: import('@playwright\u002Ftest').Page): Promise\u003Cnumber> {\n  const text = await page.getByTestId('result-count').textContent();\n  return parseInt(text || '0', 10);\n}\n```\n\n### Testing Zero-Results Pages\n\nWhen search returns no results, the application must provide a helpful experience rather than a dead end.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fzero-results\u002Fempty-state.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Zero Results Handling', () => {\n  test('displays helpful message for no results', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('xyznonexistentqueryzyx');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"zero-results\"]');\n\n    \u002F\u002F Must show a clear \"no results\" message\n    await expect(page.getByTestId('zero-results-message')).toContainText(\n      'No results found'\n    );\n\n    \u002F\u002F Must include the user's query in the message\n    await expect(page.getByTestId('zero-results-message')).toContainText(\n      'xyznonexistentqueryzyx'\n    );\n  });\n\n  test('offers spelling suggestions on zero results', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('tesitng framwork');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"zero-results\"]');\n\n    const suggestion = page.getByTestId('spelling-suggestion');\n    await expect(suggestion).toBeVisible();\n    await expect(suggestion).toContainText('testing framework');\n  });\n\n  test('shows popular searches on zero results', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('asdfjklzxcv');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"zero-results\"]');\n\n    const popularSearches = page.getByTestId('popular-searches');\n    await expect(popularSearches).toBeVisible();\n\n    const links = await popularSearches.getByRole('link').all();\n    expect(links.length).toBeGreaterThan(0);\n  });\n\n  test('zero results page has navigation to browse categories', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('zzz_nothing_matches_this');\n    await searchInput.press('Enter');\n\n    await page.waitForSelector('[data-testid=\"zero-results\"]');\n\n    const browseLink = page.getByTestId('browse-categories-link');\n    await expect(browseLink).toBeVisible();\n    await browseLink.click();\n\n    await expect(page).toHaveURL(\u002F\\\u002Fcategories\u002F);\n  });\n});\n```\n\n### Testing Search Result Pagination\n\nPagination must maintain sort order, correctly handle deep page navigation, and preserve applied filters.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fpagination\u002Fresult-pages.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Search Result Pagination', () => {\n  test('pagination controls appear when results exceed page size', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=testing');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const resultCount = await page.getByTestId('result-count').textContent();\n    const count = parseInt(resultCount || '0', 10);\n\n    if (count > 20) {\n      await expect(page.getByTestId('pagination')).toBeVisible();\n      await expect(page.getByTestId('page-next')).toBeEnabled();\n    }\n  });\n\n  test('navigating pages shows different results', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=testing');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const firstPageResults = await page\n      .getByTestId('search-result-title')\n      .allTextContents();\n\n    await page.getByTestId('page-next').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const secondPageResults = await page\n      .getByTestId('search-result-title')\n      .allTextContents();\n\n    \u002F\u002F Pages must show different results\n    expect(firstPageResults).not.toEqual(secondPageResults);\n  });\n\n  test('sort order persists across pages', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=testing&sort=date-desc');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Get date of last result on page 1\n    const lastDatePage1 = await page\n      .getByTestId('search-result-date')\n      .last()\n      .textContent();\n\n    await page.getByTestId('page-next').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Get date of first result on page 2\n    const firstDatePage2 = await page\n      .getByTestId('search-result-date')\n      .first()\n      .textContent();\n\n    \u002F\u002F Dates should be in descending order across the page boundary\n    const date1 = new Date(lastDatePage1 || '');\n    const date2 = new Date(firstDatePage2 || '');\n    expect(date1.getTime()).toBeGreaterThanOrEqual(date2.getTime());\n  });\n\n  test('filters persist across page navigation', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=testing');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Apply a filter\n    await page.getByTestId('facet-category').getByText('Unit Testing').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Navigate to page 2\n    await page.getByTestId('page-next').click();\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Filter should still be active\n    await expect(\n      page.getByTestId('active-filter-Unit Testing')\n    ).toBeVisible();\n  });\n});\n```\n\n### Testing Special Character Handling and Security\n\nSearch inputs must be sanitized to prevent injection attacks while still supporting legitimate special-character queries.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fsecurity\u002Fspecial-characters.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\nimport { specialCharacterQueries } from '..\u002Ffixtures\u002Fsearch-test-data';\n\ntest.describe('Special Character Handling', () => {\n  for (const query of specialCharacterQueries) {\n    test(`handles special characters: \"${query.substring(0, 30)}...\"`, async ({ page }) => {\n      await page.goto('\u002Fsearch');\n      const searchInput = page.getByRole('searchbox');\n      await searchInput.fill(query);\n      await searchInput.press('Enter');\n\n      \u002F\u002F Must not throw a 500 error\n      const response = await page.waitForResponse(\n        (resp) => resp.url().includes('\u002Fapi\u002Fsearch') || resp.url().includes('\u002Fsearch'),\n        { timeout: 5000 }\n      );\n      expect(response.status()).toBeLessThan(500);\n\n      \u002F\u002F Page must still be functional\n      await expect(page.getByRole('searchbox')).toBeVisible();\n    });\n  }\n\n  test('XSS payloads are sanitized in search results', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('\u003Cimg src=x onerror=alert(1)>');\n    await searchInput.press('Enter');\n\n    \u002F\u002F Wait for results or zero-results state\n    await page.waitForSelector(\n      '[data-testid=\"search-results\"], [data-testid=\"zero-results\"]'\n    );\n\n    \u002F\u002F Verify no script execution occurred\n    const alerts: string[] = [];\n    page.on('dialog', (dialog) => {\n      alerts.push(dialog.message());\n      dialog.dismiss();\n    });\n    await page.waitForTimeout(1000);\n    expect(alerts.length).toBe(0);\n\n    \u002F\u002F Verify the payload is not rendered as HTML\n    const pageContent = await page.content();\n    expect(pageContent).not.toContain('\u003Cimg src=x onerror');\n  });\n\n  test('SQL injection payloads return safe responses', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill(\"'; DROP TABLE products; --\");\n    await searchInput.press('Enter');\n\n    \u002F\u002F Should return zero results or matching results, not an error\n    await page.waitForSelector(\n      '[data-testid=\"search-results\"], [data-testid=\"zero-results\"]'\n    );\n\n    \u002F\u002F Page should be fully functional after the injection attempt\n    await searchInput.clear();\n    await searchInput.fill('headphones');\n    await searchInput.press('Enter');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const resultCount = await page.getByTestId('result-count').textContent();\n    const count = parseInt(resultCount || '0', 10);\n    expect(count).toBeGreaterThan(0);\n  });\n});\n```\n\n### Testing Search Performance Under Load\n\nSearch latency degrades user experience quickly. Measure response times and validate they remain within budget under realistic conditions.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fperformance\u002Flatency.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Search Performance', () => {\n  const LATENCY_BUDGET_MS = 500;\n\n  test('search responds within latency budget', async ({ page }) => {\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n\n    const queries = ['testing', 'playwright automation', 'performance testing tools'];\n\n    for (const query of queries) {\n      const startTime = Date.now();\n      await searchInput.fill(query);\n      await searchInput.press('Enter');\n      await page.waitForSelector('[data-testid=\"search-results\"]');\n      const elapsed = Date.now() - startTime;\n\n      expect(elapsed).toBeLessThan(LATENCY_BUDGET_MS);\n      await searchInput.clear();\n    }\n  });\n\n  test('API search endpoint responds within budget', async ({ request }) => {\n    const queries = ['unit testing', 'api testing', 'selenium'];\n\n    for (const query of queries) {\n      const startTime = Date.now();\n      const response = await request.get(`\u002Fapi\u002Fsearch?q=${encodeURIComponent(query)}`);\n      const elapsed = Date.now() - startTime;\n\n      expect(response.status()).toBe(200);\n      expect(elapsed).toBeLessThan(300); \u002F\u002F API should be faster than UI\n\n      const body = await response.json();\n      expect(body.results).toBeDefined();\n    }\n  });\n});\n```\n\n### Testing Search Analytics Event Tracking\n\nEvery search action should emit analytics events for monitoring and improvement.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Fanalytics\u002Fevent-tracking.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Search Analytics Tracking', () => {\n  test('search query emits analytics event', async ({ page }) => {\n    const analyticsEvents: any[] = [];\n\n    \u002F\u002F Intercept analytics calls\n    await page.route('**\u002Fapi\u002Ftelemetry\u002F**', async (route) => {\n      const request = route.request();\n      if (request.method() === 'POST') {\n        const body = request.postDataJSON();\n        analyticsEvents.push(body);\n      }\n      await route.fulfill({ status: 200, body: '{}' });\n    });\n\n    await page.goto('\u002Fsearch');\n    const searchInput = page.getByRole('searchbox');\n    await searchInput.fill('playwright');\n    await searchInput.press('Enter');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Allow time for async analytics to fire\n    await page.waitForTimeout(1000);\n\n    const searchEvent = analyticsEvents.find(\n      (e) => e.event === 'search' || e.event === 'search_query'\n    );\n    expect(searchEvent).toBeDefined();\n    expect(searchEvent.properties?.query).toBe('playwright');\n  });\n\n  test('result click emits click-through event with position', async ({ page }) => {\n    const analyticsEvents: any[] = [];\n\n    await page.route('**\u002Fapi\u002Ftelemetry\u002F**', async (route) => {\n      const request = route.request();\n      if (request.method() === 'POST') {\n        const body = request.postDataJSON();\n        analyticsEvents.push(body);\n      }\n      await route.fulfill({ status: 200, body: '{}' });\n    });\n\n    await page.goto('\u002Fsearch?q=testing');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    \u002F\u002F Click the second result\n    await page.getByTestId('search-result').nth(1).click();\n    await page.waitForTimeout(1000);\n\n    const clickEvent = analyticsEvents.find(\n      (e) => e.event === 'search_result_click'\n    );\n    expect(clickEvent).toBeDefined();\n    expect(clickEvent.properties?.position).toBe(2);\n    expect(clickEvent.properties?.query).toBe('testing');\n  });\n});\n```\n\n### Testing Highlighted\u002FBolded Term Matching\n\nSearch result snippets should highlight the matching query terms to help users quickly assess relevance.\n\n```typescript\n\u002F\u002F tests\u002Fsearch\u002Frelevance\u002Fhighlight-matching.spec.ts\nimport { test, expect } from '@playwright\u002Ftest';\n\ntest.describe('Search Term Highlighting', () => {\n  test('query terms are highlighted in result titles', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=playwright');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const highlights = await page\n      .getByTestId('search-result-title')\n      .first()\n      .locator('mark, strong, em.highlight')\n      .allTextContents();\n\n    expect(highlights.length).toBeGreaterThan(0);\n    expect(highlights.some((h) => h.toLowerCase().includes('playwright'))).toBe(true);\n  });\n\n  test('multi-word queries highlight each term', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=playwright+testing');\n    await page.waitForSelector('[data-testid=\"search-results\"]');\n\n    const allHighlights = await page\n      .locator('[data-testid=\"search-result\"] mark')\n      .allTextContents();\n\n    const highlightedText = allHighlights.join(' ').toLowerCase();\n    expect(highlightedText).toContain('playwright');\n    expect(highlightedText).toContain('testing');\n  });\n\n  test('highlights do not break HTML structure', async ({ page }) => {\n    await page.goto('\u002Fsearch?q=\u003Cb>test\u003C\u002Fb>');\n    await page.waitForSelector(\n      '[data-testid=\"search-results\"], [data-testid=\"zero-results\"]'\n    );\n\n    \u002F\u002F Ensure no raw HTML tags appear in rendered text\n    const resultArea = page.getByTestId('search-results');\n    if (await resultArea.isVisible()) {\n      const text = await resultArea.textContent();\n      expect(text).not.toContain('\u003Cb>');\n      expect(text).not.toContain('\u003C\u002Fb>');\n    }\n  });\n});\n```\n\n## Best Practices\n\n1. **Use data-driven test cases** -- Define expected search results in fixture files rather than hardcoding them in tests. This makes it easy to update expectations when the search index changes and enables non-developers to maintain test data.\n\n2. **Test relevance with ranked assertions** -- Do not just check that a result appears somewhere in the list. Assert that it appears in the top N positions. Relevance regression often manifests as correct results dropping below the fold.\n\n3. **Separate relevance tests from functional tests** -- Relevance tests are inherently more fragile because they depend on indexed content. Keep them in a dedicated suite with their own fixtures so they can be run independently.\n\n4. **Use stable test data** -- Seed a known dataset before running search tests rather than relying on production data. This eliminates flakiness caused by content changes and ensures consistent relevance rankings.\n\n5. **Test both the API and the UI** -- Search API tests are faster and more precise for verifying ranking logic. UI tests are necessary for verifying autocomplete interactions, highlighting, and facet controls. Use both layers strategically.\n\n6. **Measure search latency in CI** -- Add performance assertions to your CI pipeline. Search latency regressions are subtle and accumulate over time. A 500ms budget that fails the build prevents gradual degradation.\n\n7. **Test facets with realistic combinations** -- Users apply multiple filters simultaneously. Test the most common two-facet and three-facet combinations, not just individual facets in isolation.\n\n8. **Verify facet counts match actual results** -- A facet showing \"TypeScript (15)\" must produce exactly 15 results when clicked. Mismatched counts erode user trust in the search interface.\n\n9. **Test autocomplete debouncing** -- Autocomplete should debounce rapid keystrokes to avoid overwhelming the server. Verify that typing quickly does not produce stale or out-of-order suggestions.\n\n10. **Handle empty queries gracefully** -- Submitting an empty search should either show trending\u002Fpopular results or display a helpful message, never an error or completely blank page.\n\n11. **Test search URL state** -- Search queries, filters, sort order, and page number should be reflected in the URL. Users expect to share search URLs and use the browser back button to return to previous searches.\n\n12. **Validate accessibility of search components** -- The search input must have proper ARIA labels. Autocomplete dropdowns must support keyboard navigation with correct `aria-expanded`, `aria-activedescendant`, and `role=\"listbox\"` attributes.\n\n## Anti-Patterns to Avoid\n\n1. **Asserting exact result counts** -- Search indices change frequently. Asserting `expect(count).toBe(47)` will break whenever content is added or removed. Use range assertions like `toBeGreaterThan(10)` or `toBeLessThan(100)` instead.\n\n2. **Ignoring search debounce in tests** -- Failing to account for autocomplete debounce timing leads to tests that pass locally but fail in CI. Always wait for the debounce period or intercept the underlying API call rather than using fixed timeouts.\n\n3. **Testing relevance against production data** -- Production data changes constantly. A test that passes today will fail tomorrow when new content is indexed. Always seed a controlled dataset for relevance tests.\n\n4. **Hardcoding page sizes** -- If the application changes its default page size from 20 to 25, hardcoded assertions will break. Read the page size from configuration or infer it from the results.\n\n5. **Skipping zero-results scenarios** -- The zero-results page is often the most neglected UX surface. Users who see a blank dead end will leave. Always test what happens when no results match.\n\n6. **Testing only happy-path queries** -- Real users type misspelled words, paste URLs into search boxes, enter single characters, and submit empty forms. Test the full spectrum of realistic and adversarial inputs.\n\n7. **Ignoring search result snippet quality** -- Search results often show a snippet or excerpt. If the snippet does not contain the query terms, the result appears irrelevant even when it is not. Verify snippet content alongside title matching.\n\n## Debugging Tips\n\n- **Stale search index**: If relevance tests fail unexpectedly, check whether the search index has been rebuilt after recent data changes. Most search engines (Typesense, Elasticsearch, Algolia) have a reindexing delay.\n\n- **Autocomplete timing failures**: If autocomplete tests fail intermittently, increase the debounce wait or switch to intercepting the API response with `page.waitForResponse()` instead of using `waitForTimeout()`.\n\n- **Facet count mismatches**: When facet counts do not match actual results, check whether the search engine is using cached facet counts from a previous index state. Force a cache invalidation or reindex before running facet tests.\n\n- **Encoding issues in URL state**: If search queries with special characters break when loaded from URLs, verify that the application properly encodes and decodes query parameters using `encodeURIComponent` \u002F `decodeURIComponent`.\n\n- **Flaky relevance order**: If the same query sometimes returns results in different orders, check whether the search engine uses a tie-breaking strategy for equally scored results. Add a secondary sort by ID or date to ensure deterministic ordering.\n\n- **Performance test variance**: Search latency measurements can vary due to cold starts, garbage collection, and network conditions. Run performance tests multiple times and use the median rather than a single measurement. Consider warming up the search engine with a few queries before measuring.\n\n- **Highlighting breaks with special regex characters**: If highlight markup is missing for queries containing regex metacharacters (`.`, `*`, `+`, `?`), verify that the search engine escapes these characters before applying highlight patterns.\n\n- **Mobile autocomplete differences**: On mobile devices, the virtual keyboard may obscure the autocomplete dropdown. Test autocomplete visibility and interaction on mobile viewports using Playwright's device emulation.\n",{"name":5,"tags":24,"agents":32,"author":45,"domains":46,"license":15,"version":49,"languages":50,"frameworks":53,"description":7,"testingTypes":55},[25,26,27,28,29,30,31],"search-testing","search-relevance","autocomplete","faceted-search","fuzzy-search","search-ranking","zero-results",[33,34,35,36,37,38,39,40,41,42,43,44],"claude-code","cursor","github-copilot","windsurf","codex","aider","continue","cline","zed","bolt","gemini-cli","amp","Pramod",[47,48],"web","api","1.0.0",[51,52],"typescript","javascript",[54],"playwright",[56,57],"e2e","integration","main","eac438ea9216a7cb8a570cab803eb01eeafc6793","skills\u002Fqa\u002Fsearch-quality-tester","https:\u002F\u002Fgithub.com\u002FGrupoANDevelopment-m\u002Falberto-ai","https:\u002F\u002Fgithub.com\u002FGrupoANDevelopment-m","https:\u002F\u002Fid-preview--d5bef06d-6c9e-4563-a3d4-a855b9d61610.lovable.app\u002F","Eu sou Antonio B. B. Ndombe \r\nEu sou um Pesquisador e desenvolvedor de sistema autonomia cognitivos, fundador de sistemas ODCs ( organismos digital conscientes)","2026-11-04 06:53:39",{"source":8,"skillName":5,"commands":67,"prompt":77,"zipAvailable":78},[68,71,74],{"tool":69,"command":70},"npx","npx skills add GrupoANDevelopment-m\u002Falberto-ai --skill \"Search Quality Tester\"",{"tool":72,"command":73},"bunx","bunx skills add GrupoANDevelopment-m\u002Falberto-ai --skill \"Search Quality Tester\"",{"tool":75,"command":76},"pnpm","pnpm dlx skills add GrupoANDevelopment-m\u002Falberto-ai --skill \"Search Quality Tester\"","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FGrupoANDevelopment-m\u002Falberto-ai\u002Fblob\u002Fmain\u002Fskills\u002Fqa\u002Fsearch-quality-tester\u002FSKILL.md",true,[],[81,89,96,103,110,117],{"skillNo":82,"name":83,"nameZh":6,"description":84,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":85,"licenseFileUrl":6,"sourceHtmlUrl":86,"categories":87,"keywords":88,"publishedAt":19},"01M7DASC6FSCJP49JZBSK0NY1Z","nemoclaw-user-reference","Describes the NemoClaw integration layer and blueprint architecture and how they orchestrate compatible agent sandboxes. Use when looking up architecture, agent integration, plugin structure, or blueprint design. Trigger keywords - nemoclaw architecture, nemoclaw agent architecture, nemoclaw plugin blueprint structure, nemoclaw vs openshell, which cli, nemoclaw cli, openshell cli, sandbox commands, nemoclaw cli commands, nemoclaw command reference, nemoclaw network policy, sandbox egress control operator approval, nemoclaw troubleshooting, nemoclaw debug sandbox issues.","Apache-2.0","https:\u002F\u002Fgithub.com\u002FGrupoANDevelopment-m\u002Falberto-ai\u002Fblob\u002Fmain\u002Fupstream\u002Fnemoclaw\u002Fskills\u002Fnemoclaw-user-reference\u002FSKILL.md",[],[],{"skillNo":90,"name":91,"nameZh":6,"description":92,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":85,"licenseFileUrl":6,"sourceHtmlUrl":93,"categories":94,"keywords":95,"publishedAt":19},"01M7DAS5S79VK33XFGVAM2TFWF","nemoclaw-user-overview","Explains how OpenClaw, OpenShell, and NemoClaw form the ecosystem, NemoClaw's position in the stack, what NemoClaw adds beyond the community sandbox, and when to prefer NemoClaw versus integrating OpenShell and OpenClaw directly. Use when users ask about the relationship between OpenClaw, OpenShell, and NemoClaw, or when to use NemoClaw versus OpenShell. Trigger keywords - nemoclaw ecosystem, openclaw openshell, nemoclaw vs openshell, sandboxed openclaw, how nemoclaw works, nemoclaw sandbox lifecycle blueprint, nemoclaw overview, openclaw always-on assistants, nvidia openshell, nvidia nemotron, nemoclaw release notes, nemoclaw changelog.","https:\u002F\u002Fgithub.com\u002FGrupoANDevelopment-m\u002Falberto-ai\u002Fblob\u002Fmain\u002Fupstream\u002Fnemoclaw\u002Fskills\u002Fnemoclaw-user-overview\u002FSKILL.md",[],[],{"skillNo":97,"name":98,"nameZh":6,"description":99,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":85,"licenseFileUrl":6,"sourceHtmlUrl":100,"categories":101,"keywords":102,"publishedAt":19},"01M7DAS0PQEMET5MNGZ1ABSZVS","nemoclaw-user-monitor-sandbox","Inspects sandbox health, traces agent behavior, and diagnoses problems. Use when monitoring a running sandbox, debugging agent issues, or checking sandbox logs. Trigger keywords - monitor nemoclaw sandbox, debug nemoclaw agent issues.","https:\u002F\u002Fgithub.com\u002FGrupoANDevelopment-m\u002Falberto-ai\u002Fblob\u002Fmain\u002Fupstream\u002Fnemoclaw\u002Fskills\u002Fnemoclaw-user-monitor-sandbox\u002FSKILL.md",[],[],{"skillNo":104,"name":105,"nameZh":6,"description":106,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":85,"licenseFileUrl":6,"sourceHtmlUrl":107,"categories":108,"keywords":109,"publishedAt":19},"01M7DARQYGF7E9A69VH6S3CGN9","nemoclaw-user-manage-sandboxes","Explains operational tasks after the quickstart: listing sandboxes, status and health checks, logs, diagnostics, port forwards, multiple sandboxes, credential reset, rebuilds, network presets, upgrades, and uninstall. Trigger keywords - manage nemoclaw sandboxes, nemoclaw status, nemoclaw list, nemoclaw dashboard port, nemoclaw rebuild, nemoclaw upgrade sandboxes, nemoclaw uninstall, sandbox mutability, sandbox runtime configuration, sandbox rebuild, nemoclaw backup, nemoclaw restore, workspace backup, openshell sandbox download upload, nemoclaw messaging channels, nemoclaw telegram, nemoclaw discord, nemoclaw slack, nemoclaw wechat, nemoclaw whatsapp, openshell channel messaging, nemoclaw workspace files, soul.md, user.md, identity.md, agents.md, sandbox persistence.","https:\u002F\u002Fgithub.com\u002FGrupoANDevelopment-m\u002Falberto-ai\u002Fblob\u002Fmain\u002Fupstream\u002Fnemoclaw\u002Fskills\u002Fnemoclaw-user-manage-sandboxes\u002FSKILL.md",[],[],{"skillNo":111,"name":112,"nameZh":6,"description":113,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":85,"licenseFileUrl":6,"sourceHtmlUrl":114,"categories":115,"keywords":116,"publishedAt":19},"01M7DARH8BRTG51TRCAB7YWDJ4","nemoclaw-user-manage-policy","Adds, removes, or modifies allowed endpoints in the sandbox policy. Use when customizing network policy, changing egress rules, or configuring sandbox endpoint access. Trigger keywords - customize nemoclaw network policy, sandbox egress policy configuration, nemoclaw integration policy examples, post-install policy setup, openshell approval workflow, policy preset, nemoclaw approve network requests, sandbox egress approval tui.","https:\u002F\u002Fgithub.com\u002FGrupoANDevelopment-m\u002Falberto-ai\u002Fblob\u002Fmain\u002Fupstream\u002Fnemoclaw\u002Fskills\u002Fnemoclaw-user-manage-policy\u002FSKILL.md",[],[],{"skillNo":118,"name":119,"nameZh":6,"description":120,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":85,"licenseFileUrl":6,"sourceHtmlUrl":121,"categories":122,"keywords":123,"publishedAt":19},"01M7DARAT62M2VR37590VCZ5NS","nemoclaw-user-get-started","Installs NemoClaw, launches a sandbox, and runs the first agent prompt. Use when onboarding, installing, or launching a NemoClaw sandbox for the first time. Trigger keywords - nemoclaw quickstart, install nemoclaw openclaw sandbox, nemohermes quickstart, hermes agent nemoclaw, run hermes openshell sandbox, nemoclaw prerequisites, nemoclaw supported platforms, nemoclaw hardware software, nemoclaw windows wsl2 setup, nemoclaw install windows docker desktop.","https:\u002F\u002Fgithub.com\u002FGrupoANDevelopment-m\u002Falberto-ai\u002Fblob\u002Fmain\u002Fupstream\u002Fnemoclaw\u002Fskills\u002Fnemoclaw-user-get-started\u002FSKILL.md",[],[]]