Фильтрация — один из самых частых и критичных сценариев в интернет-магазинах и маркетплейсах. Ошибка в логике фильтров может стоить бизнесу продаж, а пользователям — нервов.
В этой статье я покажу, как построить гибкий и масштабируемый подход к тестированию фильтрации с помощью 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 в комментариях 👇
👋 Удачного тестирования, и пусть ваши фильтры всегда работают идеально!
ссылка на оригинал статьи https://habr.com/ru/articles/940136/
Добавить комментарий