Чек-лист тестирования поиска и фильтров — и как автоматизировать каждый пункт на Playwright
Поиск и фильтры есть почти в каждом продукте — и баги в них одни и те же из проекта в проект. Этот пост в двух частях: сначала чек-лист того, что надо проверять, а потом — как автоматизировать каждый пункт на Playwright. Все примеры на TypeScript, но идея переносится на любой UI-фреймворк.
Ключевой инструмент: перехват сети
Половину интересных кейсов нельзя стабильно воспроизвести без контроля над сетью: медленный ответ, ошибка бэкенда, пустая выдача, гонка запросов. Поэтому основа автотестов поиска — page.route(): перехватываем запрос к API и сами решаем, что и когда вернуть. Это делает тесты детерминированными. См. Network и Mock APIs в доках Playwright.
1. Запрос: граничные случаи
Чек-лист: пустой запрос и только пробелы; 1 символ; очень длинный; спецсимволы и операторы (" * - :); unicode/emoji; ведущие/хвостовые пробелы (trim); регистр.
Автотест — параметризуем «злые» запросы, проверяем, что страница не падает и не отдаёт 500:
const queries = ['', ' ', 'a', 'й'.repeat(500),
'"точная фраза"', 'café', '🚀'];
for (const q of queries) {
test(`запрос не роняет страницу: ${JSON.stringify(q)}`,
async ({ page }) => {
await page.goto(`/search?q=${encodeURIComponent(q)}`);
await expect(page.getByRole('searchbox')).toBeVisible();
// нет краша и экрана ошибки 500
});
}
2. Debounce: один запрос вместо шквала
Чек-лист: ввод не шлёт запрос на каждую букву — есть debounce/throttle (обычно 200–400 мс).
Автотест — считаем реальные обращения к API через перехват:
test('поиск дебаунсится', async ({ page }) => {
const calls: string[] = [];
await page.route('**/api/search**', route => {
calls.push(route.request().url());
route.fulfill({ json: { results: [] } });
});
await page.goto('/search');
await page.getByRole('searchbox')
.pressSequentially('iphone', { delay: 50 });
await page.waitForTimeout(600); // дольше окна дебаунса
expect(calls.length).toBe(1); // не 6 запросов на 6 букв
});
3. Гонка запросов: показан ответ на последний ввод
Чек-лист: если медленный ответ на старый запрос приходит позже быстрого на новый — на экране должен остаться результат последнего ввода, а не перетёртый старым. Классический баг живых поисков.
Автотест — искусственно тормозим старый запрос:
test('гонка запросов: побеждает последний', async ({ page }) => {
await page.route('**/api/search**', async route => {
const q = new URL(route.request().url())
.searchParams.get('q');
if (q === 'ab') await new Promise(r => setTimeout(r, 1000));
await route.fulfill({ json: { results: [{ title: q }] } });
});
await page.goto('/search');
const box = page.getByRole('searchbox');
await box.fill('ab'); // улетел медленный
await box.fill('abc'); // улетел быстрый
await expect(page.getByTestId('results'))
.toContainText('abc'); // показан результат abc
});
4. Пустое состояние, а не пустой список
Чек-лист: при нуле результатов — внятный empty state с подсказкой («ничего не найдено», предложение сбросить фильтры), а не белая пустота и не «крутилка» навсегда.
test('нет результатов: показан empty state', async ({ page }) => {
await page.route('**/api/search**',
r => r.fulfill({ json: { results: [] } }));
await page.goto('/search?q=zzxqwer');
await expect(page.getByTestId('empty-state')).toBeVisible();
await expect(page.getByText(/ничего не найдено/i))
.toBeVisible();
});
5. Инъекции и XSS в запросе
Чек-лист: спецсимволы и теги в запросе экранируются при отображении (эхо запроса, «вы искали…»); ' OR 1=1 и <script> не ломают выдачу и не исполняются. См. OWASP XSS.
test('спецсимволы экранируются', async ({ page }) => {
const xss = '<img src=x onerror=alert(1)>';
let fired = false;
page.on('dialog', () => { fired = true; });
await page.goto(`/search?q=${encodeURIComponent(xss)}`);
// эхо запроса — текстом, не HTML
await expect(page.getByTestId('query-echo')).toHaveText(xss);
expect(fired).toBe(false); // alert не сработал
});
6. Фильтры и фасеты + состояние в URL
Чек-лист: комбинация фильтров (AND/OR) корректна; «сбросить всё» работает; узкий фильтр → внятное «ничего не найдено»; состояние фильтров живёт в URL и переживает reload и кнопки назад/вперёд (шаринг ссылки даёт ту же выдачу).
test('фильтры в URL переживают reload и назад',
async ({ page }) => {
await page.goto('/search?q=phone');
await page.getByRole('checkbox', { name: 'В наличии' })
.check();
await expect(page).toHaveURL(/in_stock=1/);
await page.reload();
await expect(
page.getByRole('checkbox', { name: 'В наличии' })
).toBeChecked(); // состояние восстановлено
await page.goBack();
await expect(page).not.toHaveURL(/in_stock=1/);
});
7. Устойчивость: таймаут и ошибка бэкенда
Чек-лист: 500/таймаут/обрыв сети дают внятное состояние ошибки с «повторить», а не белый экран и не вечную крутилку; повтор работает.
test('ошибка бэкенда: внятное состояние', async ({ page }) => {
await page.route('**/api/search**',
r => r.fulfill({ status: 500 }));
await page.goto('/search?q=phone');
await expect(page.getByRole('alert'))
.toContainText(/повторить|что-то пошло не так/i);
});
Здесь же удобно гонять медленную сеть: задержать ответ через setTimeout в обработчике route и проверить, что показан лоадер, а не зависание. Подробнее про подмену ответов — class Route, про матчеры — assertions.
Что НЕ стоит автоматизировать в e2e
- Качество ранжирования. «Точное совпадение выше частичного», релевантность — субъективно и хрупко в UI-тестах. Лучше golden-set на уровне поискового бэка/юнитов, а не Playwright.
- Морфологию и синонимы. Стемминг, окончания, опечатки — отдельные тесты поискового движка, не e2e.
- Исследовательское. Реальная «находимость» и неожиданные формулировки запросов — ручками. Автотест проверяет механику, не смысл.
Полный чек-лист
Запрос:
- Пустой / только пробелы / 1 символ / очень длинный — не падает
- Trim ведущих и хвостовых пробелов
- Спецсимволы и операторы (
" * - :), unicode, emoji - Регистронезависимость (или явно задокументировано иначе)
Поведение ввода:
- Debounce/throttle — не запрос на каждую букву
- Гонка запросов — показан результат последнего ввода
- Автодополнение / недавние запросы / очистка поля
Результаты:
- No results → внятный empty state с подсказкой
- Пагинация / бесконечная прокрутка без дублей и пропусков
- Подсветка совпадений, счётчик найденного
Фильтры:
- Комбинация фильтров (AND/OR) корректна
- Сброс всех фильтров
- Состояние в URL: reload, назад/вперёд, шаринг ссылки
- Узкий фильтр → внятное «ничего не найдено»
Устойчивость и безопасность:
- 500 / таймаут / обрыв → состояние ошибки + повтор
- Медленная сеть → лоадер, не зависание
- Инъекции и XSS в запросе экранируются
- Большой каталог не тормозит UI
Локализация:
- Поиск на другом языке/раскладке
- Диакритика (café = cafe), транслит при необходимости
Итог
Поиск ломается предсказуемо: гонки запросов, отсутствие debounce, белый экран вместо empty state, состояние фильтров, теряющееся на reload. Хорошая новость — почти всё это детерминированно автоматизируется через перехват сети (page.route). Возьмите чек-лист, превратите каждый пункт в тест — и регресс поиска перестанет быть лотереей. А качество ранжирования и «находимость» оставьте человеку: автотест проверяет механику, не смысл.
Источники: Playwright — Network, Playwright — Mock APIs, Playwright — class Route, Playwright — Assertions, OWASP — Cross Site Scripting.