Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .env.e2e
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 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=<your-aws-access-key>
AWS_REGION=<your-aws-region>
AWS_SECRET_KEY=<your-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=<your-github-client-id>
GITHUB_SECRET=<your-github-client-secret>
GOOGLE_ID=<your-google-client-id> (use google+ api)
GOOGLE_SECRET=<your-google-client-secret> (use google+ api)
MAILGUN_DOMAIN=<your-mailgun-domain>
MAILGUN_KEY=<your-mailgun-api-key>
ML5_LIBRARY_USERNAME=ml5
ML5_LIBRARY_EMAIL=examples@ml5js.org
ML5_LIBRARY_PASS=helloml5
S3_BUCKET=<your-s3-bucket>
S3_BUCKET_URL_BASE=<alt-for-s3-url>
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

# 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)

# --- 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.
# 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


20 changes: 15 additions & 5 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -48,8 +57,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
Expand All @@ -74,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 start > 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
Expand Down
4 changes: 2 additions & 2 deletions e2e/editor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from '@playwright/test';
import { test, expect } from './fixtures';

test.describe('editor page', () => {
test.beforeEach(async ({ page }) => {
Expand Down Expand Up @@ -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(
Expand Down
23 changes: 23 additions & 0 deletions e2e/fixtures.ts
Original file line number Diff line number Diff line change
@@ -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 };
34 changes: 34 additions & 0 deletions e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -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 ` +
'"<name>-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}"`);
}
97 changes: 97 additions & 0 deletions e2e/signup.spec.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
// 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=<token> 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');
});
});
5 changes: 4 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,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",
Expand Down
Loading
Loading