Search UI Testing Guide

How to Test Meilisearch UI with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing Meilisearch search interfaces with Playwright. Typo tolerance, faceted filtering, sortable attributes, distinct attribute deduplication, pagination versus infinite scroll hits, and the pitfalls that break real search test suites in production.

100B+

Meilisearch has indexed over 100 billion documents across its cloud and open-source deployments, powering sub-50ms search for tens of thousands of production applications.

Meilisearch 2025 Community Report

0msTypical search latency
0Search scenarios covered
0Typo tolerance levels
0%Fewer lines with Assrt

Meilisearch Search Request Flow

BrowserSearch UIMeilisearch ClientMeilisearch ServerIndexUser types queryDebounced search callPOST /indexes/{uid}/searchQuery index with filtersRanked hits + facetsJSON response (hits, facetDistribution)Update component stateRender search results

1. Why Testing Meilisearch UI Is Harder Than It Looks

Meilisearch is a fast, typo-tolerant search engine that returns results in under 50 milliseconds. On the surface, testing a search UI sounds trivial: type a query, check the results. In practice, five structural characteristics of Meilisearch make reliable UI testing significantly more complex than a simple input/output assertion.

First, typo tolerance is enabled by default and adapts based on word length. A one-character word tolerates zero typos, a word with five or more characters tolerates up to two typos, and words in between tolerate one. This means a search for “helo” returns results for “hello,” but a search for “he” does not. Your tests must account for these thresholds or they will pass locally and fail unpredictably when someone adds shorter product names to the index.

Second, Meilisearch's filter syntax uses a specific grammar that differs from SQL and from other search engines. Filters look like genre = "sci-fi" AND rating > 4 but require every filterable attribute to be explicitly declared in filterableAttributes before the index accepts it. A filter on an undeclared attribute returns zero results without any error message in the UI, which is a common source of silent test failures.

Third, sortable attributes must also be pre-declared. Sorting by a field that is not in sortableAttributes returns a 400 error from the API, but most search UI libraries catch that error silently and show an empty result set. Fourth, the distinct attribute collapses duplicate entries (for example, multiple variants of the same product) into a single hit, which changes your expected result count in ways that are difficult to predict without understanding the index configuration. Fifth, Meilisearch offers both offset/limit pagination and a newer finite pagination mode, and they behave differently when combined with filters and sorting.

Meilisearch Index Configuration Flow

⚙️

Create Index

POST /indexes

🔒

Set Settings

filterableAttributes, sortableAttributes

⚙️

Add Documents

POST /indexes/{uid}/documents

Wait for Task

GET /tasks/{taskUid}

Index Ready

Status: succeeded

Typo Tolerance Decision Tree

🌐

Word Length

Count characters

🔒

1-4 chars

0 or 1 typo allowed

5+ chars

Up to 2 typos allowed

⚙️

Ranking

Exact > 1 typo > 2 typos

Results

Hits sorted by relevance

A good Meilisearch UI test suite covers all five of these surfaces. The sections below walk through each scenario you need, with runnable Playwright TypeScript code you can paste directly into your project.

2. Setting Up a Reproducible Test Environment

Meilisearch tests are only deterministic when you control the index state completely. That means seeding a known dataset before each test run, configuring settings (filterable, sortable, distinct attributes) explicitly, and waiting for Meilisearch to finish processing before running any search assertions. Skipping any of these steps leads to flaky tests.

Meilisearch Test Environment Setup Checklist

  • Install Meilisearch locally or use Docker (docker run -p 7700:7700 getmeili/meilisearch:latest)
  • Set a master key for API authentication (MEILI_MASTER_KEY)
  • Create a dedicated test index separate from development data
  • Declare filterableAttributes before adding documents
  • Declare sortableAttributes before adding documents
  • Configure the distinct attribute if testing deduplication
  • Seed a deterministic dataset with known values for assertions
  • Wait for all indexing tasks to complete before running tests

