Skip to content

GSOC-E2E-1: Add Playwright CI workflow and sketch execution test#4172

Open
Geethegreat wants to merge 9 commits into
processing:developfrom
Geethegreat:feat/e2e-clean
Open

GSOC-E2E-1: Add Playwright CI workflow and sketch execution test#4172
Geethegreat wants to merge 9 commits into
processing:developfrom
Geethegreat:feat/e2e-clean

Conversation

@Geethegreat

@Geethegreat Geethegreat commented Jun 12, 2026

Copy link
Copy Markdown
Member

Issue:

Fixes #

Demo:

video.-.Trim.mp4

Changes:

This PR introduces an initial Playwright E2E testing setup for the p5.js Web Editor.

  • Added Playwright configuration (playwright.config.ts)
  • Added GitHub Actions workflow for Playwright E2E execution
  • Added initial Playwright E2E tests covering:
  • Editor loading
  • Preview iframe creation
  • Iframe access
  • Sketch execution and console output validation

I have verified that this pull request:

  • has no linting errors (npm run lint)
  • has no test errors (npm run test)
  • has no typecheck errors (npm run typecheck)
  • is from a uniquely-named feature branch and is up to date with the develop branch.
  • is descriptively named and links to an issue number, i.e. Fixes #123
  • meets the standards outlined in the accessibility guidelines

@clairep94 clairep94 self-requested a review June 14, 2026 19:46
Comment thread .github/workflows/e2e.yml
Comment thread .github/workflows/e2e.yml Outdated
Comment on lines +27 to +77
- name: Create .env file
run: |
cat > .env << 'EOF'
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
EOF

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this into a file next to .env.local called .env.e2e instead and then do the following:

Suggested change
- name: Create .env file
run: |
cat > .env << 'EOF'
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
EOF
- name: Create .env file
run: cp .env.e2e .env

This makes the e2e .env file way easier to maintain

Comment thread .github/workflows/e2e.yml Outdated
Comment thread .github/workflows/e2e.yml Outdated
with:
name: playwright-report
path: playwright-report/
retention-days: 7 No newline at end of file

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to add a new-line at the end of this file

Comment thread playwright.config.ts Outdated
Comment thread playwright.config.ts Outdated

