From a304fa471b61899a43e9fd23525035a23c9a4ab2 Mon Sep 17 00:00:00 2001 From: Georgios Jason Efstathiou Date: Fri, 29 May 2026 13:35:13 +0200 Subject: [PATCH] Run e2e tests in a Docker container --- .dockerignore | 13 ++++++++ CONTRIBUTING.md | 32 +++++++++++++++++++ Dockerfile.e2e | 53 ++++++++++++++++++++++++++++++++ docker-compose.e2e.yml | 43 ++++++++++++++++++++++++++ scripts/docker-e2e-entrypoint.sh | 51 ++++++++++++++++++++++++++++++ scripts/test-e2e-docker | 51 ++++++++++++++++++++++++++++++ 6 files changed, 243 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile.e2e create mode 100644 docker-compose.e2e.yml create mode 100755 scripts/docker-e2e-entrypoint.sh create mode 100755 scripts/test-e2e-docker diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..a0daf0291 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +build +.svelte-kit +.git +tests/tmp +tests/artifacts +tests/visual/snapshots +data.ms +dumps +radicle-httpd/target +radicle-search/target +target +.wrangler diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ad46b0e4..ecf63eaaa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,6 +57,38 @@ This will: 1. Compile radicle-httpd from the `radicle-httpd/` directory 2. Run the full test suite against the locally compiled binary +**Running e2e tests in Docker:** + +If your host OS isn't supported by Playwright's browser builds (e.g. a very +new or unusual Linux distribution), run the suite inside a container instead. +Requires Docker with the Compose plugin. + + scripts/test-e2e-docker + +Any arguments are forwarded to `npm run test:e2e` (defaulting to +`--project chromium` when none are given): + + scripts/test-e2e-docker tests/e2e/repo.spec.ts + scripts/test-e2e-docker --project chromium -g "some test name" + +`SKIP_SETUP=true` is forwarded the same as for the native runner: + + SKIP_SETUP=true scripts/test-e2e-docker + +To test against a radicle-httpd you compile from source (the equivalent of +`npm run test:e2e:local`), pass `--local`: + + scripts/test-e2e-docker --local + +Notes: +* The first image build is slow (it downloads Chromium); subsequent runs are + cached. `--local` additionally installs a Rust toolchain and compiles + radicle-httpd on first use. +* The repo is bind-mounted, so test artifacts and the binary/fixture cache + land on the host as usual. `node_modules`, the cargo cache, and httpd's + build directory live in container-private volumes, leaving your host copies + untouched. + Proposing changes ----------------- When proposing changes via a patch: diff --git a/Dockerfile.e2e b/Dockerfile.e2e new file mode 100644 index 000000000..145361fb5 --- /dev/null +++ b/Dockerfile.e2e @@ -0,0 +1,53 @@ +# Runs the Playwright e2e suite in a Playwright-supported Linux distro. +# Useful when the host OS is too new/old for Playwright's browser builds. +# +# Two stages: +# base - Node + Chromium; runs the suite against the released radicle-httpd. +# local - base + a Rust toolchain; additionally compiles radicle-httpd from +# source for `--local` runs (needed when testing backend changes). +# +# Chromium and its OS dependencies are baked in at the project's pinned +# Playwright version. node_modules and the repo are provided at runtime via +# bind mount + named volume (see docker-compose.e2e.yml). +FROM node:24-bookworm AS base + +ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \ + DEBIAN_FRONTEND=noninteractive + +# xz-utils: install-binaries unpacks .tar.xz release archives. +# git/curl/ca-certificates: fetching binaries and git operations in fixtures. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + xz-utils ca-certificates git curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /work + +# Install Chromium plus its apt-level runtime dependencies, pinned to the +# Playwright version in package-lock.json so the baked browser matches the +# @playwright/test that npm ci installs into the volume at runtime. Copying +# only the lockfiles keeps this layer cached across source changes. +# --ignore-scripts: the repo's postinstall (copy-katex/twemoji assets) needs +# scripts/ which isn't in this layer; those run at runtime instead. +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts \ + && npx playwright install --with-deps chromium \ + && rm -rf node_modules + +ENTRYPOINT ["/work/scripts/docker-e2e-entrypoint.sh"] + +# --- local stage: adds Rust so the entrypoint can build radicle-httpd --- +FROM base AS local + +# Toolchain + build deps for compiling radicle-httpd (debug build). +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential pkg-config libssl-dev \ + && rm -rf /var/lib/apt/lists/* + +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:$PATH +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --no-modify-path --profile minimal --default-toolchain stable \ + && rustc --version diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml new file mode 100644 index 000000000..af5c053be --- /dev/null +++ b/docker-compose.e2e.yml @@ -0,0 +1,43 @@ +x-e2e-common: &e2e-common + init: true + working_dir: /work + environment: + PLAYWRIGHT_BROWSERS_PATH: /ms-playwright + +services: + # Runs the suite against the released radicle-httpd binary. + e2e: + <<: *e2e-common + build: + context: . + dockerfile: Dockerfile.e2e + target: base + volumes: + # Repo bind-mounted so code edits, test artifacts, and the + # binary/fixture cache (tests/tmp) are shared with the host. + - .:/work + # Container-private node_modules: keeps the host's untouched and + # avoids a glibc mismatch with host-built native modules. + - e2e-node-modules:/work/node_modules + + # Compiles radicle-httpd from source and runs the suite against it. + # Used via `scripts/test-e2e-docker --local` when testing backend changes. + e2e-local: + <<: *e2e-common + build: + context: . + dockerfile: Dockerfile.e2e + target: local + volumes: + - .:/work + - e2e-node-modules:/work/node_modules + # Container-private cargo crate cache and httpd build dir, so the + # Rust build is incremental across runs and never mixes with the + # host's (different glibc/rustc) target artifacts. + - e2e-cargo-registry:/usr/local/cargo/registry + - e2e-httpd-target:/work/radicle-httpd/target + +volumes: + e2e-node-modules: + e2e-cargo-registry: + e2e-httpd-target: diff --git a/scripts/docker-e2e-entrypoint.sh b/scripts/docker-e2e-entrypoint.sh new file mode 100755 index 000000000..9b2fefeb1 --- /dev/null +++ b/scripts/docker-e2e-entrypoint.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Entrypoint for the e2e Docker container. Installs npm deps into the +# container-private node_modules volume (only when package-lock.json changes), +# ensures the Radicle test binaries are present, then runs Playwright. Any +# arguments are forwarded to the test command. +# +# When USE_LOCAL_HTTPD=true, runs test:e2e:local (compiles radicle-httpd from +# source and tests the frontend against it) instead of test:e2e. +set -euo pipefail + +cd /work + +# The container runs as root, so anything it writes into the bind-mounted repo +# would otherwise be root-owned on the host. Hand those paths back to the +# invoking user (HOST_UID/HOST_GID set by scripts/test-e2e-docker) on exit. +# radicle-httpd/target lives in a named volume, so it's not host-visible. +cleanup() { + if [ -n "${HOST_UID:-}" ]; then + chown -R "$HOST_UID:${HOST_GID:-$HOST_UID}" \ + tests/tmp tests/artifacts tests/visual/snapshots build .svelte-kit \ + 2>/dev/null || true + fi +} +trap cleanup EXIT + +LOCK_HASH=$(md5sum package-lock.json | cut -d' ' -f1) +STAMP=node_modules/.lockhash +if [ ! -f "$STAMP" ] || [ "$(cat "$STAMP")" != "$LOCK_HASH" ]; then + echo "📦 Installing npm dependencies..." + npm ci + echo "$LOCK_HASH" >"$STAMP" +else + echo "📦 npm dependencies up to date." +fi + +echo "⬇️ Ensuring Radicle test binaries..." +mkdir -p tests/artifacts +./scripts/install-binaries + +NPM_SCRIPT="test:e2e" +if [ "${USE_LOCAL_HTTPD:-}" = "true" ]; then + NPM_SCRIPT="test:e2e:local" + echo "🦀 Local httpd mode: radicle-httpd will be compiled from source." +fi + +echo "🧪 Running e2e tests ($NPM_SCRIPT)..." +set +e +npm run "$NPM_SCRIPT" -- "$@" +status=$? +set -e +exit "$status" diff --git a/scripts/test-e2e-docker b/scripts/test-e2e-docker new file mode 100755 index 000000000..2ab5e80d4 --- /dev/null +++ b/scripts/test-e2e-docker @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Build and run the Playwright e2e suite inside a Docker container. Use this +# when the host OS is not supported by Playwright's browser builds. +# +# Usage: +# scripts/test-e2e-docker # all chromium e2e tests +# scripts/test-e2e-docker tests/e2e/foo.spec.ts # a single spec +# scripts/test-e2e-docker --project chromium -g "some test" +# scripts/test-e2e-docker --local # build radicle-httpd from +# # source and test against it +# +# Any non-flag arguments are forwarded to the test command. When none are +# given, defaults to `--project chromium`. +set -euo pipefail + +REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "$REPO_ROOT" + +COMPOSE="docker compose -f docker-compose.e2e.yml" + +# Strip our own --local flag; forward everything else to Playwright. +LOCAL=false +ARGS=() +for arg in "$@"; do + if [ "$arg" = "--local" ]; then + LOCAL=true + else + ARGS+=("$arg") + fi +done + +SERVICE=e2e +EXTRA_ENV=() +if $LOCAL; then + SERVICE=e2e-local + EXTRA_ENV=(-e USE_LOCAL_HTTPD=true) +fi + +$COMPOSE build "$SERVICE" + +if [ "${#ARGS[@]}" -eq 0 ]; then + ARGS=(--project chromium) +fi + +# SKIP_SETUP (no value) forwards the host's value through, mirroring the +# native runner's fast-iteration flag. +exec $COMPOSE run --rm \ + -e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \ + -e SKIP_SETUP \ + "${EXTRA_ENV[@]}" \ + "$SERVICE" "${ARGS[@]}"