Docker Setup and Environment Variables

Start Meilisearch with Docker
.env.test

Index Seeding Script

Every test run should start with a clean, fully indexed dataset. The seeding script deletes the existing index, creates a fresh one with all required settings, adds documents, and waits for the indexing task to complete. This guarantees deterministic results regardless of what previous test runs left behind.

test/helpers/seed-meilisearch.ts

Playwright Configuration

playwright.config.ts
test/global-setup.ts

4. Scenario: Typo Tolerance Verification

Meilisearch's typo tolerance is one of its most valuable features, but it is also one of the hardest to test correctly. The engine uses a distance-based algorithm that allows a configurable number of character substitutions, insertions, or deletions based on word length. By default, words with fewer than 5 characters allow one typo, and words with 8 or more characters allow two typos. These thresholds are configurable via the typoTolerance.minWordSizeForTypos setting.

The tricky part for testing is that typo tolerance interacts with ranking rules. An exact match always ranks higher than a one-typo match, which ranks higher than a two-typo match. Your test must verify not just that results appear, but that they appear in the correct relevance order. A search for “headphonas” (one typo in an 10-character word) should return “Headphones” results, but a search for “hep” (attempting “help” with one typo in a 3-character input) may return nothing because the word is too short for typo tolerance to activate.

2

Typo Tolerance Verification

Moderate

Goal

Verify that Meilisearch returns relevant results when the user makes common typos, and that results are ranked with exact matches above typo matches.

Playwright Implementation

typo-tolerance.spec.ts

Typo Tolerance: Playwright vs Assrt

test('single typo returns correct results', async ({ page }) => {
  await page.goto('/search');
  const searchInput = page.getByRole('searchbox');
  await searchInput.fill('headphonas');
  await page.waitForTimeout(500);

  const results = page.locator('[data-testid="search-hit"]');
  await expect(results.first()).toBeVisible();
  await expect(results.first()).toContainText(/headphones/i);
});
28% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

5. Scenario: Faceted Filter Syntax and Combination

Meilisearch supports faceted filtering through a custom filter syntax that looks like SQL but has its own rules. Filters use operators like =, !=, >, <, TO (for ranges), and IN (for multiple values). You can combine them with AND, OR, and NOT. The critical prerequisite is that every attribute you want to filter on must be declared in filterableAttributes before you index documents.

In the UI, faceted filters typically appear as checkboxes, dropdown menus, range sliders, or clickable category badges. The challenge for testing is verifying that the UI correctly translates user interactions into Meilisearch filter syntax, and that the results accurately reflect the applied filters. A common bug is the UI sending category: Electronics (colon syntax from other search engines) instead of category = "Electronics" (Meilisearch syntax), which silently returns zero results.

3

Faceted Filter Application

Moderate

Goal

Apply single and combined faceted filters through the UI and verify the result set shrinks correctly, the facet counts update, and removing a filter restores the original results.

Playwright Implementation

faceted-filters.spec.ts
Common Filter Syntax Error

6. Scenario: Sortable Attributes and Ranking

Meilisearch separates its relevance ranking from explicit sorting. By default, results are ranked by a combination of word matching, typo proximity, attribute position, and exactness. When you apply a sort (for example, “price ascending”), Meilisearch overrides the relevance ranking with the specified sort order. The key caveat is that only attributes listed in sortableAttributes can be sorted. Attempting to sort by an undeclared attribute returns an API error, which most search UI libraries catch silently and display as an empty or unsorted result set.

Testing sort behavior requires verifying three things: that the sort dropdown or button exists and is functional, that the results are actually reordered according to the selected criterion, and that switching between sort options updates the results correctly. A subtle bug to watch for is sort stability: when two documents have the same sort value (for example, two products at $79.99), Meilisearch falls back to the default ranking rules for the tiebreaker, but the UI may display them in an inconsistent order across requests.

