devops: add Playwright E2E job to CI for stammbaum spec #363
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
frontend/e2e/stammbaum.spec.tswas committed in PR #360 but cannot run in CI because the Gitea Actions runner does not have Playwright Chromium binaries installed and the workflow does not start the full Docker Compose stack.The spec is currently marked
.skip()with a reference to this issue. Once this issue is resolved the skip annotations must be removed.What needs to happen
.gitea/workflows/ci.ymlthat:docker-compose up -d)curl actuator/health)npx playwright install chromium --with-depsnpm run test:e2e(ornpx playwright test e2e/stammbaum.spec.tsscoped to the new spec)test.skip()annotations fromfrontend/e2e/stammbaum.spec.tsReference
frontend/e2e/CLAUDE.mdalready documents the intent:🏗️ Markus Keller — Application Architect
Observations
docs/infrastructure/ci-gitea.mdalready exists with a complete, well-structured E2E job YAML. The issue is essentially asking to copy that spec into the liveci.yml. The design work is done — this is an execution task.docker-compose.ci.ymloverlay already exists and is referenced correctly in the doc (docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d db minio create-buckets). The overlay correctly swaps bind mounts for named volumes (ci_postgres_data,ci_minio_data). This is the right pattern.docker-compose up -d)" but the documented approach inci-gitea.mdcorrectly starts onlydb minio create-buckets, then boots the backend as a JAR directly. This is architecturally cleaner: the backend JAR is already compiled by the preceding Maven step, and running it directly avoids Docker image build overhead in CI.depends_onin the CI doc's approach, and thestammbaum.spec.tstests do not call any OCR endpoints. Omitting OCR is correct — it requires 12 GB RAM, a 120-second start period, and model files. Including it in CI would be prohibitive on the NAS runner.backendservice'sdepends_onindocker-compose.ymlincludesocr-service: condition: service_started. This means a naivedocker compose up -dwould attempt to start the OCR service too. The selectiveup -d db minio create-bucketsapproach in the doc avoids this correctly.minio/minio:latestandminio/mcare unpinned indocker-compose.yml. This is a pre-existing concern that doesn't block this issue but is the kind of drift that causes non-reproducible CI failures over time. Worth a separate ticket.--APP_ADMIN_USERNAME=adminand--APP_ADMIN_PASSWORD=${{ secrets.E2E_ADMIN_PASSWORD }}on the JVM command line. Spring Boot 4 maps--KEY=VALUECLI args correctly to environment properties, so this is valid.Recommendations
ci-gitea.mddesign exactly. Do not start the full Compose stack — start onlydb minio create-buckets, then run the backend JAR directly with environment overrides. This keeps CI under 10 minutes on NAS hardware.E2E_ADMIN_PASSWORDbefore the CI job runs. TheUserDataInitializerseedsAPP_ADMIN_PASSWORDfrom the environment — the secret just needs to match whatauth.setup.tsuses at test time.needs:to bothunit-testsandbackend-unit-testsso it only runs after those pass. This keeps the pipeline fail-fast without redundant work.minio/minio:latest→ pinned tag). Do not bundle that fix into this PR — it's unrelated and risks blocking the E2E work.Open Decisions
on: push / pull_requestmeans it runs on every branch push. Given CI takes longer with E2E, consider restricting topull_requestor adding a path filter to skip it when only docs change. This is a product workflow preference, not a technical constraint.👨💻 Felix Brandt — Fullstack Developer
Observations
stammbaum.spec.tsfile usestest.describe('...', () => { test.skip(); ... })at the describe-block level. This skips all tests in the block unconditionally. Once CI is working, removingtest.skip()will immediately enable all four tests — no other code changes are needed in the spec file./briefwechsel), page heading render, empty-vs-populated state, and the year-range validation error. The soft assertion pattern in the third test (expect(emptyVisible || nodeCount > 0).toBe(true)) is intentional and acceptable — it handles two coherent states without coupling the test to data.auth.setup.tsfile reads credentials fromE2E_USERNAMEandE2E_PASSWORDenv vars with fallback defaults (admin@familyarchive.local/admin123). In CI the password default isadmin123— the same value thatUserDataInitializerseeds whenAPP_ADMIN_PASSWORDis not set. This means the E2E job works even without a Gitea secret configured, though using a hardcoded default password in CI is a smell (see Nora's comment).playwright.config.tswebServerblock startsnpm run dev -- --port 3000and usesreuseExistingServer: true. In CI there will be no pre-running server, so Playwright will start the SvelteKit dev server itself. This requiresnode_modulesto be installed before the test step — the CI doc correctly handles this with a priornpm cistep and cache.test:e2escript isplaywright test— it runs all specs in./e2e, not juststammbaum.spec.ts. Once the skip is removed, all E2E specs run. This is fine if all other specs pass; if they are flaky on the NAS runner, it may make sense to runstammbaum.spec.tsin isolation first to validate the job works before enabling the full suite.Recommendations
test.skip()in the same PR that adds the CI job. The two are tightly coupled — the skip exists only because CI wasn't ready. Leaving the skip in a follow-up creates a separate tracking obligation with no benefit.npx playwright test e2e/stammbaum.spec.tsfirst during development of the CI job, then switch tonpm run test:e2eonce the job is confirmed stable. This shortens the feedback loop significantly on the NAS runner.--project=chromiumto thenpm run test:e2ecall in the CI YAML to be explicit. The config only defines Chromium, so this is defensive but makes intent obvious and guards against someone adding a Firefox project later without CI budget for it.webServerauto-start works withAPI_INTERNAL_URLandAPI_PROXY_TARGETset. In Compose, these are set via Docker env. When Playwright starts the dev server via thewebServerblock, those env vars need to be available in the CI job environment, or the Vite proxy won't know where to forward/apicalls. SetAPI_INTERNAL_URL=http://localhost:8080andAPI_PROXY_TARGET=http://localhost:8080in the E2E job'senv:block.Open Decisions
stammbaum.spec.tsin this issue? The issue title says "for stammbaum spec" but the CI job will run all E2E tests. If other specs are fragile on the NAS runner, this PR will be blocked until they are fixed. Scoping to just the stammbaum spec initially is safer and keeps this issue atomic.🔧 Tobias Wendt — DevOps & Platform Engineer
Observations
docs/infrastructure/ci-gitea.mdalready contains a complete, tested E2E job YAML. This is good — the infra design is done. The issue is purely about copy-paste + wiring.docker-compose.ci.ymloverlay correctly replaces the bind-mount volumes (./data/postgres,./data/minio) from the base Compose file with named volumes (ci_postgres_data,ci_minio_data). Named volumes in CI are ephemeral per-run if the runner cleans up between jobs. The CI doc includes adocker compose down --volumes --remove-orphans || truecleanup step at the start, which handles leftover state from crashed previous runs correctly.db minio create-buckets. This is the right call for the NAS runner. The OCR service needs 12 GB RAM, a 120-second health start period, and model files. Skipping it keeps the E2E job feasible on the hardware.minio/minio:latestis unpinned indocker-compose.yml. This is a pre-existing issue but directly affects CI reproducibility. A MinIO breaking change could silently break CI at any push without explanation.axllent/mailpit:latestis also unpinned. Same concern, lower risk since E2E tests don't appear to exercise email flows heavily.Start backendstep connects the job container to the Compose network withdocker network connect familienarchiv_archive-net $(cat /etc/hostname). This is the correct technique for a non-containerized job step to reach services started via Compose. The network namefamilienarchiv_archive-netis derived from the project directory name — this is fragile if the repo is checked out under a different directory name on the runner.install-deps chromiumif cache hit. This is the correct pattern — full install on miss, system deps only on hit. Saves 60–90 seconds per run.backend-unit-testsjob already setsDOCKER_API_VERSION: "1.43"for the NAS runner. The E2E job should set the same env var since it also runs Docker commands.curl actuator/healthfor the health wait. The doc useswget -qO-which is better for Alpine-based containers. The actual YAML usescurl -sf http://localhost:8080/actuator/health | grep -q "UP"which is fine for a non-containerized step.Recommendations
ci-gitea.mdintoci.ymlverbatim. Do not rewrite it — the doc was written with the NAS runner's constraints in mind. The$(cat /etc/hostname)network connect trick and the selectivedb minio create-bucketsstart are both load-bearing.DOCKER_API_VERSION: "1.43"to the E2E job'senv:block. This is already inbackend-unit-testsand is required for the NAS runner's Docker 24.x host.minio/minioandminio/mcto specific digest or tag. Open a separate issue immediately after this one lands.RELEASE.2025-01-20T14-49-07Zor equivalent — check the MinIO releases page for the current stable tag.-p familienarchivon everydocker composecall in the E2E job, or setCOMPOSE_PROJECT_NAME=familienarchivin the job's env. This makes the network name deterministic regardless of checkout path.E2E_ADMIN_PASSWORDas a Gitea Actions secret before merging. TheUserDataInitializerseeds admin withAPP_ADMIN_PASSWORDfrom the environment — it works withadmin123as default, but using a secret is the correct posture and costs nothing.needs: [unit-tests, backend-unit-tests]on the E2E job. This prevents wasted Docker Compose start time when earlier jobs fail.Open Decisions
if: always(), which uploads on every run. On a passing suite this generates storage churn with no diagnostic value.if: failure()is cheaper but means you can't see screenshots from green runs. This is a workflow preference call.📋 Elicit — Requirements Engineer
Observations
test.skip()removed, tests green. This passes the INVEST test for "Small" and "Testable."frontend/e2e/CLAUDE.md, but the actual implementation blueprint is indocs/infrastructure/ci-gitea.md. The CLAUDE.md reference is directional ("extendinfra/gitea/workflows/ci.yml"), not prescriptive. The issue could have been clearer about where the authoritative design lives.test.skip()annotations" but does not specify the expected outcome. A definition of done that includes "all four tests instammbaum.spec.tspass green in CI on at least one successful run" would make closure unambiguous.E2E_ADMIN_PASSWORDto be set as a Gitea Actions secret. If not created before the first CI run, the secret resolves to empty string,auth.setup.tsfalls back to the hardcoded defaultadmin123, and the tests may still pass — but this is accidental success, not correct configuration. The issue should list "CreateE2E_ADMIN_PASSWORDsecret in Gitea" as an explicit task.API_INTERNAL_URL/API_PROXY_TARGETenv vars for the Playwright webServer. The SvelteKit dev server needs to know where the backend is. In Docker Compose these come from the container environment. When Playwright starts the dev server directly (which thewebServerblock does), those vars must be passed explicitly in the CI job'senv:block. This is an omission in the issue spec.Recommendations
ci.ymlhas a passinge2e-testsjob, (2)stammbaum.spec.tshas notest.skip(), (3) the job passes green on the branch CI run before merge.E2E_ADMIN_PASSWORDin Gitea → Settings → Actions → Secrets."API_INTERNAL_URL=http://localhost:8080in the E2E job env. Without this, the SvelteKit dev server started by Playwright'swebServerblock will have no target for its/apiproxy and all API calls will 502.docker compose up -dnaively, which would attempt to start the OCR service and likely exhaust RAM on the NAS runner.🔒 Nora "NullX" Steiner — Security Engineer
Observations
auth.setup.ts:admin@familyarchive.local/admin123are hardcoded as defaults inprocess.env.E2E_USERNAME ?? 'admin@familyarchive.local'. If theE2E_PASSWORDGitea secret is not set,auth.setup.tssilently usesadmin123— the same value theUserDataInitializerseeds by default. This means tests pass without any secret configured, which creates false confidence that the secret discipline is in place.admin123also appears as the defaultAPP_ADMIN_PASSWORDinapplication.yaml:password: ${APP_ADMIN_PASSWORD:admin123}. If the backend is launched in CI without--APP_ADMIN_PASSWORD=<secret>, the admin account has a known, trivially guessable password. In CI this is an ephemeral environment, so the practical risk is low — but a CI runner accessible from the network during a job run would expose the backend with a known credential.--APP_ADMIN_USERNAME=adminon the JVM command line. CLI arguments in process lists (ps aux, container inspect) are visible to any process running as the same user. On a shared NAS runner, this could expose the admin username. The password is passed via Gitea secret, which is correct.AuthE2EController(@Profile("e2e")) exposes test-only endpoints that bypass normal auth flows. The@Profile("e2e")guard is the correct mechanism to gate these, andSecurityConfigmust allow them. As long asSPRING_PROFILES_ACTIVE=e2eis never set in production, this is safe. Confirm that the production Compose file (if it exists) does not include thee2eprofile — the currentdocker-compose.ymlshowsSPRING_PROFILES_ACTIVE: dev,e2e, which is fine for local dev but should not appear in any production overlay.COMPOSE_PROJECT_NAMEisolation in the CI job. If two CI runs overlap on the same NAS runner (which can happen on parallel branch pushes), thedocker compose down --volumescleanup at the start of the second run will kill the first run's containers. This is not a security issue per se, but it means concurrent CI runs will corrupt each other — worth flagging to Tobias.Recommendations
E2E_ADMIN_PASSWORDGitea secret before the first run and set it to something that is notadmin123. The E2E profile is ephemeral, but the discipline of never accepting a known default password in any CI pipeline is worth maintaining. Any 16-char random string suffices.APP_ADMIN_PASSWORDvia environment variable, not CLI flag. Replace--APP_ADMIN_PASSWORD=${{ secrets.E2E_ADMIN_PASSWORD }}in thejava -jarcall with an explicitenv:block entry:APP_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }}. Environment variables are less visible than CLI args in process listings.SPRING_PROFILES_ACTIVEdoes not includee2ein any production configuration. Checkdocker-compose.prod.yml(if it exists) and the production.env. Thee2eprofile enablesAuthE2EControllerwhich exposes password reset tokens in plaintext — this must never reach a real deployment.admin123default fromapplication.yaml. Replacepassword: ${APP_ADMIN_PASSWORD:admin123}withpassword: ${APP_ADMIN_PASSWORD}(no default). This forces the env var to be explicitly set in every environment, making a misconfiguration loud rather than silently falling back to a known password.🧪 Sara Holt — QA Engineer
Observations
stammbaum.spec.tsfollow good E2E practices: they test user-visible behavior (headings, links, rendered nodes) rather than implementation details. The soft assertion in the third test (emptyVisible || nodeCount > 0) is intentional and correct — it tests both valid states without coupling to seeded data.expect(...).toPass()polling wrapper checksempty.isVisible()andanyNode.count()in a callback, but the default timeout for.toPass()in Playwright is 5 seconds. If the SVG tree renders slowly (e.g., on first load, data fetch in progress), 5 seconds may not be enough on the NAS runner. Theplaywright.config.tsdoes not set a customexpect.timeout.AxeBuilder) instammbaum.spec.ts. Theaccessibility.spec.tsmay or may not include/stammbaum. Since this is a new route with an interactive SVG tree, an axe scan is worth adding to verify therole="img"andaria-labelattributes on the SVG, and therole="button"on nodes, meet WCAG requirements./persons, clicks the first person, opens edit, and fills year fields. This test depends on at least one person existing in the seeded E2E data. TheUserDataInitializercreates persons whenSPRING_PROFILES_ACTIVE=e2e, so this should be reliable — but if the seed ever changes or is removed, this test will fail with a confusing selector error rather than a clear "no persons seeded" message.test.describeblock is named'Stammbaum — issue #358'. References to issue numbers in test names are an anti-pattern — they become meaningless noise after the issue is closed. The describe block name should describe the feature, not the tracking ID.playwright.config.tssetsfullyParallel: falseandworkers: 1. The stammbaum tests will run sequentially with all other E2E specs. Given the NAS runner's constraints this is acceptable, but it means total E2E time is the sum of all spec durations. Monitor whether the suite stays under 8 minutes after enabling the new tests.Recommendations
expect.timeoutof 10 seconds inplaywright.config.ts(the Playwright default is 5s). For an SVG tree rendered from an API response, this gives the page adequate time to settle without being excessively generous.test.describeblock from'Stammbaum — issue #358'to'Stammbaum page'or'Stammbaum — family tree view'. Test names should describe behavior, not ticket history.// Requires: UserDataInitializer seeds at least one Person (guaranteed by e2e profile).This documents the assumption explicitly for future maintainers./stammbaumto the axe scan inaccessibility.spec.ts(or a new entry in the stammbaum spec) — even a one-line check covers the most common SVG accessibility pitfalls (missing alt text, unlabeled interactive elements).Open Decisions
stammbaum.spec.tsorpersons.spec.ts? It tests person-edit behavior triggered from a Stammbaum journey. If the persons edit spec already covers similar validation, this test is better placed there for discoverability. If Stammbaum owns the "add relationship" flow end-to-end, keeping it here is defensible.🎨 Leonie Voss — UI/UX & Accessibility
Observations
svg[role="img"][aria-label="Stammbaum"] g[role="button"]. This tells me the current Stammbaum implementation assignsrole="img"to the outer SVG androle="button"to individual person nodes. This is a reasonable ARIA pattern for an interactive tree — butrole="img"on the containing SVG conflicts with the interactive children: a container marked as an image implies its contents are decorative and non-interactive. Screen readers may skip the innerrole="button"elements entirely.aria-labelon the individualg[role="button"]nodes. The selector checks that the elements exist but does not verify they have accessible names. A family tree node without an accessible name would be announced as "button" with no context — unusable for a screen reader user.reducedMotion: 'reduce'(set inplaywright.config.ts), which is correct and will suppress any SVG pan/zoom animations during testing. This is good practice.Recommendations
role="img"on the SVG container torole="tree"orrole="group"witharia-label="Stammbaum". Arole="tree"container signals to assistive technology that the contents are navigable nodes, not a static image. Eachg[role="button"]should then haverole="treeitem"and anaria-labelset to the person's display name (e.g.,aria-label="Karl Raddatz (1872–1943)").aria-labelto each person node. Without an accessible name,role="button"(orrole="treeitem") is announced as an unlabeled control. This is a WCAG 2.1 failure (SC 4.1.2). The test should assertanyNode.first().getAttribute('aria-label')returns a non-empty value.test.skip-free viewport test at 375px to the spec once CI is working — even a simple "page does not overflow horizontally" check (no horizontal scrollbar). Stammbaum is likely not the primary mobile view, but the empty state must be presentable on phone.tabindex="0"is set on each interactive node and that focus is visually indicated. This can be validated with the existingfocus-rings.spec.tspattern.Open Decisions
role="img"with no interactive children) or an interactive tree widget? If it's meant to be interactive (clicking nodes navigates to person detail), it needs full keyboard and screen-reader support as described above. If it's intended as a visual-only diagram (read-only overview), thenrole="img"with a single descriptivearia-labeland a separate list-based navigation fallback is the correct approach. The current implementation mixes both, which is the worst of both worlds from an accessibility standpoint. This architectural decision for the SVG tree should be made explicitly before the tests are locked in.🗳️ Decision Queue — Cross-Persona Open Items
Grouped by theme. These are genuine tradeoffs where your call is needed before or during implementation.
1. CI Trigger Scope
Raised by: Markus
Should the new
e2e-testsjob run on every branch push, or only on pull requests? Every push currently triggers all jobs. With E2E adding 5–10 minutes of runtime (Maven build + Docker + Playwright), this increases NAS load significantly on active branches.Options:
on: push / pull_request(current default) — maximum coverage, more loadon: pull_requestonly — E2E only runs before merge, not on every WIP push2. Full E2E Suite vs. Stammbaum-Only
Raised by: Felix
npm run test:e2eruns all specs, not juststammbaum.spec.ts. If any existing spec is fragile on the NAS runner, this PR will be blocked until that is fixed too.Options:
npm run test:e2e) — complete coverage from day onenpx playwright test e2e/stammbaum.spec.tsin this issue — unblock the skip removal, expand later3. E2E Artifact Upload Policy
Raised by: Tobias
if: always()uploads screenshots/traces/videos on every run. On a passing suite, this is churn with no diagnostic value.Options:
if: failure()— upload only when something broke (default recommendation)if: always()— always upload for audit trail4. Stammbaum SVG Accessibility Model
Raised by: Leonie
The current implementation uses
role="img"on the SVG container withrole="button"on child<g>nodes. These two roles conflict: an image container implies non-interactive children, and screen readers may skip the buttons entirely.Options:
role="img"and make the tree purely decorative — provide a separate list-based navigation fallback (simpler, but removes SVG interactivity for AT users)role="tree"/role="treeitem"with per-nodearia-labelvalues — full keyboard and AT support (more work, but correct for an interactive widget)This decision affects the Stammbaum component implementation, not just the CI job — but it needs to be made before the E2E tests are locked in, since the selectors in
stammbaum.spec.tstest the ARIA structure directly.5. Year-Range Test Placement
Raised by: Sara
The fourth test in
stammbaum.spec.ts(year-range validation error on person edit) navigates through a Stammbaum journey to reach a persons edit form. It tests person-edit validation behavior, not Stammbaum-specific rendering.Options:
stammbaum.spec.ts— documents the end-to-end journey from the tree to editpersons.spec.ts— test lives alongside other persons edit tests, easier to find when persons edit changes