Skip to content

Commit 6386e67

Browse files
Merge pull request #323 from MeshJS/feature/add-playwright-tests
Add Playwright end-to-end testing setup with email notifications
2 parents 194fbbc + cceb650 commit 6386e67

40 files changed

Lines changed: 2583 additions & 97 deletions

.github/workflows/pr-multisig-v1-smoke.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ on:
2727
default: ""
2828
type: string
2929

30+
# Serialize against the Playwright e2e workflow: both broadcast from the same
31+
# preprod CI wallets, so they must never run at the same time. A constant group
32+
# (not per-PR) ensures mutual exclusion across PRs too. cancel-in-progress:false
33+
# means runs queue rather than cancel each other.
34+
concurrency:
35+
group: ci-preprod-wallets
36+
cancel-in-progress: false
37+
3038
jobs:
3139
multisig-v1-smoke:
3240
if: github.repository == 'MeshJS/multisig'
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
name: PR Playwright Browser Tests
2+
3+
on:
4+
pull_request:
5+
branches:
6+
- main
7+
- preprod
8+
workflow_dispatch:
9+
inputs:
10+
transfer_lovelace:
11+
description: "Lovelace amount for ring transfer legs"
12+
required: false
13+
default: "2000000"
14+
type: string
15+
16+
# Serialize against the Multisig v1 smoke workflow: both broadcast from the same
17+
# preprod CI wallets, so they must never run at the same time. A constant group
18+
# (not per-PR) ensures mutual exclusion across PRs too. cancel-in-progress:false
19+
# means runs queue rather than cancel each other.
20+
concurrency:
21+
group: ci-preprod-wallets
22+
cancel-in-progress: false
23+
24+
jobs:
25+
playwright-browser:
26+
if: github.repository == 'MeshJS/multisig'
27+
runs-on: ubuntu-latest
28+
timeout-minutes: 60
29+
env:
30+
CI_JWT_SECRET: ${{ secrets.CI_JWT_SECRET }}
31+
CI_MNEMONIC_1: ${{ secrets.CI_MNEMONIC_1 }}
32+
CI_MNEMONIC_2: ${{ secrets.CI_MNEMONIC_2 }}
33+
CI_MNEMONIC_3: ${{ secrets.CI_MNEMONIC_3 }}
34+
CI_BLOCKFROST_PREPROD_API_KEY: ${{ secrets.CI_BLOCKFROST_PREPROD_API_KEY }}
35+
CI_NETWORK_ID: "0"
36+
CI_WALLET_TYPES: "legacy,hierarchical,sdk"
37+
CI_NUM_REQUIRED_SIGNERS: "2"
38+
CI_TRANSFER_LOVELACE: ${{ github.event_name == 'workflow_dispatch' && inputs.transfer_lovelace || '2000000' }}
39+
40+
steps:
41+
- name: Checkout repository
42+
uses: actions/checkout@v4
43+
44+
- name: Validate required secrets
45+
shell: bash
46+
run: |
47+
missing=()
48+
[[ -n "$CI_JWT_SECRET" ]] || missing+=("CI_JWT_SECRET")
49+
[[ -n "$CI_MNEMONIC_1" ]] || missing+=("CI_MNEMONIC_1")
50+
[[ -n "$CI_MNEMONIC_2" ]] || missing+=("CI_MNEMONIC_2")
51+
[[ -n "$CI_MNEMONIC_3" ]] || missing+=("CI_MNEMONIC_3")
52+
[[ -n "$CI_BLOCKFROST_PREPROD_API_KEY" ]] || missing+=("CI_BLOCKFROST_PREPROD_API_KEY")
53+
if [[ "${#missing[@]}" -gt 0 ]]; then
54+
echo "Missing required secrets: ${missing[*]}"
55+
echo "Set these in repo settings before running the Playwright browser workflow."
56+
exit 1
57+
fi
58+
59+
- name: Pull base images (with retry)
60+
shell: bash
61+
run: |
62+
for i in 1 2 3; do
63+
docker pull node:20-alpine && break
64+
echo "Pull attempt $i failed, retrying in 30s..."
65+
sleep 30
66+
done
67+
68+
- name: Build CI containers
69+
shell: bash
70+
run: docker compose -f docker-compose.playwright.yml build app bootstrap-runner playwright-runner
71+
72+
- name: Start Postgres + App containers
73+
shell: bash
74+
run: docker compose -f docker-compose.playwright.yml up -d postgres app
75+
76+
- name: Wait for app healthcheck
77+
shell: bash
78+
run: |
79+
for i in {1..60}; do
80+
status=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$(docker compose -f docker-compose.playwright.yml ps -q app)")
81+
if [[ "$status" == "healthy" ]]; then
82+
echo "App is healthy."
83+
exit 0
84+
fi
85+
sleep 2
86+
done
87+
echo "App failed to become healthy in time."
88+
docker compose -f docker-compose.playwright.yml ps
89+
exit 1
90+
91+
- name: Run CI wallet bootstrap
92+
shell: bash
93+
run: |
94+
docker compose -f docker-compose.playwright.yml --profile playwright \
95+
run --rm bootstrap-runner
96+
97+
- name: Run Playwright ring transfer tests
98+
shell: bash
99+
run: |
100+
docker compose -f docker-compose.playwright.yml --profile playwright \
101+
run --rm --no-deps playwright-runner
102+
103+
- name: Dump container logs on failure
104+
if: failure()
105+
shell: bash
106+
run: |
107+
docker compose -f docker-compose.playwright.yml logs --no-color \
108+
| sed -E 's/(Bearer )[A-Za-z0-9._-]+/\1[REDACTED]/g' \
109+
| sed -E 's/("token"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\2/g' \
110+
| sed -E 's/("secret"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\2/g' \
111+
| sed -E 's/("mnemonic([[:alnum:]_-]*)?"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\3/gI' \
112+
| sed -E 's/("private([[:alnum:]_-]*)?key([[:alnum:]_-]*)?"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\3/gI' \
113+
| sed -E 's/("signing([[:alnum:]_-]*)?key([[:alnum:]_-]*)?"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\3/gI' \
114+
| sed -E 's/("seed([[:alnum:]_-]*)?"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\3/gI' \
115+
| sed -E 's/("xprv([[:alnum:]_-]*)?"[[:space:]]*:[[:space:]]*")[^"]+(")/\1[REDACTED]\3/gI' \
116+
| sed -E 's/(ed25519e?_sk[[:alnum:]_]+)/[REDACTED]/gI' \
117+
| sed -E 's/(xprv[[:alnum:]]+)/[REDACTED]/gI' \
118+
> docker-compose-playwright.log
119+
120+
- name: Upload logs on failure
121+
if: failure()
122+
uses: actions/upload-artifact@v4
123+
with:
124+
name: docker-compose-playwright-logs
125+
path: docker-compose-playwright.log
126+
127+
- name: Upload Playwright report
128+
if: always()
129+
uses: actions/upload-artifact@v4
130+
with:
131+
name: playwright-report
132+
path: ci-artifacts/playwright-report/
133+
if-no-files-found: warn
134+
135+
- name: Upload Playwright traces on failure
136+
if: failure()
137+
uses: actions/upload-artifact@v4
138+
with:
139+
name: playwright-traces
140+
path: ci-artifacts/playwright-traces/
141+
if-no-files-found: ignore
142+
143+
- name: Tear down containers
144+
if: always()
145+
shell: bash
146+
run: docker compose -f docker-compose.playwright.yml down -v --remove-orphans

Dockerfile.ci

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ EXPOSE 3000
4949
# --- Application image: production build served with `next start` ---------------
5050
FROM base AS app
5151

52+
# NEXT_PUBLIC_* vars are inlined into the client bundle by `next build`, so the
53+
# browser-driven Playwright flow passes them as build args.
54+
ARG NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD=
55+
ARG NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET=
56+
ARG NEXT_PUBLIC_NETWORK_ID=0
57+
ENV NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD=$NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD
58+
ENV NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET=$NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET
59+
ENV NEXT_PUBLIC_NETWORK_ID=$NEXT_PUBLIC_NETWORK_ID
60+
5261
# Build the production app so the smoke runs `next start` (not `next dev`) and
5362
# exercises the same output Vercel deploys. `next dev` mis-resolves the
5463
# @meshsdk/core-csl / whisky-evaluator WASM path at runtime, 500-ing tx routes.

Dockerfile.playwright

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FROM mcr.microsoft.com/playwright:v1.60.0-jammy
2+
3+
WORKDIR /app
4+
5+
# Install app dependencies (needed for @meshsdk/* imports in Phase 2 helpers).
6+
COPY package.json package-lock.json* ./
7+
COPY prisma ./prisma
8+
RUN npm ci
9+
10+
# Install Chromium and its system dependencies.
11+
RUN npx playwright install chromium --with-deps
12+
13+
# Copy e2e suite and the CI framework types it imports.
14+
COPY e2e/ ./e2e/
15+
COPY scripts/ci/framework/ ./scripts/ci/framework/
16+
COPY tsconfig*.json ./

docker-compose.playwright.yml

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
services:
2+
postgres:
3+
image: postgres:14-alpine
4+
environment:
5+
POSTGRES_USER: postgres
6+
POSTGRES_PASSWORD: postgres
7+
POSTGRES_DB: multisig
8+
volumes:
9+
- postgres-playwright-data:/var/lib/postgresql/data
10+
- ./docker/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
11+
healthcheck:
12+
test: ["CMD-SHELL", "pg_isready -U postgres"]
13+
interval: 5s
14+
timeout: 5s
15+
retries: 20
16+
networks:
17+
- multisig-playwright-network
18+
19+
app:
20+
build:
21+
context: .
22+
dockerfile: Dockerfile.ci
23+
target: app
24+
# `next build` inlines NEXT_PUBLIC_* into the client bundle, so the
25+
# browser flow needs them at build time, not just at runtime. If any of
26+
# these change in .env.playwright, rebuild the app image.
27+
args:
28+
NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
29+
# Preprod currently requires this public env var even when the test
30+
# only uses network 0, so reuse the preprod key to satisfy validation.
31+
NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
32+
NEXT_PUBLIC_NETWORK_ID: ${CI_NETWORK_ID:-0}
33+
environment:
34+
NODE_ENV: test
35+
NEXT_TELEMETRY_DISABLED: "1"
36+
SKIP_ENV_VALIDATION: "true"
37+
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/multisig
38+
DIRECT_URL: postgresql://postgres:postgres@postgres:5432/multisig
39+
JWT_SECRET: ${CI_JWT_SECRET}
40+
CORS_ORIGINS: http://webapp:3000,http://localhost:3000
41+
NEXT_PUBLIC_NETWORK_ID: ${CI_NETWORK_ID:-0}
42+
NEXT_PUBLIC_BLOCKFROST_API_KEY_PREPROD: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
43+
NEXT_PUBLIC_BLOCKFROST_API_KEY_MAINNET: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
44+
BLOCKFROST_API_KEY_PREPROD: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
45+
depends_on:
46+
postgres:
47+
condition: service_healthy
48+
networks:
49+
multisig-playwright-network:
50+
# "app" matches the .app HSTS-preloaded gTLD — Chromium hard-redirects
51+
# all http://app:* to https:// before the request leaves the browser.
52+
# "webapp" is not on the preload list, so Playwright can reach it over HTTP.
53+
aliases:
54+
- webapp
55+
# Serve the production build, like docker-compose.ci.yml. `next dev` cannot
56+
# serve this app: Turbopack dev bundles @sidan-lab/whisky-js-nodejs despite
57+
# serverExternalPackages (breaking its WASM path, 500-ing every SSR page),
58+
# and webpack dev chokes on react-refresh's import.meta injection into the
59+
# CJS @harmoniclabs/cbor dist under the transpiled @meshsdk/react.
60+
command: >
61+
sh -c "
62+
echo 'Waiting for PostgreSQL to be ready...' &&
63+
until pg_isready -h postgres -p 5432 -U postgres; do sleep 1; done &&
64+
echo 'Running Prisma migrations...' &&
65+
npx prisma migrate deploy || npx prisma db push &&
66+
echo 'Starting application (production build)...' &&
67+
node_modules/.bin/next start --hostname 0.0.0.0 --port 3000
68+
"
69+
healthcheck:
70+
test:
71+
- CMD-SHELL
72+
- node -e "fetch('http://localhost:3000/api/swagger').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
73+
interval: 5s
74+
timeout: 5s
75+
retries: 30
76+
77+
bootstrap-runner:
78+
build:
79+
context: .
80+
dockerfile: Dockerfile.ci
81+
target: base
82+
environment:
83+
NODE_ENV: test
84+
NEXT_TELEMETRY_DISABLED: "1"
85+
SKIP_ENV_VALIDATION: "true"
86+
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/multisig
87+
DIRECT_URL: postgresql://postgres:postgres@postgres:5432/multisig
88+
API_BASE_URL: http://webapp:3000
89+
CI_NETWORK_ID: ${CI_NETWORK_ID:-0}
90+
CI_NUM_REQUIRED_SIGNERS: ${CI_NUM_REQUIRED_SIGNERS:-2}
91+
CI_JWT_SECRET: ${CI_JWT_SECRET}
92+
CI_MNEMONIC_1: ${CI_MNEMONIC_1:-}
93+
CI_MNEMONIC_2: ${CI_MNEMONIC_2:-}
94+
CI_MNEMONIC_3: ${CI_MNEMONIC_3:-}
95+
CI_BLOCKFROST_PREPROD_API_KEY: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
96+
CI_WALLET_TYPES: ${CI_WALLET_TYPES:-legacy,hierarchical,sdk}
97+
CI_CONTEXT_PATH: /artifacts/ci-wallet-context.json
98+
depends_on:
99+
app:
100+
condition: service_healthy
101+
networks:
102+
- multisig-playwright-network
103+
volumes:
104+
- ./ci-artifacts:/artifacts
105+
profiles:
106+
- playwright
107+
command: node .ci-dist/bootstrap.mjs
108+
109+
playwright-runner:
110+
build:
111+
context: .
112+
dockerfile: Dockerfile.playwright
113+
# Three Chromium instances run in parallel (one per ring-transfer leg);
114+
# Docker's default 64MB /dev/shm makes Chromium crash under that load.
115+
shm_size: 1gb
116+
environment:
117+
APP_URL: http://webapp:3000
118+
PLAYWRIGHT_WORKERS: ${PLAYWRIGHT_WORKERS:-3}
119+
CI_CONTEXT_PATH: /artifacts/ci-wallet-context.json
120+
CI_JWT_SECRET: ${CI_JWT_SECRET}
121+
CI_MNEMONIC_1: ${CI_MNEMONIC_1:-}
122+
CI_MNEMONIC_2: ${CI_MNEMONIC_2:-}
123+
CI_MNEMONIC_3: ${CI_MNEMONIC_3:-}
124+
CI_BLOCKFROST_PREPROD_API_KEY: ${CI_BLOCKFROST_PREPROD_API_KEY:-}
125+
CI_TRANSFER_LOVELACE: ${CI_TRANSFER_LOVELACE:-2000000}
126+
PLAYWRIGHT_HTML_REPORT: /artifacts/playwright-report
127+
PLAYWRIGHT_OUTPUT_DIR: /artifacts/playwright-traces
128+
depends_on:
129+
bootstrap-runner:
130+
condition: service_completed_successfully
131+
app:
132+
condition: service_healthy
133+
networks:
134+
- multisig-playwright-network
135+
volumes:
136+
- ./ci-artifacts:/artifacts
137+
# Mount e2e sources and CI framework at runtime so local edits are picked
138+
# up immediately without rebuilding the image. Docker's BuildKit layer
139+
# cache on Windows often fails to detect file changes inside COPY layers,
140+
# so this is the reliable alternative.
141+
- ./e2e:/app/e2e:ro
142+
- ./scripts/ci/framework:/app/scripts/ci/framework:ro
143+
profiles:
144+
- playwright
145+
command: npx playwright test --config=e2e/playwright.config.ts
146+
147+
volumes:
148+
postgres-playwright-data:
149+
150+
networks:
151+
multisig-playwright-network:
152+
driver: bridge

0 commit comments

Comments
 (0)