Гибкий подход к тестированию фильтров с Playwright + TypeScript

от автора

Фильтрация — один из самых частых и критичных сценариев в интернет-магазинах и маркетплейсах. Ошибка в логике фильтров может стоить бизнесу продаж, а пользователям — нервов.

В этой статье я покажу, как построить гибкий и масштабируемый подход к тестированию фильтрации с помощью Playwright + TypeScript, используя: Page Object Model, Data-driven testing, конфигурацию фильтров и кастомные фикстуры.

Такой подход делает тесты читаемыми, поддерживаемыми и легко расширяемыми — добавление нового фильтра займёт минуты.

📌 Задача

Представим страницу каталога с фильтрами:

  • «Только товары со скидкой»

  • «Цена от/до»

  • «Тип товара» (одежда, электроника и т. д.)

  • Кнопка Reset Filters

Нужно протестировать:

  1. Работу каждого фильтра по отдельности.

  2. Комбинации фильтров.

  3. Сброс фильтров.

  4. Корректные значения по умолчанию.

  5. Отсутствие результатов при невозможной комбинации.

Пример фильтруемого каталога с товарами

Пример фильтруемого каталога с товарами

📄 Page Object Model (POM) — это база

Подробней про POM можно почитать в документации Playwright’а.

Для начала давайте найдем и опишем локаторы, с которыми нам предстоит работать, для этого, создаем класс ItemsPageLocators :

