Фильтрация — один из самых частых и критичных сценариев в интернет-магазинах и маркетплейсах. Ошибка в логике фильтров может стоить бизнесу продаж, а пользователям — нервов.
В этой статье я покажу, как построить гибкий и масштабируемый подход к тестированию фильтрации с помощью Playwright + TypeScript, используя: Page Object Model, Data-driven testing, конфигурацию фильтров и кастомные фикстуры.
Такой подход делает тесты читаемыми, поддерживаемыми и легко расширяемыми — добавление нового фильтра займёт минуты.
📌 Задача
Представим страницу каталога с фильтрами:
«Только товары со скидкой»
«Цена от/до»
«Тип товара» (одежда, электроника и т. д.)
Кнопка Reset Filters
Нужно протестировать:
Работу каждого фильтра по отдельности.
Комбинации фильтров.
Сброс фильтров.
Корректные значения по умолчанию.
Отсутствие результатов при невозможной комбинации.

📄 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)
})

🧩 Частые проблемы и их решения
Асинхронное обновление страницы: После применения фильтров страница может обновляться с задержкой из-за запросов к API. Чтобы избежать ошибок, используйте
await page.waitForResponse()
или дожидайтесь исчезновения элементов лоадеров/скелетонов для того, чтобы понять когда произошло завершение загрузки.Хрупкость локаторов: Если структура страницы меняется, тесты могут ломаться. Рекомендуется использовать data-testid или другие стабильные селекторы.
Тестирование пагинации: Если каталог поддерживает пагинацию, добавьте метод
getAllItemsWithPagination
в ItemsPage, который будет собирать товары со всех страниц.Данных в UI не хватает для валдиации всех фильтров: если карточка товара, не содержит нужного количества данных, для валидации фильтра, воспользуйтесь API - сделайте запрос информации о товаре и сравните его с заданным фильтрами.
📢 Итоги
Мы рассмотрели, как реализовать тесты фильтрации, которые:
Основаны на Page Object Model
Имеют конфигурацию фильтров
Используют кастомные фикстуры
Тестируют при помощи data-driven подхода
Легко масштабируются под новые условия
Остаются читаемыми и поддерживаемыми
Такой подход помогает QA-команде тратить меньше времени на рутину и быстрее адаптироваться к изменениям продукта.
🔗 Полный пример
Исходный код примеров доступен в репозитории:
👉 old-door/qa-playground
А ещё я подготовил демо-страницу со списком товаров и фильтрацией — можно запустить и попробовать тесты самому.
💬 Ваше мнение?
Сталкивались ли вы с тестированием фильтрации?
Используете ли конфигурационный подход или обходились классическим POM?
Давайте поделимся best practices в комментариях 👇
👋 Удачного тестирования, и пусть ваши фильтры всегда работают идеально!