Playwright + TypeScript без мучений: POM, фикстуры, CI, которые реально работают
Каждый раз, начиная новый проект, автоматизатор проходит один и тот же путь: структура Page Object Model, конфиг, CI, репортинг. Работа не сложная, но нудная — и её приходится переделывать заново на каждом новом месте. Я собрала это один раз по уму и зафиксировала структуру, к которой всегда можно вернуться. Делюсь тем, что получилось, и почему сделано именно так.
Почему не просто «накидать тестов»
Плоский файл с тестами работает, пока их пять. На пятидесятом любое изменение вёрстки ломает половину сьюта, если локаторы — абсолютный XPath, а логика страницы размазана по каждому тесту. Задача архитектуры — чтобы один и тот же элемент менялся в одном месте, а не в двадцати файлах тестов.
Page Object Model с чётким базовым классом
Базовый класс держит только переиспользуемые действия — не селекторы конкретных страниц:
export abstract class BasePage {
protected readonly page: Page;
constructor(page: Page) {
this.page = page;
}
async goto(path: string = '/'): Promise<void> {
await this.page.goto(path);
}
async waitForVisible(locator: Locator, timeoutMs = 5_000): Promise<void> {
await locator.waitFor({ state: 'visible', timeout: timeoutMs });
}
}Конкретные страницы наследуются от него и держат только свои локаторы и действия. Важный момент — локаторы через data-test/id, никаких абсолютных XPath:
export class LoginPage extends BasePage {
private readonly usernameInput: Locator;
private readonly loginButton: Locator;
constructor(page: Page) {
super(page);
this.usernameInput = page.locator('[data-test="username"]');
this.loginButton = page.locator('[data-test="login-button"]');
}
async login(username: string, password: string): Promise<void> {
await this.usernameInput.fill(username);
await this.loginButton.click();
}
}Абсолютный XPath ломается от любого изменения вёрстки. data-test — контракт, который явно говорит фронтенду «это используется тестами, не трогай просто так».
Кастомные фикстуры вместо ручной инициализации Page Object'ов
Вместо new LoginPage(page) в каждом тесте — инжектим объекты прямо в тест через test.extend:
export const test = base.extend<{ loginPage: LoginPage; inventoryPage: InventoryPage }>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
inventoryPage: async ({ page }, use) => {
await use(new InventoryPage(page));
},
});В тесте это выглядит так:
test('валидный пользователь попадает в inventory', async ({ loginPage, page }) => {
await loginPage.login('standard_user', 'secret_sauce');
await expect(page).toHaveURL(/inventory.html/);
});Меньше шаблонного кода в каждом файле, точка создания объектов — одна.
API-тесты рядом с UI, а не отдельным проектом
Тонкая обёртка над APIRequestContext, чтобы не плодить голые request.get() по всем тестам:
export class ApiClient {
constructor(private readonly request: APIRequestContext, private readonly headers: Record<string, string> = {}) {}
async get(path: string, params?: Record<string, string | number>) {
return this.request.get(path, { params, headers: this.headers });
}
async post<T extends Record<string, unknown>>(path: string, data: T) {
return this.request.post(path, { data, headers: this.headers });
}
}Playwright умеет гонять UI и API тесты в одном конфиге разными проектами — не нужен отдельный репозиторий или фреймворк под API.
CI, который реально работает, а не «пример»
- name: Run UI tests
run: npm run test:ui
- name: Run API tests
run: npm run test:api
- name: Upload Playwright HTML report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/UI и API — отдельными шагами, отчёт сохраняется как артефакт даже при падении тестов (if: always()), чтобы не терять диагностику.
Итог
Такую структуру можно собрать за вечер, если делать это не в первый раз. Если делать в первый раз — уходит куда больше времени на грабли: не тот способ подключить fixtures, забытый if: always() в CI, локаторы через XPath, которые потом всё ломают.
Я оформила рабочую версию этого фреймворка (с реальными зелёными тестами, а не заглушками) в готовый стартовый набор — если не хочется тратить вечер на сборку с нуля: https://viratrach.gumroad.com/l/rldvh