import { Locators } from '../../base/locators'  export class ItemsPageLocators extends Locators {   // Example locator   itemCard(){     return this.page.getByTestId('item-card').describe('Item card')   }    // List other locators: onlyDiscountCheckboxFilter, minPriceInputFilter, etc..  }

Далее, класс страницы ItemsPage — который будет отвечать за бизнес логику. В нём мы используем локаторы из созданного ранее класса ItemsPageLocators и сделаем методы по парсингу карточек товара, применению фильтров и валидации.

Примеры реализации методов ItemsPage

Парсинг карточек товара:

getItems(): Promise<Item[]>{     return test.step('Get items', async ()=> {       const arr: Item[] = []       for (const [i, item] of (await this.locators.itemCard().all()).entries()) {         await test.step(`Item #${i+1}`, async ()=> {           const obj = {             name: (await item.locator(this.locators.itemName()).textContent())!,             type: (await item.locator(this.locators.itemType()).textContent())!,             price: Formats.PRICE.parser((await item.locator(this.locators.itemPrice()).textContent())!),             originalPrice: await item.locator(this.locators.itemOriginalPrice()).isVisible()                ? Formats.PRICE.parser((await item.locator(this.locators.itemOriginalPrice()).textContent())!)               : undefined           }           expect(obj, `Item #${i+1} ${obj.name} to be parsed successfully`).toEqual(             {               name: expect.any(String),               type: expect.any(String),               price: expect.any(Number),               originalPrice: obj.originalPrice ? expect.any(Number) : undefined             }           )           arr.push(obj)         })       }       return arr     })   }

Применение фильтра или множества фильтров:

  async filter(options: FilterOptions | FilterOptions[]){     for (const { type, value} of Array.isArray(options) ? options : [options]) {       await test.step(`Filter by ${type}: ${value}`, async ()=> {         await getFilterConfig(type).apply(this.locators, value)       })     }   }

Проверяем, что товары соответствуют выбранным фильтрам:

  async validate(options: FilterOptions | FilterOptions[]) {     const items = await this.getItems()     expect(       items.length,       `To have at least 1 filtered item`     ).toBeGreaterThanOrEqual(1)     for (const { type, value } of Array.isArray(options) ? options : [options]) {       await test.step(`Validate filter by ${type}: ${value}`, async () => {         const validate = getFilterConfig(type).validate         for (const item of items) {           await validate(value, item)         }       })     }   }

А так же проверка дефолтного состояния фильтров:

  async validateFilterDefaultState(){     await test.step(`Validate all filter inputs default state`, async ()=> {       const items = await this.getItems()       for (const type of Object.values(ItemsFilters)) {         await test.step(`Validate default state for ${type}`, async () => {           await getFilterConfig(type).defaultValidate(this.locators, items)         })       }     })   }

⚙️ Конфигурация фильтров: выносим правила в отдельный слой

Вместо того, чтобы писать switch/case, if/else, внутри методов ItemsPage, мы выносим правила работы каждого фильтра в конфигурационный объект в filter-configs.ts:

import { expect } from '@playwright/test' import { FilterConfig, ItemsFilters } from './types'  const filterConfigs: Record<ItemsFilters, FilterConfig> = {    [ItemsFilters.MIN_PRICE]: {     apply: async (locators, value) => {       await locators.minPriceInputFilter().fill(String(value))     },     validate: (value, item) => {       expect.soft(item.price, `${item.name} price to be >= ${value}`).toBeGreaterThanOrEqual(parseInt(String(value)))     },     defaultValidate: async (locators) => {       await expect(locators.minPriceInputFilter(), `Min price to be 0`).toHaveValue('0')     },   },    // ... List other filters bellow }

Теперь добавление нового фильтра = новый элемент в enum ItemsFilters + запись в filter-configs.ts. Класс ItemsPage при этом менять не нужно

🛠 Используем кастомные фикстуры

Что такое фикстуры и зачем они нужны можно почитать в документации playwright’а.
В нашем случае, в pages.fixtures.ts мы добавляем новую фикстуру itemsPage, которая будет открывать страницу ItemsPage и ждать, что страница загрузилась и готова к проведению тестирования:

  itemsPage: async ({ page }, use) => {     const itemsPage = new ItemsPage(page)     await itemsPage.open()     await use(itemsPage)   },

🧪 И наконец-то — тестируем!

Теперь, когда всё готово к тестированию, переходим к созданию тест спеки(набора тестов) filterable-items.spec.ts. В тестах мы будем использовать data-driven подход.

Примеры реализации тестов из спеки filterable-items.spec.ts

В качестве параметров определим следующие фильтры:

  const filters: FilterOptions[] = [     {       type: ItemsFilters.MIN_PRICE,       value: 50     },     {       type: ItemsFilters.MAX_PRICE,       value: 500     },     {       type: ItemsFilters.ONLY_DISCOUNT,       value: true     },     {       type: ItemsFilters.TYPE,       value: ['Clothing', 'Electronics']     },   ]

Эти параметры, мы используем для проверки каждого фильтра по отдельности:

  for (const filter of filters) {     test(`Single filter › ${filter.type}: ${filter.value}`, async ({ itemsPage }) => {       await itemsPage.filter(filter)       await itemsPage.validate(filter)     })   }

А так же и комбинации фильтров:

  test(`Multiple filters`, async ({ itemsPage }) => {     await itemsPage.filter(filters)     await itemsPage.validate(filters)   })

Сброс фильтров — проверяем при помощи:
— Проверки дефолтного состояния фильтров
— Сравнения списка товаров с первоначальным состоянием(до фильтрации):

  test(`Reset filters`, async ({ itemsPage }) => {     const before = await itemsPage.getItems()     await itemsPage.filter(filters)     await itemsPage.locators.resetFiltersButton().click()     await itemsPage.validateFilterDefaultState()     const after = await itemsPage.getItems()     expect(after,       'Items array before filtering and after reset filters to be equal'     ).toEqual(before)   })

Проверка дефолтного состояния фильтров:

  test(`Filters default state`, async ({ itemsPage }) => {     await itemsPage.validateFilterDefaultState()   })

Негативный сценарий — задаем фильтры не совпадающие ни с одним товаром:

  test(`Nothing found`, async ({ itemsPage }) => {     const filters: FilterOptions[] = [       {         type: ItemsFilters.MIN_PRICE,         value: 300       },       {         type: ItemsFilters.TYPE,         value: ['Home']       }     ]     await itemsPage.filter(filters)     await expect(itemsPage.locators.itemCard()).toHaveCount(0)   })
Результат тестов в Playwright UI mode

Результат тестов в Playwright UI mode

🧩 Частые проблемы и их решения

  1. Асинхронное обновление страницы: После применения фильтров страница может обновляться с задержкой из-за запросов к API. Чтобы избежать ошибок, используйте await page.waitForResponse() или дожидайтесь исчезновения элементов лоадеров/скелетонов для того, чтобы понять когда произошло завершение загрузки.

  2. Хрупкость локаторов: Если структура страницы меняется, тесты могут ломаться. Рекомендуется использовать data-testid или другие стабильные селекторы.

  3. Тестирование пагинации: Если каталог поддерживает пагинацию, добавьте метод getAllItemsWithPagination в ItemsPage, который будет собирать товары со всех страниц.

  4. Данных в UI не хватает для валдиации всех фильтров: если карточка товара, не содержит нужного количества данных, для валидации фильтра, воспользуйтесь API — сделайте запрос информации о товаре и сравните его с заданным фильтрами.

📢 Итоги

Мы рассмотрели, как реализовать тесты фильтрации, которые:

  • Основаны на Page Object Model

  • Имеют конфигурацию фильтров

  • Используют кастомные фикстуры

  • Тестируют при помощи data-driven подхода

  • Легко масштабируются под новые условия

  • Остаются читаемыми и поддерживаемыми

Такой подход помогает QA-команде тратить меньше времени на рутину и быстрее адаптироваться к изменениям продукта.

🔗 Полный пример
Исходный код примеров доступен в репозитории:
👉 old-door/qa-playground

А ещё я подготовил демо-страницу со списком товаров и фильтрацией — можно запустить и попробовать тесты самому.

💬 Ваше мнение?
Сталкивались ли вы с тестированием фильтрации?
Используете ли конфигурационный подход или обходились классическим POM?

Давайте поделимся best practices в комментариях 👇

👋 Удачного тестирования, и пусть ваши фильтры всегда работают идеально!


ссылка на оригинал статьи https://habr.com/ru/articles/940136/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *