Refaktoryzacja testów Playwright - akcje aplikacji vs Page Object Model

Refaktoryzacja testów Playwright - akcje aplikacji vs Page Object Model

Wstęp

Po logowaniu z Playwright specyfikacja robi się długa: ten sam setup API, te same selektory, powtarzalne asercje. W Cypressie rozwiązywałeś to custom commands i App Actions; w Playwrightcie nie ma Cypress.Commands.add, ale ten sam podział odpowiedzialności da się zrobić czysto w TypeScriptzie.

Poprzedni wpis serii: Testy logowania z Playwright. Oryginał przy Cypressie: Refaktoryzacja testów - App Actions vs POM.

„App Actions” - zwykłe funkcje + Page

Zamiast magicznego cy.login tworzysz moduł z funkcjami, które przyjmują Page lub APIRequestContext:

// helpers/auth.ts
import { expect, type APIRequestContext, type Page } from "@playwright/test"

const API = process.env.API_URL ?? "http://localhost:5000"

export async function seedUser(request: APIRequestContext) {
  const body = { user: { username: "test", email: "test@test.com", password: "test" } }
  const del = await request.delete(`${API}/users`, { data: body })
  expect([200, 204, 404]).toContain(del.status())
  const post = await request.post(`${API}/users`, { data: body })
  expect(post.ok()).toBeTruthy()
}

export async function loginViaForm(page: Page, email: string, password: string) {
  if (email) await page.locator(".form-control").nth(0).fill(email)
  if (password) await page.locator(".form-control").nth(1).fill(password)
  await page.locator(".btn").click()
}

export async function expectLoggedInNavbar(page: Page) {
  await expect(page.locator(":nth-child(4) > .nav-link")).toHaveAttribute("href", "/@test")
  await expect(page.locator(":nth-child(3) > .nav-link")).toHaveAttribute("href", "/settings")
}

Test staje się listą kroków biznesowych:

import { test, expect } from "@playwright/test"
import { seedUser, loginViaForm, expectLoggedInNavbar } from "../helpers/auth"

test("successful login", async ({ page, request }) => {
  await seedUser(request)
  await page.goto("/login")
  await loginViaForm(page, "test@test.com", "test")
  await expect(page).toHaveURL(/localhost:4100\/$/)
  await expectLoggedInNavbar(page)
})

Chcesz „łańcuch” jak w Cypressie? Zwracaj obiekt z metodami albo użyj test.extend i fixture z metodami - to odpowiednik centralnego support/index.js, tylko jawny import w pliku testu.

Page Object Model

Ten sam scenariusz w POM: klasa trzyma referencję do Page i enkapsuluje lokatory.

// page-objects/LoginPage.ts
import type { Page } from "@playwright/test"

export class LoginPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto("/login")
  }

  private email() {
    return this.page.locator(".form-control").nth(0)
  }
  private password() {
    return this.page.locator(".form-control").nth(1)
  }

  async submitCredentials(email: string, password: string) {
    if (email) await this.email().fill(email)
    if (password) await this.password().fill(password)
    await this.page.locator(".btn").click()
  }

  async loginExpectingError() {
    await this.submitCredentials("test@test.com", "wrong")
    return this
  }
}

Osobna klasa HomePage z metodami na linki w navbarze (jak w POM z Cypressa) zwracasz po udanym logowaniu - ten sam podział plików, tylko z Page zamiast cy. API użytkownika (User z remove/create) wołasz przez request.post / request.delete.

Puste stringi a fill

W Cypressie .type('') było problemem i wymagało warunków w custom command (issue). W Playwright fill('') na pustym polu zwykle wystarczy, a jeśli chcesz tylko kliknąć „Sign in”, po prostu nie wywołujesz fill - dokładnie ten sam kompromis co w poprawionej wersji cy.login z oryginalnego wpisu.

Co wybrać?

  • Funkcje / małe moduły - mniej ceremonii, świetne przy zespole, który i tak trzyma warstwę domenową w TS.
  • POM - znajomy układ dla ludzi po Selenium, łatwo trzymać „mapę” ekranu w jednym miejscu.

Oba style mogą współistnieć (np. POM dla UI, czyste funkcje dla API).

Kod na GitHubie

Logika odpowiada gałęzi z serii Cypress:

https://github.com/12masta/react-redux-realworld-example-app/tree/3-cypress

https://github.com/12masta/react-redux-realworld-example-app/pull/3/files

Commity z artykułu o Cypressie: App Actions, POM.

Następny odcinek: URL-e, baseURL i konfiguracja.