From fbd886f7a78501308be32c890fddeed8b05a5df9 Mon Sep 17 00:00:00 2001 From: stroblme Date: Thu, 15 Oct 2026 16:15:32 +0200 Subject: [PATCH] make test-frontend runs against a stack of its own The suite creates and deletes flows, dashboards and users, and ran against whatever stack was up, which is the dev instance's data. make test-stack starts the compose project fluksio-test (compose.test.yml over the local stack's files) with its own containers, volumes and image tags, served by the local Traefik on app.test.localhost and api.test.localhost, and leaves it running. The hostname is fixed, so it no longer comes off the running dev stack, and a LAN stack makes no difference. The suite's default origin is the test stack. --- Makefile | 67 ++++++++++++++++++++--------------- README.md | 2 +- development.md | 19 ++++++---- docker/compose.test.yml | 23 ++++++++++++ frontend/playwright.config.ts | 2 +- frontend/tests/config.ts | 3 +- frontend/tests/sweep.setup.ts | 6 ++-- 7 files changed, 82 insertions(+), 40 deletions(-) create mode 100644 docker/compose.test.yml diff --git a/Makefile b/Makefile index 0fb9b47..62cc4a0 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ # The workspace root delegates to these (see ../Makefile). .PHONY: init env-check dev-utils dev dev-local dev-lan rebuild-frontend up down update install dev-backend dev-frontend \ - generate-client sync-example test test-backend test-checks test-frontend soak bench-startup lint lint-backend \ - lint-frontend format-frontend umami build docs docs-serve clean help + generate-client sync-example test test-backend test-checks test-stack test-frontend soak \ + bench-startup lint lint-backend lint-frontend format-frontend umami build docs docs-serve clean help COMPOSE_ROOT := $(CURDIR) # Explicit project name keeps this stack isolated from the sibling website @@ -25,6 +25,13 @@ COMPOSE_LOCAL := $(COMPOSE_DEV) -f docker/compose.local.yml # Same stack, with the frontend also published on APP_PORT and proxying the API # there, for clients that cannot resolve app.$(DOMAIN). COMPOSE_LAN := $(COMPOSE_LOCAL) -f docker/compose.lan.yml +# The frontend suite's own stack, a project of its own behind the local Traefik +# (compose.test.yml). The hostname is fixed, never read from a file or the +# command line, so the suite can only ever reach this stack. +TEST_DOMAIN := test.localhost +COMPOSE_TEST := DOMAIN=$(TEST_DOMAIN) STACK_NAME=fluksio-test TAG=test \ + docker compose -p fluksio-test --env-file $(COMPOSE_ROOT)/.env -f docker/compose.yml \ + -f docker/compose.dev.yml -f docker/compose.local.yml -f docker/compose.test.yml help: ## Show available targets @awk 'BEGIN{FS=":.*?## "} /^[a-zA-Z_-]+:.*?##/ {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) @@ -141,6 +148,7 @@ update: ## Pull, rebuild using the layer cache, and recreate changed containers down: ## Stop all running containers -$(COMPOSE_LOCAL) down + -$(COMPOSE_TEST) down -$(COMPOSE_PROD_RUN) down # ── Development (local, no Docker) ─────────────────────────────── @@ -184,35 +192,38 @@ test-backend: ## Run backend tests (pytest + coverage) PW_VERSION = $(shell sed -n 's/.*"@playwright\/test": "[^0-9]*\([0-9.]*\)".*/\1/p' frontend/package.json | head -1) -# This suite creates and deletes flows, dashboards and users, so the hostname it -# is pointed at is not taken from any file: it is read off the running stack -# (the frontend container's own Traefik rule) and mapped onto the local Traefik -# by address, so DNS never decides. tests/guard.ts is the second line. The -# suite calls api. even on a LAN stack, so an api container recreated -# under another DOMAIN would answer every call with Traefik's 404. -test-frontend: ## Run frontend tests (Playwright e2e) against the local stack +# The suite creates and deletes flows, dashboards and users, so it runs against +# a stack of its own, never the dev instance. `up` only recreates what changed, +# and the stack is left running for the next run (and for a look at a failure +# on http://app.test.localhost). `make down` stops it. Its volumes are +# expendable: `docker compose -p fluksio-test down -v` starts it over. +test-stack: ## Start or update the frontend suite's own stack at http://app.test.localhost + @docker ps -q -f name='^fluksio-app-proxy-1$$' | grep -q . \ + || { echo " ✗ the local Traefik is not running, 'make dev' first"; exit 1; } + $(COMPOSE_TEST) up --build -d --wait redis backend frontend + +# The Playwright container, minus its network. tests/guard.ts refuses a public +# address on top of the fixed hostname. +PW_RUN = docker run --rm --ipc=host --user $$(id -u):$$(id -g) -e HOME=/tmp \ + -v $(COMPOSE_ROOT):/app -w /app/frontend \ + -e PLAYWRIGHT_BASE_URL=http://app.$(TEST_DOMAIN) \ + -e PLAYWRIGHT_API_URL=http://api.$(TEST_DOMAIN) \ + -e CI=$${CI:-1} \ + -e FIRST_SUPERUSER="$$(sed -n 's/^FIRST_SUPERUSER=//p' $(COMPOSE_ROOT)/.env | head -1)" \ + -e FIRST_SUPERUSER_PASSWORD="$$(sed -n 's/^FIRST_SUPERUSER_PASSWORD=//p' $(COMPOSE_ROOT)/.env | head -1)" +PW_IMAGE = mcr.microsoft.com/playwright:v$(PW_VERSION)-noble + +# The names are mapped onto the local Traefik by address, so DNS never decides. +# Twice, because Chromium pins *.localhost to loopback whatever /etc/hosts says. +test-frontend: test-stack ## Run frontend tests (Playwright e2e) against the test stack @ip=$$(docker network inspect proxy \ --format '{{range .Containers}}{{if eq .Name "fluksio-app-proxy-1"}}{{.IPv4Address}}{{end}}{{end}}' \ 2>/dev/null | cut -d/ -f1); \ - [ -n "$$ip" ] || { echo " ✗ proxy network or Traefik container not found — is the stack up?"; exit 1; }; \ - domain=$$(docker inspect fluksio-app 2>/dev/null \ - | grep -o 'Host(`app\.[^`]*`)' | head -1 | sed 's/Host(`app\.//;s/`)//'); \ - [ -n "$$domain" ] || { echo " ✗ no running app stack to test — 'make dev' first"; exit 1; }; \ - api=$$(docker inspect fluksio-api 2>/dev/null \ - | grep -o 'Host(`api\.[^`]*`)' | head -1 | sed 's/Host(`api\.//;s/`)//'); \ - [ "$$api" = "$$domain" ] || { echo " ✗ api container is on api.$${api:-?}, the app on app.$$domain — recreate the stack with 'make dev'"; exit 1; }; \ - docker run --rm --network proxy --ipc=host \ - --user $$(id -u):$$(id -g) -e HOME=/tmp \ - --add-host app.$$domain:$$ip --add-host api.$$domain:$$ip \ - -v $(COMPOSE_ROOT):/app -w /app/frontend \ - -e PLAYWRIGHT_BASE_URL=http://app.$$domain \ - -e PLAYWRIGHT_API_URL=http://api.$$domain \ - -e HOST_RESOLVER_RULES="MAP app.$$domain $$ip, MAP api.$$domain $$ip" \ - -e CI=$${CI:-1} \ - -e FIRST_SUPERUSER="$$(sed -n 's/^FIRST_SUPERUSER=//p' $(COMPOSE_ROOT)/.env | head -1)" \ - -e FIRST_SUPERUSER_PASSWORD="$$(sed -n 's/^FIRST_SUPERUSER_PASSWORD=//p' $(COMPOSE_ROOT)/.env | head -1)" \ - mcr.microsoft.com/playwright:v$(PW_VERSION)-noble \ - npx playwright test $(PLAYWRIGHT_ARGS) + [ -n "$$ip" ] || { echo " ✗ the local Traefik is not on the proxy network"; exit 1; }; \ + $(PW_RUN) --network proxy \ + --add-host app.$(TEST_DOMAIN):$$ip --add-host api.$(TEST_DOMAIN):$$ip \ + -e HOST_RESOLVER_RULES="MAP app.$(TEST_DOMAIN) $$ip, MAP api.$(TEST_DOMAIN) $$ip" \ + $(PW_IMAGE) npx playwright test $(PLAYWRIGHT_ARGS) # Load and chaos against a *running* stack, never part of `make test`: it # restarts this stack's containers. `SOAK_ARGS="--dry-run"` only looks. diff --git a/README.md b/README.md index e6ee9b9..1035146 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ make dev-frontend # Vite on :5173 ``` ```sh -make test # pytest + Playwright (the e2e half needs the stack up) +make test # pytest + Playwright (the e2e half on its own stack, behind make dev's Traefik) make lint # ruff + mypy + biome make generate-client # regenerate the frontend SDK from the OpenAPI schema ``` diff --git a/development.md b/development.md index a43215a..76582d2 100644 --- a/development.md +++ b/development.md @@ -129,12 +129,19 @@ SQLite file in a temp directory (deleted per session, so a run never touches dev data), its own superuser, and `ENVIRONMENT=local` with `DOMAIN=localhost`, so a checkout configured for a deployment cannot drag that deployment's configuration into the run. -**Frontend (e2e).** `make test-frontend` runs the suite inside the pinned -`mcr.microsoft.com/playwright:v-noble` image (version read from -`frontend/package.json`) on the `proxy` network, against a stack that must already be up. It -maps `app.$(DOMAIN)` and `api.$(DOMAIN)` onto the Traefik container's address twice, with -`--add-host` and with `HOST_RESOLVER_RULES`, because Chromium pins `*.localhost` to loopback -whatever `/etc/hosts` says. Narrow a run with +**Frontend (e2e).** `make test-frontend` runs the suite against a stack of its own, because +the suite creates and deletes flows, dashboards and users. `make test-stack`, which it runs +first, starts or updates the compose project `fluksio-test` (`docker/compose.test.yml` over the +local stack's files), with its own containers, volumes and image tags. The local Traefik serves +it on `app.test.localhost` and `api.test.localhost`, so `make dev` has to be up, in either +mode: the test stack never layers `compose.lan.yml` and publishes no port. It stays running +after a run for a look at a failure, `make down` stops it, and its volumes are expendable +(`docker compose -p fluksio-test down -v` starts it over). + +The suite runs inside the pinned `mcr.microsoft.com/playwright:v-noble` image +(version read from `frontend/package.json`) on the `proxy` network. It maps both names onto +the Traefik container's address twice, with `--add-host` and with `HOST_RESOLVER_RULES`, +because Chromium pins `*.localhost` to loopback whatever `/etc/hosts` says. Narrow a run with `make test-frontend PLAYWRIGHT_ARGS="--grep flows"`. The specs are `frontend/tests/*.spec.ts`; `playwright.config.ts` defines `setup` → diff --git a/docker/compose.test.yml b/docker/compose.test.yml new file mode 100644 index 0000000..9ad86d3 --- /dev/null +++ b/docker/compose.test.yml @@ -0,0 +1,23 @@ +# The frontend suite's own stack. `make test-stack` layers this on +# compose.local.yml under the project `fluksio-test`, with DOMAIN=test.localhost +# and STACK_NAME=fluksio-test. It shares the dev stack's Traefik and nothing +# else: its own containers, volumes, image tags and hostnames, so the flows, +# dashboards and users the suite creates never reach the dev instance's data. +services: + + # `container_name` ignores the project name, so the dev stack's names would + # collide. + redis: + container_name: !reset null + + backend: + container_name: !reset null + # compose.dev.yml also brings up the broker and the time-series database + # with it, which no spec talks to. + depends_on: !override + redis: + condition: service_healthy + restart: true + + frontend: + container_name: !reset null diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 7795f7b..fd9f2c7 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -28,7 +28,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. Defaults to the - integrated stack (`make dev`), the only origin the API allows CORS from. + suite's own stack (`make test-stack`), never the dev instance's data. Point PLAYWRIGHT_BASE_URL elsewhere to test another running server; the API origin follows it (see tests/config.ts). */ baseURL: appUrl, diff --git a/frontend/tests/config.ts b/frontend/tests/config.ts index 6af784a..8ab661f 100644 --- a/frontend/tests/config.ts +++ b/frontend/tests/config.ts @@ -29,7 +29,8 @@ export const firstSuperuserPassword = getEnvVar("FIRST_SUPERUSER_PASSWORD") * instance. Both origins come from the same place instead: whatever the browser * is pointed at is what teardown may write to. */ -export const appUrl = process.env.PLAYWRIGHT_BASE_URL || "http://app.localhost" +export const appUrl = + process.env.PLAYWRIGHT_BASE_URL || "http://app.test.localhost" export const apiUrl = process.env.PLAYWRIGHT_API_URL || apiOrigin(appUrl) diff --git a/frontend/tests/sweep.setup.ts b/frontend/tests/sweep.setup.ts index d142452..466c1d4 100644 --- a/frontend/tests/sweep.setup.ts +++ b/frontend/tests/sweep.setup.ts @@ -11,9 +11,9 @@ import { apiUrl, firstSuperuser, firstSuperuserPassword } from "./config.ts" * rather than after one is what makes this work at all: it happens even when * the run that made the mess died. * - * The suite runs against whatever stack is up, which is somebody's own - * instance with real flows in it, so the predicate has to be incapable of - * matching one. A name qualifies only if it has the shape every fixture has — + * `make test-frontend` runs against a stack of its own, but the suite can be + * pointed at any (PLAYWRIGHT_BASE_URL), and that may be somebody's own instance + * with real flows in it, so the predicate has to be incapable of matching one. A name qualifies only if it has the shape every fixture has — * `test__`, plus an optional `_` for a second * artefact of the same spec — and only if the timestamp it carries is an hour * old, which nothing the run now starting is about to create can be.