Wszystkie wpisy

Pierwszy projekt Playwright - setup 1.22 i @playwright/test
Pierwszy projekt Playwright - setup 1.22 i @playwright/test

English

Pierwszy projekt Playwright - setup 1.22 i @playwright/test

Maj 2022: stawiam pusty projekt Playwright 1.22 (TypeScript, trzy przeglądarki, codegen, Trace Viewer). To nie jest skończona migracja z Cypressa.

Playwright

Po kwietniowej decyzji

W kwietniu opisałem, dlaczego zaczynam schodzić z Cypressa. Powody były trzy i nadal się ich trzymam: chcę tego samego kodu na trzech silnikach, chcę runnera, który sam ogarnia równoległość i retry, i chcę artefaktu z nieudanego przebiegu, który da się otworzyć następnego dnia bez zgadywania.

Miesiąc później mam z tego dokładnie zero linijek kodu. Decyzja to nie migracja. Cypress 9.6.1, wydany 9 maja, nadal odpala u mnie wszystko, co odpalał w marcu, i nic z tego w tym miesiącu nie znika. Ten wpis jest o czymś węższym: o pustym katalogu, w którym po godzinie ma stać playwright.config.ts, jeden sensowny plik .spec.ts i komenda, która to uruchamia.

Piszę to świadomie jako scaffolding, a nie jako “jak przepisałem suite”. Przepisywanie testów zaczyna się od decyzji, których jeszcze nie podjąłem: czy zostaję przy page objectach, które porównywałem z app actions jeszcze w serii o Cypressie, czy idę w fixtures Playwrighta, jak wygląda logowanie, gdzie trzymam dane. Żadnej z nich nie chcę podejmować w tym samym tygodniu, w którym pierwszy raz uruchamiam runner. Najpierw ma być zielono na czymś trywialnym, dopiero potem architektura.

Jedna rzecz, o której warto pamiętać przez cały wpis: Playwright nie jest nowością tego miesiąca. Wersja 1.0.0 wyszła w maju 2020, runner @playwright/test doszedł w 1.12.0 w czerwcu 2021, a Trace Viewer razem z nim. Ja jestem spóźniony o dwa lata, nie wcześnie.

Node i pin paczki

Zaczynam od Node’a, bo to jedyna rzecz, którą łatwo zepsuć przed pierwszą komendą. Siedzę na Node 16 LTS (16.15.0). Node 18 jest od 19 kwietnia gałęzią Current, ale statusu LTS nie ma jeszcze i nie stawiam na nim projektu, do którego chcę wrócić na CI. Playwright 1.22 formalnie wspiera jeszcze Node 12, co tylko potwierdza, że 16 jest tu bezpiecznym środkiem.

Kreator jest w paczce od 1.18.0 ze stycznia:

$ npm init playwright@latest

Kreator przeprowadza przez kilka pytań i wypluwa gotowy szkielet z configiem, przykładowym testem i przeglądarkami. Nie przepisuję tu jego promptów jeden do jednego, bo zmieniają się z wersji na wersję i za rok ta lista będzie nieprawdziwa. Odpalam go raz, żeby zobaczyć, co uznaje za domyślne, i od razu potem robię rzecz, którą uważam za ważniejszą od samego kreatora: przypinam wersję. @latest w komendzie inicjalizującej jest w porządku, ^ w package.json przy narzędziu, które co trzy tygodnie zmienia zachowanie przeglądarek, już nie.

Mój package.json po sprzątaniu wygląda tak:

{
  "name": "playwright-scaffold",
  "private": true,
  "engines": {
    "node": ">=16"
  },
  "scripts": {
    "test": "playwright test",
    "test:headed": "playwright test --headed",
    "report": "playwright show-report"
  },
  "devDependencies": {
    "@playwright/test": "1.22.0"
  }
}

Wersja bez daszka, świadomie. 1.22.0 wyszło 12 maja, trzy dni temu, i to jest jedyna wersja, na której cokolwiek w tym wpisie sprawdzałem. Kiedy przyjdzie 1.22.1 albo 1.23, chcę je podnieść osobnym commitem i osobnym przebiegiem, a nie dowiedzieć się o zmianie z czerwonego builda w piątek.

