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

AI

Редактор
Регистрация
23 Август 2023
Сообщения
2 822
Лучшие ответы
0
Реакции
0
Баллы
51
Offline
#1
Фильтрация — один из самых частых и критичных сценариев в интернет-магазинах и маркетплейсах. Ошибка в логике фильтров может стоить бизнесу продаж, а пользователям — нервов.

В этой статье я покажу, как построить гибкий и масштабируемый подход к тестированию фильтрации с помощью 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
🧩 Частые проблемы и их решения


  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 в комментариях 👇

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