use: {
baseURL: 'http://localhost:8000',
headless: true,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this and see update for package.json commands

Comment thread package.json
Comment on lines 17 to 18
"test:ci": "npm run lint && npm run test",
"fetch-examples": "cross-env NODE_ENV=development node ./server/scripts/fetch-examples.js",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the below new scripts:

  "e2e": "playwright test --headed", 
  "e2e:ci": "playwright test", 

Then update how the workflow calls the e2e test with the e2e:ci command instead
Then add docs for devs on how to run these tests

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally can you update .gitignore to ignore the artifacts playwright generates?

Add the below:

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

Comment thread tests/playwright/editor.spec.ts Outdated
Comment on lines +3 to +152
async function dismissCookies(page: Page) {
try {
await page.waitForSelector('button', { timeout: 3_000 });
await page.evaluate(() => {
const btn = Array.from(document.querySelectorAll('button')).find((b) =>
/allow all|allow essential/i.test(b.textContent ?? '')
) as HTMLElement | undefined;
btn?.click();
});
await page.waitForTimeout(400);
} catch {
/* no banner */
}
}

test.describe('p5.js Editor – Playwright E2E', () => {
test('editor loads and has a sketch iframe', async ({ page }) => {
await page.goto('http://localhost:8000', {
waitUntil: 'domcontentloaded',
timeout: 30_000
});
await dismissCookies(page);
await page.evaluate(() => {
(document.querySelector(
'[aria-label="Play sketch"]'
) as HTMLElement)?.click();
});
const iframeHandle = await page.waitForSelector('iframe', {
timeout: 15_000
});
expect(iframeHandle).toBeTruthy();
});

test('can access iframe content frame', async ({ page }) => {
await page.goto('http://localhost:8000');
await dismissCookies(page);
await page.waitForSelector('iframe');
const body = page.frameLocator('iframe').first().locator('body');
await expect(body).toBeAttached({ timeout: 10_000 });
});

test('run button triggers sketch in iframe', async ({ page }) => {
await page.goto('http://localhost:8000');
await dismissCookies(page);
await page.evaluate(() => {
(document.querySelector(
'[aria-label="Play sketch"]'
) as HTMLElement)?.click();
});
await page.waitForSelector('iframe', { timeout: 10_000 });
const iframeSrc = await page.locator('iframe').getAttribute('src');
console.log('iframe src:', iframeSrc);
expect(iframeSrc).toBeTruthy();
await expect(page.locator('iframe')).toBeVisible({ timeout: 10_000 });
});

test.skip('sketch execution via postMessage', async () => {
// FINDING: postMessage interception via page.evaluate() returns empty.
// The sketch iframe (localhost:8002) sends messages via window.parent.parent
// but these do not surface in Playwright's main page context.
// Testing sketch output would require CDP or a dedicated message relay
// — a candidate for GSoC implementation.
});

test('sketch console.log appears in editor console', async ({ page }) => {
await page.goto('http://localhost:8000');

await dismissCookies(page);

await page.waitForFunction(() => {
const wrapper = document.querySelector('.CodeMirror') as any;
return !!wrapper?.CodeMirror;
});

await page.evaluate(
(newCode) => {
const cm = (document.querySelector('.CodeMirror') as any)?.CodeMirror;

if (!cm) {
throw new Error('CodeMirror not found');
}

cm.setValue(newCode);
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('Selected file not found');
}

store.dispatch({
type: 'UPDATE_FILE_CONTENT',
id: selectedFile.id,
content: newCode
});
},
`
function setup() {
createCanvas(400, 400);
}

function draw() {
background(220);
console.log('hi from sketch');
noLoop();
}
`
);

await page.waitForTimeout(1000);

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

const openConsoleButton = page.locator('[aria-label="Open console"]');

if (await openConsoleButton.isVisible()) {
await openConsoleButton.click();
}

await expect
.poll(() => page.locator('.preview-console__messages').textContent(), {
timeout: 15000
})
.toContain('hi from sketch');
});

@clairep94 clairep94 Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Geeth I think you have too many test blocks here.

This first initial test should just be something like "Can run sketch code from the editor"
I haven't run the below locally, so you may need to edit this, but it would also be helpful to add some comments on what the bot is doing

Remember that this is an e2e test, so you want to describe things that a human user would do, rather than describe the technical internals of the test code

Suggested change
async function dismissCookies(page: Page) {
try {
await page.waitForSelector('button', { timeout: 3_000 });
await page.evaluate(() => {
const btn = Array.from(document.querySelectorAll('button')).find((b) =>
/allow all|allow essential/i.test(b.textContent ?? '')
) as HTMLElement | undefined;
btn?.click();
});
await page.waitForTimeout(400);
} catch {
/* no banner */
}
}
test.describe('p5.js Editor – Playwright E2E', () => {
test('editor loads and has a sketch iframe', async ({ page }) => {
await page.goto('http://localhost:8000', {
waitUntil: 'domcontentloaded',
timeout: 30_000
});
await dismissCookies(page);
await page.evaluate(() => {
(document.querySelector(
'[aria-label="Play sketch"]'
) as HTMLElement)?.click();
});
const iframeHandle = await page.waitForSelector('iframe', {
timeout: 15_000
});
expect(iframeHandle).toBeTruthy();
});
test('can access iframe content frame', async ({ page }) => {
await page.goto('http://localhost:8000');
await dismissCookies(page);
await page.waitForSelector('iframe');
const body = page.frameLocator('iframe').first().locator('body');
await expect(body).toBeAttached({ timeout: 10_000 });
});
test('run button triggers sketch in iframe', async ({ page }) => {
await page.goto('http://localhost:8000');
await dismissCookies(page);
await page.evaluate(() => {
(document.querySelector(
'[aria-label="Play sketch"]'
) as HTMLElement)?.click();
});
await page.waitForSelector('iframe', { timeout: 10_000 });
const iframeSrc = await page.locator('iframe').getAttribute('src');
console.log('iframe src:', iframeSrc);
expect(iframeSrc).toBeTruthy();
await expect(page.locator('iframe')).toBeVisible({ timeout: 10_000 });
});
test.skip('sketch execution via postMessage', async () => {
// FINDING: postMessage interception via page.evaluate() returns empty.
// The sketch iframe (localhost:8002) sends messages via window.parent.parent
// but these do not surface in Playwright's main page context.
// Testing sketch output would require CDP or a dedicated message relay
// — a candidate for GSoC implementation.
});
test('sketch console.log appears in editor console', async ({ page }) => {
await page.goto('http://localhost:8000');
await dismissCookies(page);
await page.waitForFunction(() => {
const wrapper = document.querySelector('.CodeMirror') as any;
return !!wrapper?.CodeMirror;
});
await page.evaluate(
(newCode) => {
const cm = (document.querySelector('.CodeMirror') as any)?.CodeMirror;
if (!cm) {
throw new Error('CodeMirror not found');
}
cm.setValue(newCode);
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('Selected file not found');
}
store.dispatch({
type: 'UPDATE_FILE_CONTENT',
id: selectedFile.id,
content: newCode
});
},
`
function setup() {
createCanvas(400, 400);
}
function draw() {
background(220);
console.log('hi from sketch');
noLoop();
}
`
);
await page.waitForTimeout(1000);
await page.locator('#play-sketch').click();
const openConsoleButton = page.locator('[aria-label="Open console"]');
if (await openConsoleButton.isVisible()) {
await openConsoleButton.click();
}
await expect
.poll(() => page.locator('.preview-console__messages').textContent(), {
timeout: 15000
})
.toContain('hi from sketch');
});
test.beforeEach(async ({ page }) => {
// go to homepage
await page.goto('/');
// close cookie dialog if it appears
const cookieDialog = page.getByRole('dialog');
if (await cookieDialog.isVisible({ timeout: 3_000 }).catch(() => false)) {
await cookieDialog.getByText(/allow essential/i).click();
await cookieDialog.waitFor({ state: 'hidden' });
}
});
test.describe('p5.js Editor – Playwright E2E', () => {
test('can execute code from the editor by clicking the Play button', async ({ page }) => {
const editor = page.locator('.CodeMirror');
await editor.click();
// type in editor
// add a console log that we should see in the terminal
await page.keyboard.press('Control+A');
await page.keyboard.type(
[
'function setup() {',
' createCanvas(400, 400);',
'}',
'',
'function draw() {',
' background(220);',
" console.log('hi from sketch');",
' noLoop();',
'}'
].join('\n'),
{ delay: 5 }
);
// click Play button
await page.getByLabel('Play sketch').click();
// check that console log from editor code is shown in editor console
const openConsole = page.getByLabel('Open console');
if (await openConsole.isVisible().catch(() => false)) {
await openConsole.click();
}
await expect(page.locator('.preview-console__messages')).toContainText(
'hi from sketch',
{ timeout: 15_000 }
);
// not sure if there's a way to find the iframe, check the src and verify that its' sourcecode comes from the editor? In order to verify that the iframe also displays the code from the editor.
});

@clairep94 clairep94 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Geeth great start!
Some small bits of clean up for your setup code + some suggestions for the first E2e test

Add new environmnet for e2e rather than using staging.

Co-authored-by: Claire Peng <128436909+clairep94@users.noreply.github.com>
replace npm install with npm ci

Co-authored-by: Claire Peng <128436909+clairep94@users.noreply.github.com>
add white space

Co-authored-by: Claire Peng <128436909+clairep94@users.noreply.github.com>
@clairep94 clairep94 changed the title Add Playwright CI workflow and sketch execution test GSOC-E2E-1: Add Playwright CI workflow and sketch execution test Jun 15, 2026
@clairep94

Copy link
Copy Markdown
Collaborator

Could we also pre-fix all the GSOC E2e PRs with "GSOC-E2E-"?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants