Hermetic E2E tests
The E2E suites run a production-built AutoBangumi image against local, deterministic upstream services. Docker Compose publishes random loopback ports and uses an internal network for service-to-service traffic. Tests must not use public RSS, TMDB, image, player, or downloader endpoints.
Prerequisites
- Docker Engine with Compose v2
- Python 3.13 and
uv - Node.js 20 and pnpm 9.11
- enough disk space for the AutoBangumi, Python, Playwright, and qBittorrent images
Install dependencies and browser binaries before starting a stack:
uv sync --directory backend --locked --group dev
pnpm --dir webui install --frozen-lockfile
pnpm --dir webui exec playwright install --with-deps chromium webkit firefox
Build the production test image
The build helper places the compiled WebUI and generated version module in a
temporary Docker context. It does not write dist or
module/__version__.py into the source tree.
pnpm --dir webui run build
python3 e2e/scripts/build_test_image.py \
--version 3.3.999-e2e.1 \
--image auto-bangumi:e2e \
--dist webui/dist
Run a lane locally
Create a fresh project and work directory for every invocation. Reusing a work directory also reuses setup state, which defeats first-run coverage.
export LANE=runtime
export AB_E2E_PROJECT="ab-e2e-local-$LANE-$$"
export AB_E2E_WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ab-e2e-$LANE.XXXXXX")"
export AB_E2E_APP_IMAGE=auto-bangumi:e2e
Runtime API and process coverage:
python3 e2e/scripts/stack.py run \
--profile browser \
--project-name "$AB_E2E_PROJECT" \
--work-dir "$AB_E2E_WORK_DIR" \
-- uv run --directory backend pytest src/test/e2e/runtime -m e2e -q
Chromium desktop:
python3 e2e/scripts/stack.py run \
--profile browser \
--project-name "$AB_E2E_PROJECT" \
--work-dir "$AB_E2E_WORK_DIR" \
-- pnpm --dir webui run test:e2e:chromium --retries=0
WebKit at the 390×844 mobile viewport:
python3 e2e/scripts/stack.py run \
--profile browser \
--project-name "$AB_E2E_PROJECT" \
--work-dir "$AB_E2E_WORK_DIR" \
-- pnpm --dir webui run test:e2e:webkit --retries=0
Real qBittorrent plus the packaged-image workflow:
python3 e2e/scripts/stack.py run \
--profile downloader \
--project-name "$AB_E2E_PROJECT" \
--work-dir "$AB_E2E_WORK_DIR" \
-- python3 e2e/scripts/run_downloader_lane.py
The downloader runner executes the focused Chromium download/rename spec and
backend/src/test/e2e/downloader inside that same fresh stack.
Firefox is a nightly, non-required lane:
python3 e2e/scripts/stack.py run \
--profile browser \
--project-name "$AB_E2E_PROJECT" \
--work-dir "$AB_E2E_WORK_DIR" \
-- pnpm --dir webui run test:e2e --project=firefox-nightly --retries=0
The nightly workflow runs browser and downloader twice as separate matrix passes. Each pass receives a fresh project and work directory; locally, repeat the corresponding command after recreating both variables above.
Useful lifecycle checks:
python3 e2e/scripts/stack.py validate --profile browser
python3 e2e/scripts/stack.py validate --profile downloader
python3 e2e/scripts/stack.py smoke --profile browser
python3 e2e/scripts/stack.py ports --profile browser \
--project-name "$AB_E2E_PROJECT" --work-dir "$AB_E2E_WORK_DIR"
python3 e2e/scripts/stack.py env --profile browser \
--project-name "$AB_E2E_PROJECT" --work-dir "$AB_E2E_WORK_DIR"
python3 e2e/scripts/stack.py restart --profile browser \
--project-name "$AB_E2E_PROJECT" --work-dir "$AB_E2E_WORK_DIR" app
python3 e2e/scripts/stack.py stop --profile browser \
--project-name "$AB_E2E_PROJECT" --work-dir "$AB_E2E_WORK_DIR"
Duration and artifacts
The pull-request checks have a hard 15-minute limit. Typical targets are 5–8 minutes for runtime, 8–12 minutes for each browser, and 10–15 minutes for the real-downloader/image lane.
stack.py run collects diagnostics before Compose teardown, including
Compose state and logs, mock-service request journals, qBittorrent state,
backend logs, and SQLite files. Local stack artifacts are written to
$AB_E2E_WORK_DIR/artifacts. Playwright writes screenshots and videos to
webui/test-results, with its HTML report in webui/playwright-report.
Raw Playwright traces are disabled because they serialize authenticated
storage state and response bodies; token-reveal tests also disable visual
recordings. Browser console errors and failed responses remain available in
the redacted browser-diagnostics.json attachment. CI uploads these paths
with if: always() and retains them for 14 days. Never print credentials or
session material from a test.
Legacy workflow migration
The former numbered, session-scoped workflow and its fixed ports were removed.
Setup/auth/config/restart persistence now live in isolated runtime tests;
parser, offset-review, and Windows-path regressions have focused runtime and
browser scenarios; the real qB lane owns download, rename, idempotency, and
file deletion. Endpoint shape, validation, CRUD, empty-list, log, search, and
notification contracts remain in the faster backend/src/test/test_api_*.py
unit/integration suites. No test depends on state created by an earlier test.
Add or change a mock scenario
- Put static RSS, TMDB, player, torrent, image, or file data under
e2e/fixtures. Fixture URLs must use only Compose service names or loopback addresses. - Add named TMDB behavior to
e2e/fixtures/tmdb/scenarios.json. - Select or reset scenarios through
/__admin/scenario/<name>and/__admin/reset; assert requests through/__admin/requestsor/__admin/state. - Keep responses deterministic and add a contract test for any new endpoint, error mode, or request field.
- Run
python3 e2e/scripts/audit_sources.pybefore opening a PR.
Regenerate and verify the deterministic media and torrent assets with:
python3 e2e/scripts/generate_fixtures.py
python3 e2e/scripts/generate_fixtures.py --check
Commit the generated media, torrent, and checksum changes together.
Update the qBittorrent image digest
The downloader overlay must retain both an exact patch tag and an immutable manifest digest. To update it:
-
Choose the exact linuxserver/qbittorrent patch release and inspect its published manifest:
docker buildx imagetools inspect linuxserver/qbittorrent:5.2.3 -
Replace only the tag and
sha256digest ine2e/compose/downloader.yml. Never uselatestor a moving major/minor tag. -
Pull that exact reference, then run the downloader validation and smoke checks. Startup verifies the fixed
admin/adminadmincredentials and rejects enabled DHT, PeX, LSD, UPnP, RSS processing, or a changed save path. -
Run the downloader Playwright lane and inspect its artifacts before merging.