From ec524dddf8c19b8d585d2d00d9975106cbe5fee6 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 13 Jul 2026 19:05:37 +0100 Subject: [PATCH 1/8] add .env.e2e --- .env.e2e | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .env.e2e diff --git a/.env.e2e b/.env.e2e new file mode 100644 index 0000000000..72d09cd5aa --- /dev/null +++ b/.env.e2e @@ -0,0 +1,61 @@ +# Environment variables for running the e2e tests (Playwright). +# +# The Playwright config loads this file automatically for the test process. + +# ======== COPIED FROM .env ======== +API_URL=/editor +AWS_ACCESS_KEY= +AWS_REGION= +AWS_SECRET_KEY= +CORS_ALLOW_LOCALHOST=true +EMAIL_VERIFY_SECRET_TOKEN=whatever_you_want_this_to_be_it_only_matters_for_production +EXAMPLE_USER_EMAIL=examples@p5js.org +EXAMPLE_USER_PASSWORD=hellop5js +GG_EXAMPLES_USERNAME=generativedesign +GG_EXAMPLES_EMAIL=benedikt.gross@generative-gestaltung.de +GG_EXAMPLES_PASS=generativedesign +GITHUB_ID= +GITHUB_SECRET= +GOOGLE_ID= (use google+ api) +GOOGLE_SECRET= (use google+ api) +MAILGUN_DOMAIN= +MAILGUN_KEY= +ML5_LIBRARY_USERNAME=ml5 +ML5_LIBRARY_EMAIL=examples@ml5js.org +ML5_LIBRARY_PASS=helloml5 +PORT=8000 +PREVIEW_PORT=8002 +EDITOR_URL=http://localhost:8000 +PREVIEW_URL=http://localhost:8002 +S3_BUCKET= +S3_BUCKET_URL_BASE= +SESSION_SECRET=whatever_you_want_this_to_be_it_only_matters_for_production +TRANSLATIONS_ENABLED=true +UI_ACCESS_TOKEN_ENABLED=true +UPLOAD_LIMIT=250000000 + + +# ======== CHANGES FROM .env for e2e testing: ======== +# Separate database so e2e test data never touches the regular dev database +MONGO_URL=mongodb://localhost:27017/p5js-web-editor-e2e + +# Force no redux devtools +HIDE_REDUX_DEVTOOLS=true + +# --- TEST USER: ---- +# (username is formed with Date.now() to allow for parallel tests) +E2E_TEST_USERNAME_PREFIX=e2e +E2E_TEST_EMAIL_SUFFIX=@example.com +E2E_TEST_PASSWORD=e2e-Test-Password-1 + +# --- EMAIL SETUP: ---- +EMAIL_SENDER=from_e2e_test_run@example.com + +# Send emails over SMTP to a local mail catcher (Mailpit) instead of Mailgun +EMAIL_TRANSPORT=smtp +EMAIL_SMTP_HOST=localhost +EMAIL_SMTP_PORT=1025 +# Mailpit REST API, used by e2e specs to read captured emails +MAILPIT_API_URL=http://localhost:8025 + + From eb974e3e3a8e3f2d6bb19cec557aef8fd1f20e4c Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 13 Jul 2026 19:49:04 +0100 Subject: [PATCH 2/8] use env.e2e when running start:e2e --- index.js | 5 ++++- package.json | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 0bc460ad45..15aae60429 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,10 @@ if (process.env.NODE_ENV === 'production') { require('./dist/server.bundle.js'); require('./dist/previewServer.bundle.js'); } else { - let parsed = require('dotenv').config(); + // ENV_FILE lets scripts point at an alternate env file (e.g. .env.e2e for e2e tests) + let parsed = require('dotenv').config({ + path: process.env.ENV_FILE || '.env' + }); require('@babel/register')({ extensions: ['.js', '.jsx', '.ts', '.tsx'], presets: ['@babel/preset-env', '@babel/preset-typescript'] diff --git a/package.json b/package.json index 0f4581b338..a592679678 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "clean": "rimraf dist", "start": "cross-env BABEL_DISABLE_CACHE=1 NODE_ENV=development nodemon index.js", + "start:e2e": "cross-env ENV_FILE=.env.e2e npm run start", "start:prod": "cross-env NODE_ENV=production node index.js", "lint": "eslint client server --ext .jsx --ext .js --ext .tsx --ext .ts", "lint-fix": "eslint client server --ext .jsx --ext .js --ext .tsx --ext .ts --fix", From e48e1cc0c5f52db6ea2da0156f837036405fb698 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 13 Jul 2026 19:50:13 +0100 Subject: [PATCH 3/8] use start:e2e on CI for e2e tests --- .github/workflows/e2e.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d3603cbbf9..9a6c269c93 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -48,8 +48,6 @@ jobs: node-version: '18.20.x' cache: 'npm' - - name: Create .env file - run: cp .env.example .env - name: Start MongoDB uses: supercharge/mongodb-github-action@1.10.0 @@ -75,7 +73,7 @@ jobs: run: npx playwright install-deps chromium - name: Start app - run: npm start > app.log 2>&1 & + run: npm run start:e2e > app.log 2>&1 & - name: Wait for app to be ready run: | From 135aeaab36fb2280e8cf507b6010bf6f9e104207 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 13 Jul 2026 19:52:23 +0100 Subject: [PATCH 4/8] hide redux devtools for e2e testing --- .env.e2e | 4 ++-- e2e/editor.spec.ts | 2 +- e2e/fixtures.ts | 23 +++++++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 e2e/fixtures.ts diff --git a/.env.e2e b/.env.e2e index 72d09cd5aa..01109dda45 100644 --- a/.env.e2e +++ b/.env.e2e @@ -39,8 +39,8 @@ UPLOAD_LIMIT=250000000 # Separate database so e2e test data never touches the regular dev database MONGO_URL=mongodb://localhost:27017/p5js-web-editor-e2e -# Force no redux devtools -HIDE_REDUX_DEVTOOLS=true +# (Redux DevTools dock is hidden by the shared fixture in e2e/fixtures.ts, +# which stubs the browser extension global — no env var needed) # --- TEST USER: ---- # (username is formed with Date.now() to allow for parallel tests) diff --git a/e2e/editor.spec.ts b/e2e/editor.spec.ts index 99eb31123d..e5450b3397 100644 --- a/e2e/editor.spec.ts +++ b/e2e/editor.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from './fixtures'; test.describe('editor page', () => { test.beforeEach(async ({ page }) => { diff --git a/e2e/fixtures.ts b/e2e/fixtures.ts new file mode 100644 index 0000000000..77fe6dea31 --- /dev/null +++ b/e2e/fixtures.ts @@ -0,0 +1,23 @@ +import { test as base, expect } from '@playwright/test'; + +/** + * Shared e2e test setup. Specs should import { test, expect } from here + * instead of '@playwright/test'. + * + * In development the app renders the Redux DevTools dock monitor whenever the + * browser has no Redux DevTools extension (see showReduxDevTools in + * client/store.ts) — which is always the case in Playwright's clean browser. + * Stubbing the extension global before the app loads keeps the dock monitor + * from rendering over the UI during tests. + */ +export const test = base.extend({ + page: async ({ page }, use) => { + await page.addInitScript(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).__REDUX_DEVTOOLS_EXTENSION__ = () => {}; + }); + await use(page); + } +}); + +export { expect }; From d353d9a446d7afd2407aac6239587077679275da Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 13 Jul 2026 22:50:26 +0100 Subject: [PATCH 5/8] add global setup with pre-test dropping of e2e db's --- e2e/global-setup.ts | 34 ++++++++++++++++++++++++++++++++++ playwright.config.ts | 2 ++ 2 files changed, 36 insertions(+) create mode 100644 e2e/global-setup.ts diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts new file mode 100644 index 0000000000..8f46f715ea --- /dev/null +++ b/e2e/global-setup.ts @@ -0,0 +1,34 @@ +import mongoose from 'mongoose'; + +/** + * Runs once before all e2e tests (see globalSetup in playwright.config.ts). + * Drops the e2e database so every run starts from a clean slate. + * + * MONGO_URL comes from .env.e2e, which playwright.config.ts loads before this + * runs. The app server (npm run start:e2e) points at the same database. + */ +export default async function globalSetup() { + const mongoUrl = process.env.MONGO_URL; + + if (!mongoUrl) { + throw new Error( + 'MONGO_URL is not set — expected it from .env.e2e (loaded by playwright.config.ts).' + ); + } + + // Safety guard: never drop anything that isn't explicitly an e2e database. + // Protects against a shell MONGO_URL (which overrides .env.e2e) pointing at + // a dev or production database. + const dbName = new URL(mongoUrl).pathname.replace(/^\//, ''); + if (!dbName.endsWith('-e2e')) { + throw new Error( + `Refusing to drop database "${dbName}": e2e databases must be named ` + + '"-e2e" (check MONGO_URL in .env.e2e or your shell environment).' + ); + } + + const connection = await mongoose.createConnection(mongoUrl).asPromise(); + await connection.dropDatabase(); + await connection.close(); + console.log(`[e2e global-setup] dropped database "${dbName}"`); +} diff --git a/playwright.config.ts b/playwright.config.ts index 379f33091c..641c7ade6b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -13,6 +13,8 @@ import { defineConfig, devices } from '@playwright/test'; */ export default defineConfig({ testDir: './e2e', + /* Drops the e2e database before each run so tests start from a clean slate */ + globalSetup: './e2e/global-setup', /* Timeout per individual test (w/ before & after hooks). Make CI longer than default 30s to accomodate */ timeout: process.env.CI ? 60_000 : 30_000, /* Run tests in files in parallel */ From 30c010e771ce3f17369eae6f1185e67e0b6a57a3 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 13 Jul 2026 23:17:58 +0100 Subject: [PATCH 6/8] add mailpit for e2e testing email --- .env.e2e | 8 +++----- .github/workflows/e2e.yml | 9 +++++++++ package-lock.json | 20 ++++++++++++++++++++ package.json | 1 + playwright.config.ts | 34 ++++++++++++++++++++++------------ server/utils/mail.ts | 33 +++++++++++++++++++++++++-------- 6 files changed, 80 insertions(+), 25 deletions(-) diff --git a/.env.e2e b/.env.e2e index 01109dda45..02a606866e 100644 --- a/.env.e2e +++ b/.env.e2e @@ -51,11 +51,9 @@ E2E_TEST_PASSWORD=e2e-Test-Password-1 # --- EMAIL SETUP: ---- EMAIL_SENDER=from_e2e_test_run@example.com -# Send emails over SMTP to a local mail catcher (Mailpit) instead of Mailgun +# Send emails over SMTP to a local mail catcher (Mailpit) instead of Mailgun. +# Mailpit's default ports are assumed everywhere (SMTP :1025, HTTP API :8025); +# the server and the specs fall back to them when no env vars are set. EMAIL_TRANSPORT=smtp -EMAIL_SMTP_HOST=localhost -EMAIL_SMTP_PORT=1025 -# Mailpit REST API, used by e2e specs to read captured emails -MAILPIT_API_URL=http://localhost:8025 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 9a6c269c93..5af27fa5ed 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -23,6 +23,15 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest + services: + # Local mail catcher: the app sends verification emails here over SMTP (port 1025) + # and the e2e tests read them back via Mailpit's REST API (port 8025) + mailpit: + image: axllent/mailpit:latest + ports: + - 1025:1025 + - 8025:8025 + steps: - name: Resolve PR head ref if: github.event_name == 'issue_comment' diff --git a/package-lock.json b/package-lock.json index 043700f10b..d5b1abea3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -163,6 +163,7 @@ "@testing-library/react": "^12.1.2", "@types/bcryptjs": "^2.4.6", "@types/classnames": "^2.3.0", + "@types/dotenv": "^6.1.1", "@types/friendly-words": "^1.2.2", "@types/jest": "^29.5.14", "@types/js-cookie": "^3.0.6", @@ -15525,6 +15526,16 @@ "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==", "dev": true }, + "node_modules/@types/dotenv": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz", + "integrity": "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/escodegen": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/escodegen/-/escodegen-0.0.6.tgz", @@ -50266,6 +50277,15 @@ "integrity": "sha512-w5jZ0ee+HaPOaX25X2/2oGR/7rgAQSYII7X7pp0m9KgBfMP7uKfMfTvcpl5Dj+eDBbpxKGiqE+flqDr6XTd2RA==", "dev": true }, + "@types/dotenv": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz", + "integrity": "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/escodegen": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/escodegen/-/escodegen-0.0.6.tgz", diff --git a/package.json b/package.json index a592679678..e9a5cd583e 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "@testing-library/react": "^12.1.2", "@types/bcryptjs": "^2.4.6", "@types/classnames": "^2.3.0", + "@types/dotenv": "^6.1.1", "@types/friendly-words": "^1.2.2", "@types/jest": "^29.5.14", "@types/js-cookie": "^3.0.6", diff --git a/playwright.config.ts b/playwright.config.ts index 641c7ade6b..1141b8ef04 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -4,9 +4,14 @@ import { defineConfig, devices } from '@playwright/test'; * Read environment variables from file. * https://github.com/motdotla/dotenv */ -// import dotenv from 'dotenv'; -// import path from 'path'; -// dotenv.config({ path: path.resolve(__dirname, '.env') }); +import dotenv from 'dotenv'; +import path from 'path'; + +dotenv.config({ path: path.resolve(__dirname, '.env.e2e') }); + +const baseURL = process.env.EDITOR_URL || 'http://localhost:8000'; +// Mailpit's default HTTP API port; SMTP is on :1025 (see server/utils/mail.ts) +const mailPitBaseURL = 'http://localhost:8025'; /** * See https://playwright.dev/docs/test-configuration. @@ -30,12 +35,24 @@ 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('')`. */ - baseURL: 'http://localhost:8000', - + baseURL, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry' }, + /** + * Auto-start Mailpit (local mail catcher for signup/verification emails) if + * it isn't already running; reuses a running instance (e.g. the CI service + * container, or one you started yourself). Requires the mailpit binary + * locally: `brew install mailpit`. Runs on Mailpit's default ports. + */ + webServer: { + command: 'mailpit', + url: `${mailPitBaseURL}/api/v1/info`, + reuseExistingServer: true, + timeout: 15_000 + }, + /* Configure projects for major browsers */ projects: [ { @@ -73,11 +90,4 @@ export default defineConfig({ // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // }, ] - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://localhost:3000', - // reuseExistingServer: !process.env.CI, - // }, }); diff --git a/server/utils/mail.ts b/server/utils/mail.ts index bbe9d4fb37..4925a45663 100644 --- a/server/utils/mail.ts +++ b/server/utils/mail.ts @@ -2,15 +2,32 @@ import nodemailer from 'nodemailer'; import mg from 'nodemailer-mailgun-transport'; import { RenderedMailerData } from '../types/email'; -if (!process.env.MAILGUN_KEY) { - throw new Error('Mailgun key missing'); -} - -const auth = { - api_key: process.env.MAILGUN_KEY, - domain: process.env.MAILGUN_DOMAIN +// When EMAIL_TRANSPORT=smtp, emails are sent over plain SMTP (e.g. to a local +// mail catcher like Mailpit during development and e2e tests) instead of Mailgun. +const useSmtpTransport = process.env.EMAIL_TRANSPORT === 'smtp'; +const SMPT_TRANSPORT_CONFIG = { + host: 'localhost', + port: 1025, + secure: false }; +function createTransport(): nodemailer.Transporter { + if (useSmtpTransport) { + return nodemailer.createTransport(SMPT_TRANSPORT_CONFIG); + } + + if (!process.env.MAILGUN_KEY) { + throw new Error('Mailgun key missing'); + } + + const auth = { + api_key: process.env.MAILGUN_KEY, + domain: process.env.MAILGUN_DOMAIN + }; + + return nodemailer.createTransport(mg({ auth })); +} + /** Mail service class wrapping around mailgun */ class Mail { client: nodemailer.Transporter; @@ -18,7 +35,7 @@ class Mail { sendOptions: Pick; constructor() { - this.client = nodemailer.createTransport(mg({ auth })); + this.client = createTransport(); this.sendOptions = { from: process.env.EMAIL_SENDER }; From 4de1459bbc485c3d65ae5f8fd0c1633938531745 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Mon, 13 Jul 2026 23:42:24 +0100 Subject: [PATCH 7/8] split port for e2e testing vs local dev server to prevent collision & clean up package.json commands and playwright webservers --- .env.e2e | 11 +++++++---- .github/workflows/e2e.yml | 9 ++++++--- e2e/editor.spec.ts | 2 +- package.json | 1 - playwright.config.ts | 39 ++++++++++++++++++++++++++++----------- 5 files changed, 42 insertions(+), 20 deletions(-) diff --git a/.env.e2e b/.env.e2e index 02a606866e..d8a04f141d 100644 --- a/.env.e2e +++ b/.env.e2e @@ -23,10 +23,6 @@ MAILGUN_KEY= ML5_LIBRARY_USERNAME=ml5 ML5_LIBRARY_EMAIL=examples@ml5js.org ML5_LIBRARY_PASS=helloml5 -PORT=8000 -PREVIEW_PORT=8002 -EDITOR_URL=http://localhost:8000 -PREVIEW_URL=http://localhost:8002 S3_BUCKET= S3_BUCKET_URL_BASE= SESSION_SECRET=whatever_you_want_this_to_be_it_only_matters_for_production @@ -39,6 +35,13 @@ UPLOAD_LIMIT=250000000 # Separate database so e2e test data never touches the regular dev database MONGO_URL=mongodb://localhost:27017/p5js-web-editor-e2e +# Dedicated e2e ports (dev uses 8000/8002). Guarantees the Playwright-managed +# server never collides with — or accidentally reuses — a regular dev server. +PORT=9000 +PREVIEW_PORT=9002 +EDITOR_URL=http://localhost:9000 +PREVIEW_URL=http://localhost:9002 + # (Redux DevTools dock is hidden by the shared fixture in e2e/fixtures.ts, # which stubs the browser extension global — no env var needed) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 5af27fa5ed..fefa3ec78a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -81,13 +81,16 @@ jobs: if: steps.playwright-cache.outputs.cache-hit == 'true' run: npx playwright install-deps chromium + # Start the app here (rather than leaving it to Playwright's webServer) + # so its output is captured to app.log for debugging. + # Playwright's reuseExistingServer detects it on :9000 and skips its own start. - name: Start app - run: npm run start:e2e > app.log 2>&1 & + run: ENV_FILE=.env.e2e npm start > app.log 2>&1 & - name: Wait for app to be ready run: | - npx wait-on@7 http://localhost:8000 --timeout 180000 || { - echo "::error::App failed to become ready at http://localhost:8000 within 180s" + npx wait-on@7 http://localhost:9000 --timeout 180000 || { + echo "::error::App failed to become ready at http://localhost:9000 within 180s" echo "----- app.log -----" cat app.log exit 1 diff --git a/e2e/editor.spec.ts b/e2e/editor.spec.ts index e5450b3397..4cdd880aa9 100644 --- a/e2e/editor.spec.ts +++ b/e2e/editor.spec.ts @@ -50,7 +50,7 @@ test.describe('editor page', () => { // Wait for the sketch iframe src to confirm the sketch actually started await expect( page.locator('iframe[title="sketch preview"]') - ).toHaveAttribute('src', /8002/, { timeout: 10_000 }); + ).toHaveAttribute('src', /9002/, { timeout: 10_000 }); // Assert console output await expect( diff --git a/package.json b/package.json index e9a5cd583e..9e3c19d4e2 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "scripts": { "clean": "rimraf dist", "start": "cross-env BABEL_DISABLE_CACHE=1 NODE_ENV=development nodemon index.js", - "start:e2e": "cross-env ENV_FILE=.env.e2e npm run start", "start:prod": "cross-env NODE_ENV=production node index.js", "lint": "eslint client server --ext .jsx --ext .js --ext .tsx --ext .ts", "lint-fix": "eslint client server --ext .jsx --ext .js --ext .tsx --ext .ts --fix", diff --git a/playwright.config.ts b/playwright.config.ts index 1141b8ef04..0897052a5d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,7 +9,7 @@ import path from 'path'; dotenv.config({ path: path.resolve(__dirname, '.env.e2e') }); -const baseURL = process.env.EDITOR_URL || 'http://localhost:8000'; +const baseURL = process.env.EDITOR_URL || 'http://localhost:9000'; // Mailpit's default HTTP API port; SMTP is on :1025 (see server/utils/mail.ts) const mailPitBaseURL = 'http://localhost:8025'; @@ -41,17 +41,34 @@ export default defineConfig({ }, /** - * Auto-start Mailpit (local mail catcher for signup/verification emails) if - * it isn't already running; reuses a running instance (e.g. the CI service - * container, or one you started yourself). Requires the mailpit binary - * locally: `brew install mailpit`. Runs on Mailpit's default ports. + * Servers auto-started before the tests (reused if already running): + * + * 1. Mailpit — local mail catcher for signup/verification emails, on its + * default ports. Requires the binary locally (`brew install mailpit`); + * on CI an already-running service container is reused instead. + * 2. The app — started with .env.e2e config on dedicated ports (9000/9002), + * so reuseExistingServer can never latch onto a regular dev server on + * 8000 (which would point at the real dev database and Mailgun). + * First start compiles webpack from scratch, hence the long timeout. */ - webServer: { - command: 'mailpit', - url: `${mailPitBaseURL}/api/v1/info`, - reuseExistingServer: true, - timeout: 15_000 - }, + webServer: [ + { + name: 'Mailpit', + command: 'mailpit', + url: `${mailPitBaseURL}/api/v1/info`, + reuseExistingServer: true, + timeout: 15_000 + }, + { + name: 'App server', + command: 'npm run start', + env: { ENV_FILE: '.env.e2e' }, + url: baseURL, + reuseExistingServer: true, + timeout: 120_000, + stdout: 'pipe' + } + ], /* Configure projects for major browsers */ projects: [ From 1bde796d73ee6a324ac7ca7aab7eea1353186d89 Mon Sep 17 00:00:00 2001 From: Claire Peng Date: Tue, 14 Jul 2026 00:14:00 +0100 Subject: [PATCH 8/8] add test for signup with email verification --- e2e/signup.spec.ts | 97 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 e2e/signup.spec.ts diff --git a/e2e/signup.spec.ts b/e2e/signup.spec.ts new file mode 100644 index 0000000000..8bdc3e0210 --- /dev/null +++ b/e2e/signup.spec.ts @@ -0,0 +1,97 @@ +import { test, expect } from './fixtures'; + +/** + * Signup + email verification flow. + * + * Emails are captured by Mailpit (https://mailpit.axllent.org/), a local fake + * SMTP server, instead of Mailgun (EMAIL_TRANSPORT=smtp in .env.e2e), on + * Mailpit's default ports (SMTP :1025, HTTP API :8025). + * + * Both Mailpit and the app (with .env.e2e config, on port 9000) are started + * automatically by playwright.config.ts (webServer). The only local one-time + * setup is `brew install mailpit`. On CI, Mailpit runs as a service container + * (see e2e.yml) and is reused. + */ + +// Mailpit's default HTTP API port (also health-checked in playwright.config.ts) +const MAILPIT_API = 'http://localhost:8025/api/v1'; + +async function getVerificationLink(email: string): Promise { + // Email delivery is async, so poll Mailpit until the message arrives + let messageId: string | undefined; + await expect + .poll( + async () => { + const res = await fetch( + `${MAILPIT_API}/search?query=${encodeURIComponent(`to:${email}`)}` + ); + const data = await res.json(); + messageId = data.messages?.[0]?.ID; + return messageId; + }, + { + message: `Expected a verification email to ${email} to arrive in Mailpit`, + timeout: 15_000 + } + ) + .toBeTruthy(); + + const res = await fetch(`${MAILPIT_API}/message/${messageId}`); + const message = await res.json(); + const match = (message.HTML as string).match( + /href="(http[^"]*\/verify\?t=[^"]+)"/ + ); + expect( + match, + 'Expected the email to contain a /verify?t= link' + ).toBeTruthy(); + return match![1]; +} + +test.describe('signup and email verification', () => { + test('can sign up and verify email via the emailed link', async ({ + page + }) => { + // Unique per run so the duplicate username/email check passes + const uniqueId = `e2e${Date.now()}`; + const email = `${uniqueId}@example.com`; + const password = 'e2e-Test-Password-1'; + + await page.goto('/signup'); + + // Dismiss cookie banner if it appears (see editor.spec.ts for why querySelectorAll) + await page.evaluate(() => { + const btn = Array.from(document.querySelectorAll('button')).find((b) => + /allow essential|allow all/i.test(b.textContent ?? '') + ) as HTMLElement | undefined; + btn?.click(); + }); + + await page.locator('#username').fill(uniqueId); + await page.locator('#email').fill(email); + await page.locator('#password').fill(password); + await page.locator('#confirmPassword').fill(password); + + // The submit button stays disabled until the async duplicate + // username/email check resolves; click() auto-waits for enabled + await page.getByRole('button', { name: 'Sign Up', exact: true }).click(); + + // Successful signup logs the user in and redirects to the editor + await expect(page.locator('.editor-holder')).toBeVisible({ + timeout: 15_000 + }); + + // "Receive" the verification email and click its link + const verificationLink = await getVerificationLink(email); + await page.goto(verificationLink); + + // On success the verify page flashes a message for ~1s and then redirects + // home, so asserting on the message races the redirect. The redirect only + // happens for a valid token (invalid tokens stay on /verify showing an + // error), so assert the redirect, then the durable outcome via the API. + await expect(page).toHaveURL('/', { timeout: 15_000 }); + + const session = await page.request.get('/editor/session'); + expect((await session.json()).verified).toBe('verified'); + }); +});