author-avatar
Dominik Martyniak
Playwright 9 minut

Playwright + Docker: jak postawiłem E2E w Devset CE

Przed każdym merge'em do main miałem ten moment: „no, chyba nic nie pękło". Unity zielone, integracyjne zielone, klikam manualnie po UI , działa. A i tak gdzieś z tyłu głowy ten mały głosik: „a sprawdziłeś dispatch po ostatniej zmianie w collectionContext? a save schemy z Ace? a routing SPA po build do jara?".

W końcu mi się znudziło. Wszedłem na branch ci-add-playwright-e2e-gating-main-with-bundled-FE+BE (tak, nazwy moich branchy to osobny temat) i napisałem porządne E2E. Playwright, Docker, gate na PR-ach. Krótka historia jak to wygląda i gdzie się przejechałem.

Co weszło

Po krótce:

  • Playwright w devset-ce-fe/e2e/ — na razie dwa pliki: smoke.spec.ts (SPA się montuje, /api/workflows zwraca 200) i schema-repo.spec.ts (twórz JSON-a, twórz protobufa, edytuj body, zrób reload, sprawdź czy się zapisało).
  • docker-compose.e2e.yml — Kafka, RabbitMQ, backend z osadzonym frontendem i kontener Playwrighta. Healthchecki, pinowane SHA, depends_on z service_healthy. Wszystko co trzeba.
  • scripts/e2e-build.sh — buduje FE, wrzuca do devset-ce-be/src/main/resources/static, robi ./gradlew bootJar. Wychodzi jeden artefakt, dokładnie taki jak ten z ghcr.io/devset-io/devset-ce:latest.
  • .github/workflows/e2e.yml — odpala się na PR-ach i na pushu do main. Cancel-in-progress dla PR-ów (no, jak ktoś pushuje 10 commitów w 5 minut, to nie chcę 10 runów). Trace upload przy failu.
  • W package.json cztery skrypty: e2e:build, e2e:up, e2e:down, e2e:full. Lokalnie odpalam npm run e2e:full i mam to samo co CI. Bez „u mnie działa".

Przy okazji wywaliłem stary docker-compose.yml z roota. Był tam zombi z czasów, gdy projekt miał inną strukturę, wskazywał na nieistniejący folder devset-ce/, a README opisywał obejście dla obejścia. Lepiej nie mieć niż mieć coś, co kłamie.

Dlaczego Docker, a nie po prostu webServer Playwrighta

Pierwsza myśl była najprostsza: Playwright ma w configu webServer, podnieś backend Gradle'em, Kafkę przez Testcontainers, i jedziesz. Wybiłem to sobie z głowy po jakichś 15 minutach, bo:

Backend nie startuje „od razu". Spring Boot musi się zbootować, dogadać z Kafką i Rabbitem, dopiero wtedy /api/* jest faktycznie żywe. sleep 10 w configu? Nie, dziękuję. W docker-compose mam healthchecki i condition: service_healthy  i to się po prostu... dzieje samo.

Chcę żeby lokalnie i w CI było identycznie. Nie 95% identycznie, nie „no prawie". Jeden compose, jeden skrypt, dwa miejsca uruchomienia. Jak pęka w CI, robię npm run e2e:full u siebie i odtwarzam fail w minutę. To jest waluta, którą płacisz raz, a wraca codziennie.

Wersja przeglądarki idzie razem z Playwrightem. Obraz mcr.microsoft.com/playwright:v1.59.0-noble ma już chromium dopasowane do @playwright/test@1.59.0. Nikt nie zapomni o npx playwright install, nikt nie debug-uje „dlaczego u Janka działa".

No i  testuję prawdziwy artefakt. Nie devowy Vite z proxy do backendu, tylko jar, który serwuje frontend ze static/. Łapię regresje w CORS, w SPA routingu, w mapowaniu resources. Mock-server tego nie wyłapie, bo tych ścieżek po prostu nie odpala.

