Розділ 1 з 12
Навіщо Page Object Model
Коли тестів десятки, ті самі локатори й послідовності дій повторюються у багатьох файлах. Page Object Model збирає знання про сторінку в одному класі, а тести говорять мовою застосунку: «увійти», «додати в кошик».
Документація описує page object як представлення частини вебзастосунку. В інтернет-магазині це можуть бути головна сторінка, каталог і сторінка оформлення замовлення. За документацією, page objects дають дві переваги. Вони спрощують написання тестів, бо створюють API вищого рівня, що відповідає вашому застосунку. І вони спрощують підтримку, бо селектори зібрано в одному місці, а код можна перевикористати замість повторювати.
POM — не функція Playwright, а патерн організації коду. Playwright нічого не вимагає від ваших класів: page object — звичайний TypeScript-клас, який усередині використовує page.
POM — не обов'язкова церемонія
Проблема. Інколи POM сприймають як правило «на кожну сторінку клас, на кожну кнопку метод», навіть для двох тестів.
Чому. Патерн окупається, коли сторінкою користуються кілька тестів і інтерфейс змінюється. Для одноразової перевірки він лише додає шар коду.
Рішення. Виносьте в page object те, що повторюється: локатори, які використовуються більше ніж в одному тесті, і послідовності дій з бізнес-змістом.
Коли. Щойно той самий локатор чи сценарій з'являється вдруге.
Розділ 2 з 12
Анатомія page object
Мінімальний page object має чотири частини: посилання на page, поля-локатори, конструктор, який їх ініціалізує, і асинхронні методи-дії.
Ініціалізувати локатори в конструкторі безпечно: як ми бачили в темі про локатори, створення локатора нічого не шукає на сторінці. Тому page object можна створити ще до goto(). Поля мають модифікатор readonly, щоб тест не міг випадково підмінити локатор. А поле page публічне, щоб тест за потреби мав доступ до самої сторінки, як у прикладі з документації.
Скорочений запис. У документації Playwright трапляється форма constructor(public readonly page: Page) {}: TypeScript сам оголошує поле й присвоює значення. Це те саме, що три рядки в прикладі вище. Для навчання явний запис зрозуміліший.
Розділ 3 з 12
Локатори в page object
У page object діють ті самі правила вибору локаторів, що й у тестах: спершу ролі, підписи й текст, потім test id, і лише в крайньому разі CSS. Зібрані в одному місці, вони стають ще ціннішими.
Приклад на сторінці документації про POM написано через page.locator('a', { hasText: 'Get started' }) і CSS-ланцюжки. Він ілюструє структуру класу, а не вибір локаторів. Для самих локаторів орієнтуйтеся на розділ Best Practices: ролі та явні контракти з розробниками.
Три форми локатора
Поле підходить для елементів, які на сторінці одні: поле Email, кнопка Sign in. Метод з параметром потрібен, коли елемент визначається даними: картка конкретного товару, рядок таблиці з конкретним замовленням. Похідний локатор будується від іншого: кнопка всередині картки знаходиться від локатора картки, а не від кореня сторінки.
Метод productCard(name) повертає Locator, а не Promise: він лише будує локатор і нічого не чекає. Тому він синхронний і без async. Тест може одразу перевірити картку: await expect(catalog.productCard('Wireless Mouse')).toBeVisible().
Розділ 4 з 12
Методи-дії
Метод page object описує, що робить користувач, а не які елементи він натискає. Саме це дає «API вищого рівня» з документації.
Порівняйте await loginPage.login(email, password) з трьома рядками fill, fill, click. Перший варіант читається як сценарій, і коли форма логіну зміниться (наприклад, додасться поле «Remember me»), правка буде в одному методі. Назва методу — дієслово з предметної області: login, search, addToCart, checkout. Назви на кшталт clickButton чи fillInput2 лише переносять технічні деталі з тесту в клас.
Кожен метод з діями Playwright асинхронний і повертає Promise. Явний тип повернення, наприклад Promise<void>, робить контракт видимим: у місці виклику одразу зрозуміло, що потрібен await.
Перехід на іншу сторінку
Якщо дія веде на іншу сторінку, метод може повернути її page object. Тоді тест не створює новий об'єкт вручну, а продовжує роботу з тим, що повернув метод.
CheckoutPage побудований за тим самим шаблоном. Його код є в розділі 7.
Розділ 5 з 12
Assertions: у тесті чи в page object
Найчастіше питання про POM — де писати expect. Практична відповідь: перевірки результату сценарію живуть у тесті, а page object дає для них локатори.
Тест — це історія: дія, дія, перевірка результату. Коли expect захований у page object, тест читається як «зроби щось», і вже не видно, що саме він перевіряє. До того ж той самий метод потрібен і в позитивному, і в негативному сценарії, а вбудована перевірка робить один з них неможливим.
Водночас документація показує expect усередині методу: у прикладі PlaywrightDevPage.getStarted() після кліку йде await expect(this.gettingStartedHeader).toBeVisible(). Така перевірка не про результат тесту, а про те, що дія завершилася і сторінка готова до наступного кроку. Це розумний компроміс: внутрішні перевірки стану в методах допустимі, а перевірки того, заради чого написано тест, лишаються в тесті.
Розділ 6 з 12
Компоненти
Шапка сайту, картка товару, модальне вікно повторюються на багатьох сторінках. Їх виносять в окремі класи-компоненти й підключають у page objects. Так одна частина інтерфейсу описана рівно один раз.
Компонент відрізняється від сторінки тим, що отримує в конструкторі не page, а кореневий Locator, і шукає всі свої елементи всередині нього. Тому той самий клас ProductCard працює з будь-якою карткою на будь-якій сторінці.
Розділ 7 з 12
BasePage: наслідування і композиція
Спільне для всіх сторінок можна винести в базовий клас. Але базовий клас — найпоширеніше місце, де POM перетворюється на звалище, тому в ньому має бути лише справді спільне.
Абстрактний BasePage зазвичай містить посилання на page, абстрактну адресу сторінки та метод goto(), який нею користується. abstract гарантує, що кожна сторінка оголосить свою адресу: якщо забути, tsc повідомить про помилку. Модифікатор protected робить поле видимим для нащадків і прихованим від тестів.
Шапка тут підключена не наслідуванням, а композицією: сторінка має шапку, а не є нею. Це правило зручно застосовувати й далі. Наслідування описує, чим сторінка є (будь-яка сторінка вміє відкриватися за адресою). Композиція описує, з чого сторінка складається (шапка, картки, модальні вікна).
Що не варто класти в BasePage
Проблема. У базовий клас поступово переїжджають «корисні» методи: waitForPageLoad() з waitForLoadState('networkidle'), clickElement(selector), getText(selector), sleep(ms).
Чому. networkidle у документації позначено як discouraged для тестування, а обгортки над click чи textContent з рядком-селектором повертають у код CSS-селектори й ручне читання тексту замість web-first assertions. До того ж клас, що вміє все, перестає бути зрозумілим.
Рішення. Тримайте в BasePage лише те, що справді спільне для всіх сторінок. Готовність сторінки перевіряйте assertion на видимий елемент, а спільні частини інтерфейсу виносьте в компоненти.
Коли додавати метод у BasePage. Коли він потрібен кожній сторінці і не залежить від конкретної розмітки.
Розділ 8 з 12
Page objects як фікстури
Створювати page objects у кожному тесті через new незручно. Документація про фікстури показує саме POM як головний приклад: page object стає фікстурою, і тест просто оголошує, що він йому потрібен.
Документація перелічує переваги фікстур над хуками. Фікстури інкапсулюють підготовку і прибирання в одному місці. Вони перевикористовуються між файлами. Вони створюються на вимогу: раннер піднімає лише ті фікстури, які згадав тест. Вони комбінуються і можуть залежати одна від одної. І вони звільняють від describe-блоків, що існують лише заради спільного beforeEach.
Назви фікстур, за документацією, мають починатися з літери або підкреслення і містити лише літери, цифри й підкреслення. Детально фікстури, їхній scope і порядок виконання розбираються в окремій темі.
Розділ 9 з 12
Структура проєкту
Playwright не диктує, де лежать page objects. Але домовленість про структуру важлива: нова людина в команді має з першого погляду розуміти, де шукати сторінку, компонент чи фікстуру.
Page objects, компоненти й фікстури лежать поза testDir, тож раннер не намагається зібрати їх як тести. Щоб імпорти не перетворювалися на ../../../pages/LoginPage, зручні path aliases з tsconfig.json, які Playwright підтримує без додаткових пакетів (тема про TypeScript для Playwright).
Розділ 10 з 12
Тестові дані і типи
Коли метод приймає п'ять рядків поспіль, їх легко переплутати. Об'єкт з інтерфейсом робить виклик самодокументованим, і TypeScript перевіряє, що нічого не забуто.
Порівняйте fillShipping('John Doe', '1 Main St', 'Springfield') з fillShipping({ fullName: 'John Doe', street: '1 Main St', city: 'Springfield' }). У другому варіанті видно, що де, порядок полів неважливий, а додавання нового поля в інтерфейс одразу підсвітить усі місця, де його не передано. Дані для тестів зберігають в окремих файлах і типізують тими самими інтерфейсами, що й методи page object.
Розділ 11 з 12
Типові помилки
Більшість проблем з POM з'являються не одразу, а через кілька місяців, коли класи розростаються. Ці помилки варто впізнавати рано.
Найчастіше трапляються такі помилки. Методи, що повертають прочитаний текст для порівняння в тесті, замість локатора для web-first assertion. Перевірки результату, сховані в page object. CSS-селектори в полях замість ролей і підписів. Забутий await перед методом page object. Методи з назвами за механікою (clickButton). Утилітні методи в BasePage з networkidle і паузами. Page object, що зберігає стан між тестами. І фікстура без виклику use.
Розділ 12 з 12
Шпаргалка
Усе основне на одному екрані. Після шпаргалки — підсумковий квіз для самоперевірки.
Чотири правила на пам'ять. Page object описує частину застосунку і містить її локатори та дії, названі мовою користувача. Він повертає локатори, а тест перевіряє результат через web-first assertions. Спільні частини інтерфейсу — компоненти, а не методи BasePage. Page objects підключаються в тести через фікстури.
Самоперевірка
Підсумковий квіз
15 питань за всіма розділами. Після кожної відповіді побачите пояснення, а в кінці — список тем, які варто повторити.