惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
人人都是产品经理
人人都是产品经理
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
MongoDB | Blog
MongoDB | Blog
Google DeepMind News
Google DeepMind News
WordPress大学
WordPress大学
量子位
美团技术团队
大猫的无限游戏
大猫的无限游戏
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Last Week in AI
Last Week in AI
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
小众软件
小众软件
C
Check Point Blog
博客园 - 三生石上(FineUI控件)
N
Netflix TechBlog - Medium
Recent Announcements
Recent Announcements
有赞技术团队
有赞技术团队
月光博客
月光博客

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Тестирование поиска и фильтров: чек-лист + как автоматизи...
Earthly Man · 2026-06-23 · via Все публикации подряд на Хабре

Средний

6 мин

0

Поиск и фильтры есть почти в каждом продукте, и баги в них одни и те же из проекта в проект: запрос на каждую букву, гонка запросов, белый экран вместо «ничего не найдено», фильтры, которые слетают на reload. Хорошая новость — почти всё это детерминированно автоматизируется. Разбираем чек-лист того, что проверять, и для каждого пункта показываем рабочий автотест на Playwright через перехват сети.

Поиск кажется простым: поле ввода, запрос, список результатов. Но именно поэтому его тестируют поверхностно — «ввёл слово, что-то нашлось, ок». А реальные баги живут в граничных запросах, в тайминге (debounce, гонки), в пустых и ошибочных состояниях и в фильтрах. Ниже — двухслойный разбор: сначала что проверять, потом как это автоматизировать, с реальными сниппетами на TypeScript. Идея переносится на любой UI-фреймворк, но Playwright особенно удобен из-за встроенного перехвата сети.

Ключевая идея: контролируем сеть

Половину интересных кейсов невозможно стабильно воспроизвести, если не управлять ответами бэкенда: медленный ответ, ошибка 500, пустая выдача, гонка запросов. Если просто бить по реальному API, тесты будут флакать, а нужные состояния — не воспроизводиться по требованию.

Поэтому основа автотестов поиска — page.route(): перехватываем запрос к API и сами решаем, что и когда вернуть. Это делает тесты детерминированными и быстрыми. См. Network и Mock APIs в документации Playwright.

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 мс). Иначе на «iphone» прилетает шесть запросов, бэкенд получает лишнюю нагрузку, а UI — лишние перерисовки.

Автотест. Считаем реальные обращения к 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. Гонка запросов: показан ответ на последний ввод

Что проверять: если медленный ответ на старый запрос приходит позже быстрого на новый, на экране должен остаться результат последнего ввода, а не перетёртый устаревшим ответом. Это классический баг живых поисков: пользователь печатает «abc», но видит результаты по «ab», потому что тот ответ пришёл последним.

Автотест. Искусственно тормозим старый запрос и проверяем, что побеждает свежий:

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> не ломают выдачу и не исполняются. Поисковая строка часто отражается обратно в DOM — это классический вектор reflected XSS. См. OWASP XSS.

test('спецсимволы экранируются', async ({ page }) => {
  const xss = '<img src=x>';
  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 не сработал
});

Перехват dialog — простой и надёжный сигнал: если alert() из инъекции сработал, тест это поймает.

6. Фильтры и фасеты + состояние в URL

Что проверять: комбинация фильтров (AND/OR) корректна; «сбросить всё» работает; узкий фильтр даёт внятное «ничего не найдено»; состояние фильтров живёт в URL и переживает reload и кнопки назад/вперёд. Последнее особенно важно: если состояние не в URL, пользователь не сможет ни поделиться ссылкой на выдачу, ни вернуться к ней кнопкой «назад».

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

Автотест проверяет механику, а не смысл. Поэтому есть вещи, которые в e2e тащить не нужно:

  • Качество ранжирования. «Точное совпадение выше частичного», релевантность — субъективно и хрупко в UI-тестах. Лучше golden-set на уровне поискового бэка или юнитов, а не Playwright.

  • Морфология и синонимы. Стемминг, окончания, опечатки — отдельные тесты поискового движка, не сквозные.

  • Исследовательское тестирование. Реальная «находимость» и неожиданные формулировки запросов — руками. Тут человек незаменим.

Полный чек-лист

Запрос:

  • Пустой / только пробелы / 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 — NetworkPlaywright — Mock APIsPlaywright — class RoutePlaywright — AssertionsOWASP — Cross Site Scripting.


Пишу про практику QA каждый день в Telegram-канале «QA — Quality Assurance»: t.me/qa10100011000001. Если зашло — забегайте.