E2E tests
The E2E suite under e2e/ builds the binary, starts a real server with Docker, deploys actual compose apps, and exercises every UI flow. Run with make e2e (~20 min) or make e2e-lite (~6-8 min, skips slow specs).
Layout
Section titled “Layout”e2e/ playwright.config.js serial, 1 worker, chromium, 2-min timeout global-setup.js builds binary, starts server on random port global-teardown.js kills server, cleans temp dirs fixtures/ compose files for test apps (nginx, multi, postgres) helpers/ server.js server lifecycle (build, start, stop, config gen) auth.js login/logout helpers, test admin credentials api.js direct API fetch for setup/teardown tests/ 01-setup.spec.js initial admin account creation 02-login.spec.js login, logout, wrong password, session redirect ... (numbered, run in order) 19-cleanup.spec.js remove all apps, verify clean stateHow tests work
Section titled “How tests work”- Tests run serially in numbered order. State accumulates: setup creates an admin, deploy creates apps, later tests use them.
- Each test gets a fresh browser context (isolated cookies) but the server is shared.
- Server starts with TLS off, very high rate limits, temp data and apps dirs.
- Three fixture apps:
e2e-nginx,e2e-multi,e2e-postgres. - Cleanup tests remove everything at the end.
Adding a spec
Section titled “Adding a spec”- Add a numbered file in
e2e/tests/that fits the suite’s order. - Use
loginAsAdmin(page)fromhelpers/auth.jsinbeforeEachfor authenticated tests. - Use
getState().baseURLfor the server URL. Hash routing:${baseURL}/#/path. - Wait for
asideafter login (sidebar means dashboard rendered). - Use
.first()ongetByText()when the text appears in sidebar and main. - Scope assertions to
page.locator('main')to avoid sidebar matches. - For modals, scope to
page.getByRole('dialog').
Common pitfalls
Section titled “Common pitfalls”getByText('foo')matches substrings, case-sensitive. Use{ exact: true }when needed.- Don’t run individual files in isolation; they depend on prior server state.
- Docker image pulls are slow on first run. Deploy test timeout is 180s.
- The
Securecookie flag is conditional on TLS mode; tests run with TLS off.
Running locally
Section titled “Running locally”make e2e # full suite, headless (~20 min)make e2e-lite # skip slow specs (~6-8 min)make e2e-mirror # full suite via GHCR mirror (no Docker Hub limits)make e2e-lite-mirror # lite via GHCR mirrormake e2e-headed # full suite, visible browsermake e2e-report # open HTML report from last runmake e2e-templates # deploy every app template end-to-end (on-demand)Requires Docker daemon, Go, Node.js.
Avoiding Docker Hub rate limits
Section titled “Avoiding Docker Hub rate limits”Deploying the full template/fixture matrix pulls ~45 images from Docker Hub. Anonymous Hub accounts are capped at 100 pulls per 6h per IP; authenticated free accounts at 200; CI on shared IPs hits the anonymous cap fast. The suite offers an opt-in mirror mode so every pull goes to GHCR instead.
How it works
Section titled “How it works”.github/workflows/mirror-images.ymlmirrors every image listed bynode e2e/scripts/list-mirror-images.mjsintoghcr.io/<owner>/simpledeploy-mirror/...viaskopeo copy --all(multi-arch). Triggered on changes toui/src/lib/{app,service}Templates.jsore2e/fixtures/**, and on manual dispatch.- At deploy time,
internal/mirror.RewriteComposerewrites docker.io-bound image refs in the compose YAML to<SIMPLEDEPLOY_IMAGE_MIRROR_PREFIX><path>before the file is persisted. Covers initial deploy, rollback, and restore. e2e/helpers/server.jssetsSIMPLEDEPLOY_IMAGE_MIRROR_PREFIX=ghcr.io/vazra/simpledeploy-mirror/whenE2E_USE_MIRROR=1; override the prefix by exporting a differentSIMPLEDEPLOY_IMAGE_MIRROR_PREFIXin your shell.
Using it
Section titled “Using it”E2E_USE_MIRROR=1 make e2e # or: make e2e-mirrormake mirror-images-list # print the image set the workflow pushesThe same env var works for any local ./bin/simpledeploy serve session: set SIMPLEDEPLOY_IMAGE_MIRROR_PREFIX and deploys route through GHCR. Leave it unset in production so users get upstream images.
Bootstrapping the mirror
Section titled “Bootstrapping the mirror”First run only: gh workflow run mirror-images.yml after the workflow is merged. Subsequent runs are automatic and incremental (tags that already exist on GHCR are skipped).
Authenticating docker for mirror pulls
Section titled “Authenticating docker for mirror pulls”GHCR packages are private by default. Both CI workflows (ci.yml, templates.yml) run docker login ghcr.io with GITHUB_TOKEN before the test step. For local mirror use, either:
docker login ghcr.ioonce with a PAT that hasread:packages, or- Open each
ghcr.io/<owner>/simpledeploy-mirror/...package in the GitHub UI and set visibility to Public (one-time per package).
Template validation
Section titled “Template validation”Two layers guard the built-in app and service templates:
00-template-images.spec.js(runs ine2eande2e-lite): importsappTemplates.jsandserviceTemplates.js, collects everyimage:reference, and runsdocker manifest inspecton each in parallel. Fails fast when a tag is typo’d, yanked, or private. Takes ~10-30s.templates-deploy-all.spec.js(only runs undermake e2e-templates/E2E_TEMPLATES=1): deploys every app template through the UI wizard, asserts the “Deployed” pill, then deletes the app via API. Expensive (pulls ~20 multi-service stacks), so it is excluded from bothe2eande2e-lite. The.github/workflows/templates.ymlworkflow triggers it automatically whenappTemplates.js,serviceTemplates.js, or either template spec changes, so it runs once per template-edit PR.
When adding or editing a template, bias toward running make e2e-templates locally before opening the PR; the CI workflow runs it too but locally you get faster feedback.