Cena? Pierwszy run jest wolniejszy, ale z cache npm/Gradle w GitHub Actions cały workflow mieści się w jakichś 6-8 minutach. Akceptowalne.

Z czym się męczyłem (i co mnie zaskoczyło)

Trzy rzeczy zjadły mi sensownie czasu. Spisuję, bo następnym razem chcę to mieć pod ręką.

Ace editor. Schemy JSON edytuje się w Ace. Ace renderuje swój własny DOM, ma ukryty textarea, page.fill() jest losowy, keyboard.type() powolny i wrażliwy na keymap. Zacząłem od tego, że spróbowałem trzech „normalnych" podejść — żadne nie było stabilne. Dopiero gdy wszedłem na devtoolsy i poklikałem po wnętrznościach Ace'a, zobaczyłem że trzyma swój Editor pod containerElement.env.editor (używa tego sam, jako re-entry guard w ace.edit()). Wjeżdżam tam przez page.evaluate() i wołam editor.setValue(value, -1). Brzydkie? Brzydkie. Ale działa za każdym razem. Dorzuciłem // SAFETY: żeby ten przyszły ja, który przyjdzie i zobaczy as unknown as, nie wywalił tego z „no co to ma być".

SQLite mnie pogonił. Devset trzyma stan w SQLite. Playwright domyślnie odpala testy w jednym pliku równolegle. SQLite serializuje cały zapis - jak trzy testy naraz robią DELETE, dwa dostają SQLITE_BUSY i jest czerwono. Próbowałem retry, mocniejszego cleanup, kombinacji — i w końcu odpuściłem: test.describe.configure({ mode: 'serial' }) na pliku ze schemami. Wolniej, ale 100% zielone. Retry z backoffem maskuje problem, nie rozwiązuje go.

getByRole robi substring match. Czyli getByRole('button', { name: 'Edit' }) mi się odpalał i strict-mode violation: „found 2 elements". Bo w sidebarze był węzeł, którego ID zawierało słowo „edit" (mój test edycji nazwał schemę e2e_json_edit_…, no genialnie). Lekcja: exact: true wszędzie, gdzie znam pełny tekst. Powinno być default, ale nie jest, trudno.

Cleanup po failach. Testy E2E na współdzielonym backendzie mają to do siebie, że jak coś padnie w środku, zostają śmieci. Każdy test dorzuca swoje stworzone ID do listy, w afterEach lecę Promise.all z DELETE-ami w trybie best-effort (.catch(() => undefined)). Następny run zawsze startuje czysty.

Co z tego mam

  • Merge do main jest gated. Czerwony E2E = nie wejdzie. Koniec z „ufam, że nic nie pękło".
  • Trace z Playwrighta przy failu. Ściągam zip z artefaktów, otwieram, mam screen + nagranie + network. Debug zdalnego fail-a to teraz pięć kliknięć, nie godzina śledztwa.
  • Testuję ten sam jar, który idzie na produkcję. Frontend zaszyty w backend, dokładnie tak jak w obrazie z GHCR. Jak coś działa lokalnie i pęka po release  to już nie wina E2E.
  • Lokalnie jedna komenda: npm run e2e:full. Nowy kontrybutor - git clone, npm run e2e:full, idzie kawę zrobić, wraca i widzi wynik. Próg wejścia spadł.

Co dalej

Smoke + schema repo to dopiero początek. Na liście: workflows (twórz / edytuj / odpal), message dispatch z collectionContext (świeży feature, idealny moment żeby to zacementować), connection management dla Kafki i Rabbita. Każdy nowy ekran dostaje swój spec - to teraz część „feature gotowy", nie opcjonalny dodatek na potem.

Jak któryś z tych testów zacznie być flaky, będzie kolejny wpis. Bo flaky test, którego nikt nie naprawia, to gorzej niż brak testu - uczy zespół ignorować czerwony status, a to nawyk, z którego się ciężko wyleczyć.

1
[playwright]

Więcej od Dominik Martyniak

Więcej artykułów