4

Sort by Price and Rating

Moderate

Goal

Toggle between sort options (price ascending, price descending, rating descending) and verify that the result order changes correctly.

Playwright Implementation

sortable-attributes.spec.ts

Sort Verification: Playwright vs Assrt

test('sort by price ascending', async ({ page }) => {
  await page.goto('/search');
  await page.getByRole('searchbox').fill('');
  await page.waitForTimeout(500);

  const sortSelect = page.getByLabel('Sort by');
  await sortSelect.selectOption('price:asc');
  await page.waitForTimeout(500);

  const priceElements = page.locator('[data-testid="hit-price"]');
  const count = await priceElements.count();
  const prices: number[] = [];
  for (let i = 0; i < count; i++) {
    const text = await priceElements.nth(i).textContent();
    prices.push(parseFloat(text?.replace('$', '') || '0'));
  }
  for (let i = 1; i < prices.length; i++) {
    expect(prices[i]).toBeGreaterThanOrEqual(prices[i - 1]);
  }
});
42% fewer lines

7. Scenario: Distinct Attribute Deduplication

The distinctAttribute setting in Meilisearch collapses multiple documents that share the same value for a given field into a single result. A common use case is product variants: if you have five color variants of the same shoe, each as a separate document, setting distinctAttribute: "sku" or distinctAttribute: "brand" ensures only one variant appears per group.

This feature is deceptive to test because it changes the result count in ways that are not obvious from the query alone. A search for “wireless” in our test dataset would normally return four SoundMax products, but with distinctAttribute: "brand" set, only one SoundMax product appears (the most relevant one). Your test must know the distinct attribute configuration to predict the correct result count. If the index configuration changes and the distinct attribute is removed, your tests will suddenly see more results and fail. Conversely, if someone adds a distinct attribute you did not account for, tests expecting a certain number of results will see fewer and break.

5

Distinct Attribute Deduplication

Complex

Goal

Verify that the distinct attribute correctly deduplicates results, showing only one hit per distinct value, and that the most relevant document from each group is the one displayed.

Playwright Implementation

distinct-attribute.spec.ts

8. Scenario: Pagination vs Infinite Hits

Meilisearch offers two pagination strategies, and they work differently under the hood. The default uses offset and limit parameters for infinite-scroll-style loading. The alternative uses page and hitsPerPage for traditional numbered pagination. The critical difference is that the offset/limit approach does not return a totalPages or totalHits count by default (these appear as estimatedTotalHits), while the page/hitsPerPage approach returns exact totalHits and totalPages.

This distinction matters for testing because your assertions about “how many total results exist” depend on which pagination mode your UI uses. The maxTotalHitssetting in the index's pagination configuration caps the number of results Meilisearch will return across all pages. If you set maxTotalHits: 100 but your index has 500 matching documents, the API only returns the first 100 regardless of how many pages you request.

6

Pagination and Infinite Scroll

Complex

Goal

Verify that traditional page-based pagination navigates correctly, that infinite scroll loads additional results, and that the total hit count is accurate.

Playwright Implementation (Traditional Pagination)

pagination.spec.ts

Playwright Implementation (Infinite Scroll)

infinite-scroll.spec.ts
Pagination Mode Differences

Pagination: Playwright vs Assrt

test('page navigation shows correct results', async ({ page }) => {
  await page.goto('/search');
  await page.getByRole('searchbox').fill('');
  await page.waitForTimeout(500);

  const results = page.locator('[data-testid="search-hit"]');
  const page1First = await results.first().textContent();

  const nextBtn = page.getByRole('button', { name: /next/i });
  if (await nextBtn.isVisible()) {
    await nextBtn.click();
    await page.waitForTimeout(500);
    const page2First = await results.first().textContent();
    expect(page2First).not.toBe(page1First);

    await page.getByRole('button', { name: /prev/i }).click();
    await page.waitForTimeout(500);
    const restored = await results.first().textContent();
    expect(restored).toBe(page1First);
  }
});
43% fewer lines