Warto też wiedzieć, czego tu nie ma. Nie ma paczki typescript, mimo że config i testy są w TypeScripcie. @playwright/test transpiluje pliki .ts sam i do uruchomienia testów niczego więcej nie potrzebuje. To znaczy jednocześnie, że runner nie sprawdza typów, tylko je zdejmuje. Jeśli chcę realnego type-checkingu, muszę dołożyć typescript osobno i odpalać tsc --noEmit jako oddzielny krok. Na pustym projekcie z jednym testem sobie to daruję, ale zapisuję jako dług.

Po instalacji paczki trzeba jeszcze ściągnąć same przeglądarki:

$ npm install
$ npx playwright install

To pobiera Chromium, Firefoksa i WebKit w wersjach zbundlowanych z 1.22.0, czyli Chromium 102.0.5005.40, Firefox 99.0.1 i WebKit 15.4. Nie są to przeglądarki zainstalowane w systemie i to jest cecha, nie wada: przebieg u mnie i przebieg na agencie mają ten sam silnik, bo silnik przyjechał z paczką. Na Linuksie na czystej maszynie dojdzie jeszcze npx playwright install --with-deps, żeby doinstalować biblioteki systemowe. Zapisuję tę komendę teraz, bo będzie mi potrzebna, kiedy w końcu dotknę pipeline’u.

playwright.config.ts

Config jest sercem tego setupu i to jest największa różnica wobec tego, co pamiętam z pierwszych testów w Cypressie. Tam konfiguracja była krótkim JSON-em, a większość decyzji zapadała w kodzie testu. Tutaj plik konfiguracyjny odpowiada za równoległość, retry, artefakty i macierz przeglądarek, więc test może zostać głupi.

W 1.22 typowany config wygląda tak (defineConfig to jeszcze nie ta epoka, więc typuję obiekt wprost):

import type { PlaywrightTestConfig } from '@playwright/test'
import { devices } from '@playwright/test'

