Skip to content
Open
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
39 changes: 39 additions & 0 deletions .env.e2e
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
API_URL=/editor
CORS_ALLOW_LOCALHOST=true
TRANSLATIONS_ENABLED=true
UI_ACCESS_TOKEN_ENABLED=false
UPLOAD_LIMIT=250000000
PORT=8000
PREVIEW_PORT=8002
EDITOR_URL=http://localhost:8000
PREVIEW_URL=http://localhost:8002
MONGO_URL=mongodb://localhost:27017/p5js-web-editor
SESSION_SECRET=ci-session-secret
EMAIL_VERIFY_SECRET_TOKEN=ci-verify-token
# These accounts are used by the app to serve examples.
# Real values not needed for E2E tests — dummy strings prevent crashes.
EMAIL_SENDER=ci@example.com
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
ML5_LIBRARY_USERNAME=ml5
ML5_LIBRARY_EMAIL=examples@ml5js.org
ML5_LIBRARY_PASS=helloml5
# Mailgun — non-empty string required to pass the startup check in
# server/utils/mail.ts. No emails are sent during E2E tests.
MAILGUN_KEY=dummy-mailgun-key
MAILGUN_DOMAIN=dummy.mailgun.org
# AWS S3 — non-empty strings required. No file uploads in E2E tests.
AWS_ACCESS_KEY=dummy-aws-access-key
AWS_SECRET_KEY=dummy-aws-secret-key
AWS_REGION=us-east-1
S3_BUCKET=dummy-bucket
S3_BUCKET_URL_BASE=https://dummy-bucket.s3.amazonaws.com
# GitHub OAuth — not tested in E2E suite, dummy values prevent crash.
GITHUB_ID=dummy-github-id
GITHUB_SECRET=dummy-github-secret
# Google OAuth — not tested in E2E suite, dummy values prevent crash.
GOOGLE_ID=dummy-google-id
GOOGLE_SECRET=dummy-google-secret
67 changes: 67 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: E2E Tests

on:
pull_request:
branches:
- develop

jobs:
e2e:
name: End-to-end tests
environment: e2e-tests
runs-on: ubuntu-latest
Comment thread
Geethegreat marked this conversation as resolved.

steps:
- uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '18.20.x'
cache: 'npm'

- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.10.0
with:
mongodb-version: '6.0'

- name: Create .env file
run: cp .env.e2e .env

- name: Install dependencies
run: npm ci

- name: Install Playwright browsers
run: npx playwright install --with-deps chromium

- name: Start the app
run: npm run start &

- name: Wait for app to be ready
run: |
for i in $(seq 1 120); do
if curl -sf http://localhost:8000 | grep -q "p5"; then
echo "App fully ready"
exit 0
fi

echo "Still bundling... $i/120"
sleep 5
done

echo "App did not become ready"
exit 1

- name: Run E2E tests
run: npm run e2e:ci
env:
CI: true

- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7

5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ localhost.key
privkey.pem
terraform/.terraform/

# Playwright
test-results/
playwright-report/
.playwright/

storybook-static
duplicates.json

Expand Down
74 changes: 74 additions & 0 deletions package-lock.json

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

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
"test": "NODE_ENV=test jest",
"test:watch": "NODE_ENV=test jest --watch",
"test:ci": "npm run lint && npm run test",
"e2e": "playwright test --headed",
"e2e:ci": "playwright test",
"fetch-examples": "cross-env NODE_ENV=development node ./server/scripts/fetch-examples.js",
"fetch-examples-gg": "cross-env NODE_ENV=development node ./server/scripts/fetch-examples-gg.js",
"fetch-examples-ml5": "cross-env NODE_ENV=development node ./server/scripts/fetch-examples-ml5.js",
Expand Down Expand Up @@ -121,6 +123,7 @@
"@babel/preset-env": "^7.14.7",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.27.1",
"@playwright/test": "^1.60.0",
"@storybook/addon-actions": "^7.6.8",
"@storybook/addon-docs": "^7.6.8",
"@storybook/addon-essentials": "^7.6.8",
Expand Down
18 changes: 18 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './tests/playwright',
testMatch: '**/*.spec.ts',
timeout: 120_000,

use: {
baseURL: 'http://localhost:8000'
},

projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
]
});
99 changes: 99 additions & 0 deletions tests/playwright/editor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { test, expect } from '@playwright/test';

test.describe('p5.js Editor – Playwright E2E', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');

// Wait for the page to be interactive before checking for the banner
await page.waitForSelector('.CodeMirror', { timeout: 30_000 });

// Dismiss cookie banner via JS — handles the case where the button
// is outside the viewport due to the Redux DevTools sidebar
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.waitForTimeout(400);
});

test('can execute code from the editor by clicking the Play button', async ({
page
}) => {
const newCode = [
'function setup() {',
' createCanvas(400, 400);',
'}',
'',
'function draw() {',
' background(220);',
" console.log('hi from sketch');",
' noLoop();',
'}'
].join('\n');

// Wait for CodeMirror to be ready
await page.waitForFunction(
() => {
const wrapper = document.querySelector('.CodeMirror') as any;
return (wrapper?.CodeMirror?.getValue?.() ?? '').length > 0;
},
{ timeout: 30_000 }
);

// Update code via CodeMirror API + Redux dispatch
// (confirmed working approach from earlier diagnostic work)
await page.evaluate((code) => {
const cm = (document.querySelector('.CodeMirror') as any).CodeMirror;
cm.setValue(code);
cm.refresh();

const root = document.querySelector('#root') as any;
const fiberKey = Object.keys(root).find((k) =>
k.startsWith('__reactContainer')
);
let node = root[fiberKey];
let store: any = null;
while (node) {
if (node.memoizedProps?.store) {
store = node.memoizedProps.store;
break;
}
node = node.child;
}
if (!store) throw new Error('Redux store not found');

const selectedFile = store
.getState()
.files.find((f: any) => f.isSelectedFile);
if (!selectedFile) throw new Error('No selected file');

store.dispatch({
type: 'UPDATE_FILE_CONTENT',
id: selectedFile.id,
content: code
});
}, newCode);

await page.waitForTimeout(500);

// Click Play
await page.locator('#play-sketch').click();

// Wait for the sketch iframe to confirm the sketch actually started
await page.waitForFunction(
() =>
Array.from(document.querySelectorAll('iframe')).some((f) =>
(f as HTMLIFrameElement).src.includes('8002')
),
{ timeout: 10_000 }
);

// Assert console output
await expect(
page.locator('.preview-console__messages')
).toContainText('hi from sketch', { timeout: 15_000 });
});
});
Loading