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.
“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
Meilisearch Search Request Flow
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
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.
Playwright Configuration
3. Scenario: Basic Search and Result Rendering
The first scenario every Meilisearch integration needs is a basic search query that returns results and renders them correctly in the UI. This is your smoke test. If this breaks, nobody can search your application and you want to know immediately. The flow is straightforward: the user types a query in the search input, the UI debounces the input (typically 200 to 300 milliseconds), sends a request to Meilisearch, receives the response, and renders the hits.
Basic Search and Result Rendering
StraightforwardGoal
Type a known query into the search input, confirm that results appear within a reasonable time, and verify the rendered hit count and content match the expected data.
Preconditions
- App running at
APP_BASE_URL - Meilisearch index seeded with the test dataset
- Search UI component connected to the test index
Playwright Implementation
What to Assert Beyond the UI
Basic Search Assertions
- Results appear within 500ms of typing (Meilisearch p99 is under 50ms, debounce adds 200-300ms)
- Hit count in the UI matches the number of rendered result cards
- Each result card displays title, price, and category from the document
- Empty query shows no results or a default state, not an error
- Search input retains the query text after results render
Basic Search: Playwright vs Assrt
import { test, expect } from '@playwright/test';
test('basic search returns matching results', async ({ page }) => {
await page.goto('/search');
const searchInput = page.getByRole('searchbox');
await searchInput.fill('headphones');
await page.waitForTimeout(500);
const results = page.locator('[data-testid="search-hit"]');
await expect(results.first()).toBeVisible();
const hitCount = await results.count();
expect(hitCount).toBeGreaterThanOrEqual(2);
await expect(results.first()).toContainText(/headphones/i);
const hitCounter = page.getByTestId('hit-count');
await expect(hitCounter).toContainText(/\d+ result/i);
});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.
Typo Tolerance Verification
ModerateGoal
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: 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);
});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.
Faceted Filter Application
ModerateGoal
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
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.
Sort by Price and Rating
ModerateGoal
Toggle between sort options (price ascending, price descending, rating descending) and verify that the result order changes correctly.
Playwright Implementation
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]);
}
});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.
Distinct Attribute Deduplication
ComplexGoal
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
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.
Pagination and Infinite Scroll
ComplexGoal
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)
Playwright Implementation (Infinite Scroll)
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);
}
});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.
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.
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.
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.