const config: PlaywrightTestConfig = {
  testDir: './tests',
  timeout: 30_000,
  expect: {
    timeout: 5_000,
  },
  fullyParallel: false,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 1 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: 'html',
  use: {
    baseURL: process.env.BASE_URL ?? 'https://demo.playwright.dev',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
}

export default config

Kilka pól wymaga komentarza, bo nie wybrałem ich domyślnie.

baseURL biorę ze zmiennej środowiskowej z fallbackiem na publiczne demo. Dzięki temu ten sam config obsłuży lokalną aplikację i demo, a w testach nie ma ani jednego pełnego adresu. Jest tu jedna pułapka, na którą wszedłem w pierwszej godzinie: względny URL jest sklejany przez new URL(), więc page.goto('/todomvc/') z baseURL bez ścieżki daje to, czego oczekuję, ale page.goto('/') przy baseURL zakończonym ścieżką wyrzuci mnie do korzenia domeny. Dlatego trzymam baseURL na samym origin, a ścieżkę podaję w teście.

fullyParallel mam wyłączone i to jest decyzja, nie zapomnienie. Flaga jest w runnerze od linii 1.20 i puszcza równolegle pojedyncze testy, a nie tylko pliki. Na pustym projekcie z dwoma testami nie zyskam nic, a stracę czytelność pierwszego czerwonego przebiegu. Włączę ją wtedy, kiedy będę wiedział, że testy nie dzielą stanu. Kolejność jest tu dokładnie ta sama, którą rok temu domykałem podsumowaniem o izolacji: najpierw izolacja, potem równoległość. Runner, który obiecuje “zero flake’a po włączeniu flagi”, nie istnieje, a Playwright wcale tego nie obiecuje.

retries na CI ustawiam na 1, a lokalnie na 0. To nie jest przypadkowe i wiąże się bezpośrednio z następnym polem.

trace: 'on-first-retry' oznacza dosłownie: nagraj trace dopiero wtedy, gdy test padł i runner próbuje po raz drugi. Zielony przebieg nie kosztuje nic, a pierwsza czerwona próba zostawia komplet dowodów. Alternatywy w 1.22 to off, on i retain-on-failure. on wygląda kusząco przy trzech testach i zaczyna boleć przy stu, bo każdy trace to plik z zrzutami DOM i zdjęciami. Konsekwencja on-first-retry, o której łatwo zapomnieć: lokalnie przy retries: 0 nigdy nie dostanę trace’u, bo nie ma drugiej próby. Kiedy chcę go u siebie, odpalam przebieg z --retries=1 albo tymczasowo przełączam na retain-on-failure.

reporter: 'html' daje statyczny raport z listą testów, krokami i podpiętymi artefaktami. Otwieram go przez npx playwright show-report.

Pierwszy test na runnerze Playwrighta, nie na Jeście

Pierwsza rzecz, którą warto powiedzieć wprost, bo mnie samego myliła: @playwright/test to własny runner. Nie Jest, nie Mocha, nie Cypress. Import jest jednoznaczny:

import { test, expect } from '@playwright/test'

test i expect przychodzą z tej samej paczki co przeglądarka. Nie konfiguruję środowiska Jesta, nie szukam adaptera, nie mam dwóch expect z różnych bibliotek w jednym pliku. Jeżeli w projekcie żyją testy jednostkowe na Jeście, to jest osobny runner na osobnych plikach i nie chcę ich mieszać.

Mój tests/smoke.spec.ts wygląda tak:

import { test, expect } from '@playwright/test'

test.describe('smoke', () => {
  test('strona demo TodoMVC się otwiera', async ({ page }) => {
    await page.goto('/todomvc/')

    await expect(page).toHaveTitle(/TodoMVC/)
    await expect(page.locator('.new-todo')).toBeVisible()
  })

  test('dodane zadanie pojawia się na liście', async ({ page }) => {
    await page.goto('/todomvc/')

    await page.locator('.new-todo').fill('napisać config')
    await page.locator('.new-todo').press('Enter')

    await expect(page.locator('.todo-list li')).toHaveText(['napisać config'])
  })
})

Aplikacja to publiczne demo demo.playwright.dev/todomvc, nie żaden projekt, nad którym pracuję. Na etapie sprawdzania, czy runner w ogóle chodzi, celowo nie chcę aplikacji z logowaniem, bo wtedy pierwsza czerwona rzecz będzie o sesji, a nie o konfiguracji.

Warte podkreślenia są dwa szczegóły składni. Po pierwsze, page przychodzi jako fixture w argumencie testu, a nie jako globalny obiekt. Każdy test dostaje własny kontekst przeglądarki, więc ciasteczka i localStorage nie przeciekają między przypadkami. To ta sama zasada, o którą walczyłem w testach .NET: izolacja ma być domyślna, a współdzielenie jawne.

Po drugie, expect jest web-first. await expect(locator).toHaveText([...]) samo ponawia sprawdzenie do momentu timeoutu z expect.timeout, więc nie ma tu miejsca na ręczny sleep. Asercja na tablicy sprawdza jednocześnie liczbę elementów i ich treść, co jest wygodniejsze niż dwa osobne warunki.

Uruchomienie:

$ npx playwright test
$ npx playwright test --project=chromium --headed
$ npx playwright show-report

Pierwsza komenda odpala sześć przypadków, bo dwa testy razy trzy projekty. Druga zawęża do jednego silnika i pokazuje okno przeglądarki, kiedy chcę zobaczyć, co się dzieje.

Selektory w tych testach są celowo proste: klasa i tekst. Wiem, że 1.22 wyprowadza role selectors z czysto eksperymentalnej flagi i że da się dziś pisać wyłącznie po rolach dostępnościowych. Nie robię tego w pierwszym tygodniu, bo zmiana strategii lokatorów to osobna decyzja, którą chcę podjąć z całym zespołem i na prawdziwej aplikacji, a nie przy okazji sprawdzania, czy npx playwright test w ogóle wystartuje.

Codegen, czyli generator do wyrzucenia

npx playwright codegen otwiera przeglądarkę i Inspector, który zapisuje moje kliknięcia jako kod:

$ npx playwright codegen https://demo.playwright.dev/todomvc/

Sam Inspector nie jest nowy, jest w narzędziu od 1.9.0 z lutego zeszłego roku. Nowe jest to, jak go używam. Kiedy w Cypressie zaczynałem od dokumentacji selektorów, tutaj zaczynam od kliknięcia w element i sprawdzenia, jaki lokator zaproponuje narzędzie. Wynik z klikania po TodoMVC wygląda mniej więcej tak:

import { test, expect } from '@playwright/test'

test('test', async ({ page }) => {
  await page.goto('https://demo.playwright.dev/todomvc/')
  await page.locator('.new-todo').click()
  await page.locator('.new-todo').fill('napisać config')
  await page.locator('.new-todo').press('Enter')
})

I tu jest sedno: to nie jest test. To jest nagranie. Nazwa test, pełny URL zamiast baseURL, zbędny click() przed fill(), zero asercji. Traktuję codegen jak podpowiedź do lokatorów, przeklejam z niego dwie linijki i resztę wyrzucam. Widziałem podejście, w którym generator jest źródłem całej suite, i kończy się tak samo jak nagrywarki sprzed dekady: setki linii, których nikt nie umie poprawić po zmianie layoutu.

Jest jeszcze jedno zastosowanie tego samego Inspectora, które lubię bardziej niż generowanie od zera. Wstawiony w środek testu await page.pause() zatrzymuje przebieg na konkretnym kroku i otwiera to samo okno, w którym mogę wyklikać dalszy ciąg na już zalogowanej i przewiniętej stronie. To zamienia debugowanie z “dopisz linijkę, uruchom całość od początku, czekaj” na coś interaktywnego i przy pierwszym kontakcie z narzędziem oszczędza sporo czasu.

Trace Viewer w maju 2022

Trace jest głównym powodem, dla którego w ogóle robię ten projekt, więc należy mu się akapit bez marketingu. Trace Viewer nie jest nowością tego miesiąca. GUI jest od 1.12.0 z czerwca zeszłego roku, razem z paskiem czasu i klatkami ze zrzutów kolejnych akcji. Wersja online, na którą przeciąga się plik, jest od 1.17.0 z końca listopada.

Praktycznie wygląda to tak. Po czerwonym przebiegu z retry w test-results/ leży katalog na test, a w nim trace.zip. Otwieram go lokalnie:

$ npx playwright show-trace test-results/smoke-smoke-dodane-zadanie-pojawia-sie-na-liscie-chromium/trace.zip

Albo, jeżeli plik przyszedł z cudzego przebiegu, wrzucam go na trace.playwright.dev. To jest PWA, która przetwarza plik lokalnie w przeglądarce, więc nie wysyłam nagrania cudzej aplikacji na cudzy serwer. To rozróżnienie ma znaczenie, kiedy trace zawiera dane z prawdziwego środowiska.

W środku dostaję listę akcji, zrzut DOM przed i po każdej z nich, konsolę, ruch sieciowy i pasek czasu. Najważniejsze jest to, że zrzut DOM jest klikalny: mogę najechać na element w momencie, w którym test go nie znalazł, i sprawdzić, czy go tam nie było, czy był przykryty, czy miał inny atrybut. Tego nie da mi żaden screenshot ani log.

Ostatnia rzecz w tej sekcji jest bardziej o dyscyplinie niż o narzędziu. Kuszące jest ustawienie trace: 'on' i “niech się nagrywa zawsze”. Przy jednym projekcie i dwóch testach to niewidoczne. Przy trzech przeglądarkach i suite, którą chcę mieć za pół roku, to dziesiątki megabajtów na przebieg i wolniejszy każdy zielony test. on-first-retry daje mi dowód dokładnie tam, gdzie jest potrzebny, czyli przy pierwszej porażce.

Trzy przeglądarki bez pętli w teście

W projects siedzi ta część kwietniowej decyzji, którą najtrudniej było mi wcześniej dowieźć. Ten sam plik .spec.ts idzie na Chromium, Firefoksa i WebKit, bo macierz jest w konfiguracji, a nie w kodzie. W testach nie ma ani jednego if po nazwie przeglądarki i nie ma pętli po silnikach.

$ npx playwright test --project=webkit

O WebKicie trzeba powiedzieć uczciwą rzecz, bo łatwo tu o nadinterpretację. WebKit na Linuksie to zbudowany przez zespół Playwrighta build silnika, a nie zainstalowane Safari. Wyłapię na nim różnice w silniku renderującym i w API, których Chromium nie pokaże, ale nie jest to dowód, że aplikacja działa na Safari na iPhonie konkretnej wersji. Traktuję ten projekt jako wczesne ostrzeżenie, nie jako certyfikat zgodności. Analogicznie devices['Desktop Safari'] to profil z rozmiarem okna i user agentem, a nie prawdziwe urządzenie.

Druga uczciwa rzecz to koszt. Trzy projekty to trzy razy tyle przypadków i mniej więcej trzy razy tyle czasu. Docelowo nie zamierzam puszczać wszystkiego wszędzie: smoke pójdzie na trzy silniki, reszta na Chromium, a Firefox i WebKit dostaną pełny przebieg nocą. Na dziś, przy dwóch testach, zostawiam macierz w komplecie, bo różnica to kilkanaście sekund, a chcę od początku widzieć, czy któryś silnik odstaje.

Jeżeli aplikacja pod testami stoi lokalnie w kontenerach, nic w tym configu się nie zmienia poza BASE_URL. Playwright sam z siebie Dockera nie wymaga i nie zamierzam go tu wciągać na siłę. Środowisko podnoszę tak jak przy wpisie o środowisku testowym na Compose, z tą jedną różnicą, że od zeszłego miesiąca Compose V2 jest GA i komenda to docker compose, bez myślnika.

Czego świadomie tu nie ma

Kilka rzeczy leżało w zasięgu ręki i celowo ich nie ruszyłem. Wypisuję je, żeby za pół roku było wiadomo, że to były decyzje, a nie przeoczenia.

Component testing. Ten sam release 1.22.0 przynosi paczki @playwright/experimental-ct-react i ich odpowiedniki dla Vue i Svelte. To jest preview, wprost oznaczony jako eksperymentalny, i nie chcę na nim budować niczego, co ma przetrwać kilka wersji. Kiedy dojrzeje, będzie osobnym tematem.

Visual regression. W wydaniu z 12 maja doszła web-first asercja na screenshotach i to jedno zdanie jest wszystkim, co dziś o niej napiszę. Porównywanie obrazków to rozmowa o progach tolerancji, o różnicach między maszynami i o tym, kto zatwierdza nowe wzorce, więc zostawiam ją na osobny wpis.

Pipeline. Ten wpis kończy się na npx playwright test na moim laptopie. YAML zostawiam na później, świadomie, bo najpierw chcę mieć suite wartą uruchamiania. Kiedy przyjdzie ten moment, punktem wyjścia będzie npx playwright install --with-deps na agencie i doświadczenia z Cypressa na CI, a nie kopiowanie cudzego workflow.

Architektura testów. Nie ma tu page objectów, fixture’ów własnych, warstwy danych ani strategii logowania. Dwa testy nie uzasadniają żadnej z tych rzeczy, a wybór między page objectami a app actions raz już rozstrzygałem w kontekście Cypressa i wiem, że to rozmowa na cały wpis.

Usunięcie Cypressa. Nic nie znika. Cypress 9.6.1 dalej stoi obok i będzie stał, dopóki Playwright nie pokryje tego, co tamta suite realnie sprawdza. Migracja, w której najpierw kasuje się stare testy, to nie migracja.

Podsumowanie

Po tygodniu z pustym projektem mam trzy wnioski i żaden z nich nie jest o tym, że Playwright jest lepszy od czegokolwiek.

  • Wersję przypinam od pierwszego commita. @playwright/test w wersji 1.22.0 ciągnie za sobą konkretne buildy Chromium, Firefoksa i WebKit, więc daszek w package.json oznacza cichą zmianę przeglądarek w środku sprintu.
  • Ciężar konfiguracji przesuwa się z testu do configu i to jest największa zmiana nawyku wobec tego, jak stawiałem środowisko pod Cypressa. Równoległość, retry, artefakty i macierz przeglądarek to cztery pola w jednym pliku, a test zostaje krótki.
  • Trace jest wart więcej niż liczba silników. Trzy przeglądarki dają mi szerszy zasięg, ale to możliwość otwarcia nagrania nieudanego przebiegu decyduje o tym, czy czerwony wynik zamienię w poprawkę, czy w ponowne uruchomienie.

Czego nie wiem po tym tygodniu: czy fullyParallel da się u mnie włączyć bez przepisywania danych testowych, i ile z obecnej suite Cypressa w ogóle warto przenosić, zamiast napisać od nowa albo skasować. To drugie pytanie jest zresztą starsze niż wybór narzędzia i wraca za każdym razem, gdy myślę o całym procesie testowym, a nie o pojedynczym frameworku. Narzędzie zmieniło się w maju. Pytanie, co warto sprawdzać, zostało to samo.