checklistautomationplaywrightsearchqa

Чек-лист тестирования поиска и фильтров — и как автоматизировать каждый пункт на 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.