Каждый раз, начиная новый проект, автоматизатор проходит один и тот же путь: структура 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