9. Common Pitfalls That Break Meilisearch Test Suites

After reviewing dozens of Meilisearch GitHub issues and community forum threads, here are the pitfalls that most frequently break real search test suites. Each one is sourced from actual production failures.

Meilisearch Testing Anti-Patterns

  • Not waiting for indexing tasks to complete before searching. Meilisearch processes documents asynchronously, so adding documents and immediately searching may return stale or empty results. Always call client.waitForTask(taskUid) after addDocuments or updateSettings.
  • Forgetting to declare filterableAttributes before applying filters. An undeclared filter attribute returns an API error, but many UI libraries catch this silently and show zero results. Your test passes with zero results and you think the filter works.
  • Using the wrong pagination mode assertions. Asserting on totalHits when using offset/limit mode (which only provides estimatedTotalHits), or expecting estimatedTotalHits in page/hitsPerPage mode (which provides exact totalHits). Mixing these causes intermittent assertion failures.
  • Ignoring typo tolerance thresholds in assertions. Asserting that a 3-character typo query matches a 3-character word when minWordSizeForTypos.oneTypo is set to 4. The test works in development (where typo settings may differ) and fails in CI.
  • Not accounting for the distinct attribute in result count assertions. Expecting 5 results for a query that matches 5 documents, but the distinct attribute collapses them to 3. The count changes silently when someone modifies the index settings.
  • Hardcoding debounce timing instead of waiting for network idle. Using waitForTimeout(200) to account for debounce, but the actual debounce time varies between search UI libraries (InstantSearch uses 200ms, custom implementations vary). Use waitForResponse or a result-visible assertion instead.
  • Testing against a shared Meilisearch instance. Multiple test suites or developers using the same index causes flaky tests because one suite's seed data overwrites another's. Use Docker containers with unique ports per test run.
  • Not resetting the index between test files. Meilisearch settings persist across requests. A test that changes sortableAttributes in one file affects all subsequent files. Reset settings in beforeAll or use separate indexes per test file.

The Indexing Race Condition

The single most common cause of flaky Meilisearch tests is the indexing race condition. Unlike synchronous databases, Meilisearch processes document additions and setting updates as asynchronous tasks. When you call index.addDocuments(), it returns immediately with a task ID while the actual indexing happens in the background. If your test starts searching before the task completes, it will see stale or empty results.

test/helpers/wait-for-index.ts

Network Request Interception for Debugging

When a Meilisearch test fails, intercepting the network request reveals whether the problem is in the UI (sending the wrong query) or the backend (returning unexpected data). Playwright's route interception is invaluable for diagnosing these failures.

debug-search-requests.spec.ts

10. Writing These Scenarios in Plain English with Assrt

The Playwright scenarios above are thorough but verbose. Each one requires understanding Playwright's locator API, managing timeouts and waits, parsing text content for assertions, and handling the specific behavior of your search UI library. Assrt lets you express the same scenarios in plain English, and it compiles them into the equivalent Playwright TypeScript code.

Here is the faceted filter scenario (Section 5) rewritten as an Assrt file. The scenario file describes the intent, not the implementation. Assrt figures out the selectors, waits, and assertions based on your actual running application.

scenarios/meilisearch-faceted-filters.assrt

Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real tests you can read, run, and modify. When Meilisearch updates its API, when your search UI library renames components, or when someone changes the index configuration, Assrt detects the failure, analyzes the new DOM, and opens a pull request with the updated locators. Your scenario files stay untouched.

Start with the basic search smoke test. Once it is green in your CI, add the typo tolerance scenario, then the faceted filter tests, then sort verification, then distinct attribute deduplication, then pagination. In a single afternoon you can have complete Meilisearch search UI coverage that most production applications never manage to achieve by hand.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk