Skip to main content

How to Test Search, Filters, and Faceted Navigation

Short answer

Search and faceted UIs fail on URL desync, stale counts, and combinatorial filter bugs—not missing search boxes. Seed a catalog with known tags per dimension, assert query params and probe result counts, and prioritize filter combinations by prod usage via TrueCoverage rather than exhaustive Cartesian products.

Part of Testing Guides by UI patterns.

Who this is for

Teams building catalog, admin, analytics, or marketplace UIs with search boxes, facet chips, sort dropdowns, and sharable filtered URLs. Typical stacks: Algolia/Elastic frontends, REST list APIs with query strings, React Query + URL state libraries (nuqs, React Router search params).

Why testing search and filters matters

Filter bugs hide revenue and break trust:

  • Hidden inventory — wrong facet logic excludes in-stock products; marketing campaigns point to empty result pages.
  • Broken deep links — shared URLs drop filters on refresh; sales teams cannot reproduce customer views.
  • Wrong analytics — admin filters export incorrect subsets; compliance reports omit regulated transactions.
  • Performance incidents — unbounded filter combos trigger slow queries only in prod with real catalog size.
  • Accessibility gaps — facet counts update without live region announcements; screen reader users apply filters blindly.

Tests must assert URL state, result cardinality, and probe/API agreement—not that a checkbox toggles.

Complexity map

ScenarioEdge caseWhy tests breakApproach
URL syncRefresh drops filtersShared links brokenAssert page.url() query params
Zero resultsEmpty state vs errorFlaky skeleton UIProbe total=0 + empty state role
Multi-facet ANDOver-restrictive logicFalse emptySeed item matching all facets
Multi-facet ORUnder-restrictive logicToo many rowsProbe count decreases monotonically
Sort + filterSort resets filterUser confusionApply both; assert params persist
Pagination + filterPage 2 invalid after filter404 or duplicate rowsReset page index on filter change
Full-text + facetSearch narrows wrong setIndex lagProbe IDs in result set
Clear allPartial clearStale chipsAssert all params removed
Debounced searchTypeahead racesWrong final querypressSequentially + wait for response
Mobile drawer filtersDesktop-only selectorsUntested pathSeparate viewport spec or component probe
Saved searchesPersist user prefsCross-user leakProbe scoped to user id
Special characters& in queryBroken URLEncode and assert round-trip

Seed catalog pattern

Arrange a deterministic catalog via test seed route—never rely on prod-like random data:

// POST /api/test/seed-catalog
// Body: { runId, items: [{ id, tags: { color: 'red', size: 'M' }, price: 100 }] }
await request.post('/api/test/seed-catalog', {
data: { runId, items: catalogFixtures },
});

Each fixture item should be tagged so one unique combination exists for your highest-priority facet pair (e.g., red + size M → exactly 3 SKUs).

URL and probe Assert pattern

await page.getByRole('checkbox', { name: 'Red' }).check();
await page.getByRole('checkbox', { name: 'Medium' }).check();
await expect(page).toHaveURL(/color=red/);
await expect(page).toHaveURL(/size=M/);

const probe = await request.get(`/api/test/catalog-count?runId=${runId}&color=red&size=M`);
expect(await probe.json()).toEqual({ total: 3, ids: expect.arrayContaining(['sku-1']) });

UI list length is optional when virtual scroll hides rows—probe is authoritative.

Prioritizing filter combinations

Combinations explode (2^n facets). Use TrueCoverage dimensions:

  • filter_combo — normalized facet key (e.g., color:red|size:M)
  • sort_orderprice_asc, newest, relevance

Compare prod search/filter event distributions to test runs. When prod shows filter_combo=brand:x|category:y at 18% traffic but tests never exercise it, run /testchimp evolve to add a SmartTest—link with // @Scenario: in markdown plans (requirement traceability).

Zero results and error states

await page.getByLabel('Search').fill('zzzz-no-match-' + runId);
await page.keyboard.press('Enter');
await expect(page.getByRole('status')).toContainText(/no results/i);
const probe = await request.get(`/api/test/catalog-count?q=zzzz-no-match-${runId}`);
expect((await probe.json()).total).toBe(0);

Distinguish empty catalog (seed failure) from valid zero results via probe.

const responsePromise = page.waitForResponse(r =>
r.url().includes('/api/search') && r.status() === 200
);
await page.getByLabel('Search').pressSequentially('desk', { delay: 50 });
await responsePromise;
await expect(page.getByTestId('result-row')).toHaveCount(await probeCount('desk'));

Avoid typing with fill() when debounce listens to individual keystrokes.

CI checklist

  1. Seed catalog per worker with unique runId prefix on SKUs
  2. Assert URL params after every filter mutation
  3. Probe result counts for at least top 3 prod filter_combo slices
  4. Test clear-all and browser back/forward if SPA uses history API
  5. Do not assert exact product card copy—assert IDs or probe list
  6. Run ExploreChimp on mobile filter drawer paths for layout regressions

Anti-patterns

Anti-patternWhy it failsBetter approach
Test every facet permutationUnmaintainable suiteTrueCoverage-prioritized combos
Assert visible row count onlyVirtual scroll hides rowsProbe API count
Ignore URL paramsRefresh breaks sharingAssert query string
Shared catalog in parallel CICross-test pollutionPer-run seed
waitForTimeout after filterRace with networkwaitForResponse or poll probe
Skip zero-results pathSEO pages show errorsExplicit empty state spec

Example scenario

Situation: User applies color=red and size=M, copies URL, opens in new tab.

Expected outcome: Same three products; filters pre-selected; probe count matches.

Why UI-only automation breaks: Chips show red+M but API returns unfiltered list—new tab shows all items.

  1. Arrange: Seed catalog where only three SKUs match red+M for runId.
  2. Act: Apply filters, copy URL, open in fresh context without cookies.
  3. Assert: Probe count=3; URL params intact; facet checkboxes checked; result IDs match probe.

TestChimp workflow: Instrument filter_apply events with filter_combo and sort_order; expand tests when prod combo exceeds test coverage.

Same Arrange/Act/Assert pattern as expired-coupon checkout.

Connect scenarios to your QA workflow

Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).

External references

Frequently asked questions

Which filter combinations should I test first?

Compare prod vs test-run across filter_combo and sort_order in TrueCoverage. Prioritize combos with high prod traffic—not every Cartesian product. Run /testchimp evolve when gaps appear.

How do I test filters with virtual scrolling?

Do not count DOM rows. Probe API total and assert a known result ID appears via search-in-grid helper or scroll-into-view only for spot checks.

Should filtered state live in the URL?

If your product supports shareable searches, E2E must assert query params survive refresh and new-tab open. Probe should accept same params as UI.

How do I avoid flaky debounced search tests?

Use pressSequentially with waitForResponse on the search API, or poll probe until count stabilizes. Avoid fixed timeouts.

How do saved searches differ from URL filters?

Saved searches persist server-side per user—probe that another user cannot load saved filter IDs; URL tests alone miss ACL bugs.

Can ExploreChimp help faceted navigation?

Yes—run exploratory paths on mobile filter drawers and RTL locales where chips overflow; convert UX breaks to SmartTests with seeded catalogs.

How do I test zero results vs API errors?

Seed catalog known to miss query; probe total=0. Separate spec stubs 500 on search API and asserts error banner—not empty state.

Apply these patterns in your repo

Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.

Start free on TestChimp · Book a demo