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 collect
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/workflowszwraca 200) ischema-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_onzservice_healthy. Wszystko co trzeba.scripts/e2e-build.sh— buduje FE, wrzuca dodevset-ce-be/src/main/resources/static, robi./gradlew bootJar. Wychodzi jeden artefakt, dokładnie taki jak ten zghcr.io/devset-io/devset-ce:latest..github/workflows/e2e.yml— odpala się na PR-ach i na pushu domain. 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.jsoncztery skrypty:e2e:build,e2e:up,e2e:down,e2e:full. Lokalnie odpalamnpm run e2e:fulli 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
mainjest 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ć.