To nie jest premiera Playwrighta
Zacznę od zdania, które trzeba postawić na początku, bo w rozmowach wraca zwykle w odwrotnej wersji: Playwright nie jest nowy. Pierwsze tagowane wydanie, v0.10.0, jest z 1 lutego 2020, a 1.0.0 z 6 maja 2020. Narzędzie ma dwa lata, ma za sobą dwadzieścia jeden wydań minor od 1.0 i dawno wyszło z etapu, na którym API zmienia się co miesiąc.
Nowa jest tylko moja decyzja. Serię o Cypressie zaczynałem od przygotowania środowiska i pierwszych testów jesienią 2019, a skończyłem na pipeline na Azure DevOps i raportowaniu wyników wiosną 2020. Cały 2021 poszedł u mnie na .NET, kontenery i CI, więc w podsumowaniu roku słowo “Playwright” nie padło ani razu. Teraz wracam do testów przez przeglądarkę i pytanie brzmi konkretnie: dopisuję kolejne spec’i do Cypressa, czy zakładam obok drugi projekt na @playwright/test.
Wersje, na których stoi ten wpis, są świeże z zeszłego tygodnia. Cypress 9.5.4 wyszedł 11 kwietnia, playwright i @playwright/test 1.21.0 dzień później, 12 kwietnia. Node mam na 16 LTS. Aplikacja do dema jest ta sama publiczna Conduit z projektu RealWorld, na której chodzi cała seria Cypress, więc porównuję dwa runnery na tych samych scenariuszach logowania, a nie na dwóch różnych systemach. Podnoszę ją nadal przez docker-compose, bo Compose V2 ma dostać GA jeszcze w tym miesiącu i dopiero wtedy z komendy zniknie myślnik.
I od razu granica tego tekstu: to jest lista powodów, a nie raport z zakończonej migracji. Nie mam przepisanej suite, nie mam liczby “ile procent gotowe” i nie zamierzam takiej liczby wymyślać. Mam decyzję, że nowe testy przez przeglądarkę piszę od teraz w Playwright, a stare zostają tam, gdzie są.
Co mi w Cypressie nadal pasuje
Zanim wypiszę powody zmiany, wypiszę powody, dla których dwa i pół roku temu wybrałem Cypressa i dla których nadal go bronię, kiedy ktoś nazywa go przestarzałym.
Test Runner z time-travel jest do dzisiaj najlepszym lokalnym doświadczeniem debugowania, jakie widziałem w testach webowych. Klikam krok na liście po lewej, a po prawej mam stan DOM z tamtej sekundy. Nie odtwarzam scenariusza, tylko oglądam go w miejscu, w którym się wywrócił.
Komunikaty błędów są napisane po ludzku. W poście o CI wkleiłem output, w którym Cypress wprost napisał, że nie widzi serwera pod baseUrl i że spróbuje jeszcze trzy razy. Nie musiałem zgadywać, czy problem jest w teście, w aplikacji, czy w agencie.
Custom commands z refaktoru App Actions vs Page Object Model nadal uważam za dobry pomysł, a nie za obejście braku struktury. cy.login() i cy.createNewUserAPI() są czytelne w teście i tanie w utrzymaniu, dopóki ktoś pilnuje, żeby plik commands.js nie stał się wysypiskiem.
Automatyczne czekanie na element też zostaje po stronie plusów. Cypress ponawia zapytanie o element i asercję aż do timeoutu, dzięki czemu cy.get() bez jawnego wait zwykle po prostu działa. To była w 2019 roku różnica klasy wobec Selenium, w którym Thread.Sleep wyrastał w kodzie jak chwast.
Nic z tego nie znika i nic z tego nie kasuję z bloga. Cypress 9.5.4 zostaje w repozytorium na testach, które już działają, a seria od /cypress-0/ do /cypress-9/ zostaje tym, czym była. Zmieniam narzędzie dla nowej pracy, nie ogłaszam pogrzebu.
Trzy silniki, jeden API
Pierwszy powód jest najbardziej mierzalny. Playwright 1.21.0 przywozi ze sobą trzy silniki: Chromium 101, Firefox 98.0.2 i WebKit 15.4. To nie są przeglądarki z mojego systemu, tylko buildy pobierane przez npx playwright install, więc na moim laptopie i na agencie CI mam dokładnie te same wersje.
$ npx playwright install --with-deps
$ npx playwright test --project=webkitWebKit jest tu sednem. W poście o cross-browser testingu cieszyłem się z Firefoksa i Edge, które Cypress dowiózł w wersji 4.0.1 w lutym 2020. Cieszyłem się słusznie, ale ta lista od tamtej pory wygląda podobnie: silnik Chromium w kilku wariantach plus Firefox. Safari na moim biurku nie ma i nie będzie, bo pracuję na Linuksie, a użytkownicy iPhone’ów istnieją niezależnie od mojego sprzętu. Playwright daje mi silnik WebKit na Linuksie i to jest rzecz, której w Cypressie 9.5.4 nie kupię za żadne pieniądze.
Druga różnica jest w tym, jak przeglądarki się instalują. W lutym 2020 musiałem doinstalować Firefoksa i Edge na dysk, a Test Runner potrafił się przy tym zawiesić, o czym pisałem wtedy dość szczerze. Tutaj przeglądarki są zależnością projektu i wersjonują się razem z paczką. Kiedy CI robi zielony build, a lokalnie test pada, mogę wykluczyć wersję przeglądarki z listy podejrzanych.
Nie przesadzam z tym argumentem. Realnie większość defektów łapię w Chromium, a WebKit dokłada garść różnic w renderowaniu i w API dat. Chodzi o to, że decyzja “czy w ogóle sprawdzamy Safari” przestaje być decyzją o zakupie sprzętu, a staje się jedną linijką w konfiguracji.
Runner: @playwright/test, nie Jest i nie Mocha
Drugi powód jest mniej widowiskowy i ważniejszy. Cypress ma Mocha wbudowaną na stałe. To wygodne na starcie, ale runner jest częścią narzędzia i nie podlega negocjacji: dostaję taki podział na pliki, takie hooki i taką równoległość, jaką ktoś zaprojektował.
Playwright długo miał ten sam problem od drugiej strony. Przez pierwszy rok standardową ścieżką było wpięcie biblioteki w Jest, z osobnymi paczkami i konfiguracją, którą trzeba było skleić samemu. Ten przepis krąży po internecie do dzisiaj i nie polecam go jako punktu startu w 2022 roku. Od 1.12.0, czyli od czerwca 2021, jest oficjalny runner @playwright/test i to on jest w kwietniu 2022 domyślną drogą, a nie eksperymentem.
Konfiguracja całości mieści się w jednym pliku:
import { PlaywrightTestConfig, devices } from '@playwright/test'
const config: PlaywrightTestConfig = {
testDir: './tests',
timeout: 30_000,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 4 : undefined,
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:4100',
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 configInteresują mnie w nim dwie rzeczy. projects to ten sam zestaw testów puszczony w kilku wariantach, więc cross-browser nie wymaga trzech pipeline’ów ani kopiowania plików. workers to równoległość w jednym procesie, na jednej maszynie, bez dodatkowej usługi.
Ten drugi punkt jest miejscem, w którym porównanie robi się nieprzyjemne dla Cypressa. Lokalnie i na jednym agencie Playwright rozdziela pliki testowe między procesy workerów i tyle. W Cypressie równoległość na CI idzie przez flagę --record i Dashboard, czyli przez usługę z kluczem, darmowym limitem nagrań i płatnymi planami wyżej. To jest sensowny model biznesowy i nie mam o niego pretensji, ale kiedy porównuję koszt przyspieszenia przebiegu, to po jednej stronie mam wpis w konfiguracji, a po drugiej rozmowę o budżecie.
Poziomy równoległości rozpisywałem w październiku na .NET i NUnit i tamten podział na wątki frameworka, procesy na maszynie i joby pipeline’u obowiązuje tu tak samo. Zmienia się tylko to, że warstwę środkową dostaję w paczce.
Do tego dochodzą fixtures runnera, czyli mechanizm wstrzykiwania kontekstu do testu przez argumenty funkcji. Bazowy page to najprostszy przykład:
import { test, expect } from '@playwright/test'
test('poprawne logowanie', async ({ page }) => {
await page.goto('/login')
await page.locator('input[type="email"]').fill('test@test.com')
await page.locator('input[type="password"]').fill('test')
await page.locator('button[type="submit"]').click()
await expect(page).toHaveURL('http://localhost:4100/')
await expect(page.locator('a[href="/settings"]')).toBeVisible()
})Dla porównania ten sam scenariusz z serii Cypress wyglądał tak, po tym jak przeniosłem logowanie do custom command:
it('Successfull login', function () {
cy.createNewUserAPI('test', 'test@test.com', 'test')
.visit('http://localhost:4100/login')
.login('test@test.com', 'test')
.shouldUrlContain('http://localhost:4100/')
})Kod jest podobnie długi i podobnie czytelny, więc nie o to toczy się gra. Różnica jest w tym, co siedzi pod spodem: w Cypressie łańcuch cy jest kolejką komend, a w Playwright to zwykły async/await w TypeScripcie. Debugger się nie gubi, try/catch działa tak, jak się spodziewam, a pętla po danych testowych nie wymaga tłumaczenia, kiedy właściwie wykona się ciało pętli. Dla kogoś, kto pisze na co dzień w C#, to jest wygodniejszy model umysłowy niż kolejka.
Drobiazg z tego tygodnia, ale przydatny: 1.21.0 dokłada expect.poll, czyli asercję ponawianą na dowolnej funkcji, nie tylko na lokatorze. Do sprawdzania stanu w API po akcji w UI to jest dokładnie to, czego brakowało.
Izolacja kontekstu i origin
Trzeci powód dotyczy tego, co widzi test na starcie. Playwright dla każdego testu tworzy nowy BrowserContext, czyli osobny profil w tym samym procesie przeglądarki: puste ciasteczka, puste localStorage, własna pamięć podręczna. Koszt to milisekundy, bo to nie jest nowy proces przeglądarki.
Konsekwencja jest praktyczna. Test, który zaczyna od zalogowania, nie może przypadkiem zaliczyć się dlatego, że poprzedni test zostawił sesję. Odwrotnie też: logowanie przez UI w każdym teście przestaje być koniecznością, bo stan sesji da się zapisać raz i podać kolejnym kontekstom przez storageState. To ta sama zasada, którą rozpisywałem przy organizacji fixture w .NET, tylko obiektem jest tu profil przeglądarki, a nie sterownik.
Druga rzecz to origin. Cypress uruchamia testy wewnątrz tej samej karty co aplikację i wynika z tego twarde ograniczenie: jeden superdomain na test. Wszystko, co przechodzi na inną domenę, czyli logowanie przez zewnętrznego dostawcę tożsamości, przekierowanie do bramki płatności czy klik w link do dokumentu na innym hoście, wymaga obejścia. Zwykle kończy się to strzałem w API zamiast przejścia przez ekran, co jest rozsądne, ale testuje coś innego niż to, co robi użytkownik.
Twórcy Cypressa pracują nad tym otwarcie i w repozytorium widać, że obsługa wielu originów ma trafić do narzędzia za flagą eksperymentalną. W wersji 9.5.4, którą mam dzisiaj zainstalowaną, tego po prostu nie ma, więc nie mogę wpisać tego do kolumny plusów. Nie twierdzę też, że Cypress nigdy tego nie ugryzie, bo najwyraźniej ugryzie.
W Playwright wieloorigin nie jest funkcją, tylko brakiem ograniczenia. page.goto() idzie pod dowolny adres, w jednym teście mogę mieć kilka kart i kilka kontekstów, a okno popup po kliknięciu w przycisk logowania jest zwykłym obiektem Page. Sterowanie odbywa się z procesu Node przez protokół, a nie z wnętrza strony, i cała różnica bierze się właśnie stąd.
Debug na CI: trace, nie tylko screenshot
Czwarty powód jest tym, który przeważył. Lokalnie GUI Cypressa wygrywa i napisałem to wyżej bez zastrzeżeń. Problem w tym, że najdroższe godziny nie schodzą mi na debugowaniu testu na laptopie, tylko na ustalaniu, dlaczego coś padło na agencie o trzeciej w nocy.
W konfiguracji z Azure DevOps i raportowania mam z takiego przebiegu wideo i screenshot z momentu porażki. To lepsze niż sam stack trace, ale wideo to obraz, a nie dane. Nie sprawdzę w nim, jaki request poszedł do API, co przyszło w odpowiedzi ani jak wyglądał DOM dwa kroki wcześniej.
Trace Viewer istnieje od 1.12.0 z czerwca 2021, więc też nie jest nowością tej wiosny, ale dopiero teraz mam powód, żeby na niego popatrzeć poważnie. Trace to plik ZIP z osi czasu przebiegu: zrzut DOM przed i po każdej akcji, screenshoty, ruch sieciowy, konsola i źródło testu. Od 1.17.0 z listopada 2021 da się go otworzyć w przeglądarce na trace.playwright.dev, bez instalowania czegokolwiek, a od linii 1.20 widać w nim także requesty API wykonane z testu.
Włącza się to jedną opcją, którą wpisałem już w konfigurację wyżej:
use: {
trace: 'on-first-retry',
}Ustawienie on-first-retry jest tu świadome. Nagrywanie wszystkiego kosztuje czas i miejsce na artefakty, a interesują mnie przebiegi niestabilne, czyli te, które padły i zostały ponowione. Dokładnie te, przy których zwykle nie ma jak odtworzyć sytuacji lokalnie.
$ npx playwright show-trace trace.zipTo jest ten sam rodzaj wygody co time-travel w Cypressie, tylko przeniesiony z mojego laptopa na agenta. I to jest dla mnie właściwe miejsce dla takiego narzędzia.
Jednej rzeczy z tej sekcji świadomie nie ruszam: porównania obrazków. Wizualną regresję robiłem w serii Cypress wtyczką i to jest osobny temat, do którego wrócę osobnym wpisem. Tutaj expect().toMatchSnapshot() i page.screenshot() wystarczają mi do artefaktów, a nie do budowania strategii.
Codegen i Inspector
Piąty powód jest miękki, ale realny przy starcie. npx playwright codegen otwiera przeglądarkę i okno Inspectora, a wszystko, co klikam, ląduje w gotowym kodzie testu. Inspector jest w narzędziu od 1.9.0 z lutego 2021, a polecenie siedzi w CLI głównej paczki, więc nie trzeba szukać żadnego osobnego pakietu.
$ npx playwright codegen http://localhost:4100/loginDo tego jest tryb debugowania krok po kroku, który zatrzymuje wykonanie przed każdą akcją i podświetla element:
$ PWDEBUG=1 npx playwright test tests/login.spec.tsDo czego to realnie służy? Do nauki API i do szybkiego szkicu, a nie do produkcji testów. Selektory z nagrywarki wyglądają jak te z moich pierwszych postów o Cypressie, czyli :nth-child(1) > .form-control, i psują się przy pierwszej zmianie layoutu. Ten sam problem opisywałem pod koniec 2019 roku, kiedy dokładałem do aplikacji atrybuty data-cy, i nagrywarka go nie rozwiązuje, tylko przyspiesza dojście do momentu, w którym trzeba go rozwiązać samemu.
Warto też wiedzieć, czego w 1.21 jeszcze nie ma jako domyślnej ścieżki. Selektory po roli dostępności są w tej wersji eksperymentem za zmienną PLAYWRIGHT_EXPERIMENTAL_FEATURES i nie zamierzam budować na nich konwencji projektu, dopóki nie wyjdą z tego trybu. Na razie zostaję przy atrybutach testowych na elementach, czyli przy tej samej dyscyplinie co w Cypressie.
Czego nie obiecuje ten post
Trzy zastrzeżenia, żeby nikt nie wyszedł stąd z wnioskiem, którego nie postawiłem.
Nie mam gotowej suite w Playwright. Mam przeczytane release notes, zainstalowaną paczkę i wypisane powody. Następny krok jest mały i konkretny: pusty projekt, konfiguracja, jeden test logowania na Conduit i sprawdzenie, ile z powyższych obietnic zostaje po zderzeniu z rzeczywistością. O tym będzie następny wpis.
Nie migruję istniejących testów Cypress na siłę. Zestaw, który działa i jest zielony, nie jest długiem technicznym tylko dlatego, że powstał w innym narzędziu. Przepisywanie go bez powodu kosztuje czas i wprowadza defekty w miejsca, które od dwóch lat nikogo nie budziły. Cypress 9.5.4 zostaje na tym, co ma, a Playwright bierze nowe obszary.
Nie podam procentu. Widuję posty w stylu “przepisaliśmy siedemdziesiąt procent suite w dwa tygodnie” i za takimi liczbami zwykle nie stoi nic, co dałoby się zweryfikować. Kiedy będę miał wynik, opiszę, co konkretnie działa, a co mnie zaskoczyło.
Podsumowanie
Zestawienie, do którego doszedłem po tygodniu czytania i kilku godzinach klikania:
- Playwright
1.21.0biorę do nowych testów przez przeglądarkę, przede wszystkim za trzy silniki z jednego API, zaBrowserContextna test i za trace z agenta CI. - Cypress
9.5.4zostaje tam, gdzie już jest. Lokalne GUI z time-travel nadal nie ma odpowiednika, a custom commands z refaktoru z 2019 są w tych testach dobrą strukturą, a nie długiem. @playwright/testjest w kwietniu 2022 domyślnym runnerem tego narzędzia i to zamyka temat sklejania Playwrighta z Jest. Fixtures,projectsi workery w jednej konfiguracji to argument osobny od samego API przeglądarki.- Wieloorigin i izolacja sesji są dziś po stronie Playwrighta. To może się zmienić i najwyraźniej się zmienia, więc traktuję to jako stan na dziś, a nie jako wyrok.
Czego tu nie ma i czego celowo nie zmieniam: procesu. Wybór runnera nie odpowiada na pytanie, które scenariusze w ogóle zasługują na test przez przeglądarkę, a które powinny skończyć na poziomie API. To jest osobna rozmowa, którą prowadzę od 2019 roku i której żadna biblioteka za mnie nie odbędzie. Nowe narzędzie zmienia koszt uruchomienia testu, a nie sens tego, co ten test sprawdza.

