From 15e1f6de892fb0b119f5d5e12ea1e220cc949ff2 Mon Sep 17 00:00:00 2001 From: Srikrishnageeth Kuppa Date: Sat, 8 Aug 2026 16:57:34 +0530 Subject: [PATCH 1/7] add multifile sketch test --- e2e/specs/editor.spec.ts | 114 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/e2e/specs/editor.spec.ts b/e2e/specs/editor.spec.ts index 1227f4fc96..def201c155 100644 --- a/e2e/specs/editor.spec.ts +++ b/e2e/specs/editor.spec.ts @@ -163,4 +163,118 @@ test.describe('editor page', () => { ); }); }); + + test('User can create a functioning sketch with multiple files', async ({ + page + }) => { + await page + .locator('button[aria-label="Open Sketch files navigation"]') + .click(); + + await page + .locator('button[aria-label="Toggle open/close sketch file options"]') + .click(); + + await page.locator('button[aria-label="add file"]').click(); + + await page.locator('.new-file-form__name-input').fill('fileA.js'); + + await page.keyboard.press('Enter'); + + const fileACode = [ + 'function fileA(){', + ' console.log("log from file A");', + '}' + ].join(''); + + await page.waitForTimeout(1000); + + await page.locator('.editor-holder').click(); + await page.keyboard.type(fileACode, { delay: 5 }); + + // Wait for CodeMirror's debounced onChange (1000ms) to commit this + // file's content to Redux before switching tabs. The debounce reads + // "current file" at fire time, not at schedule time, so switching away + // too early makes the pending save misfire against whichever file is + // active when the timer eventually runs, silently dropping this edit. + await page.waitForTimeout(1000); + + // Register the new file in index.html + await page.locator('button[aria-label="index.html"]').click(); + await page.locator('.editor-holder').click(); + await page.keyboard.press('ControlOrMeta+F'); + await page.keyboard.type(''); + await page.keyboard.press('Enter'); // find it + await page.keyboard.press('Escape'); // close search + + // Move to end of that line and add new line + await page.keyboard.press('End'); + await page.keyboard.press('Enter'); + // Autocorrect takes care of the closing tag + await page.keyboard.type(''); + await page.keyboard.press('Enter'); // find it + await page.keyboard.press('Escape'); // close search + + // Move to end of that line and add new line + await page.keyboard.press('End'); + await page.keyboard.press('Enter'); + // Both files live inside folderA, so the src needs the folder-relative + // path — resolvePathToFile() (server/utils/filePath.js) walks from the + // sketch root by name at each path segment, it doesn't search + // recursively, so a bare "fileA.js" won't resolve to a nested file. + // Autocorrect takes care of the closing tag + await page.keyboard.type('', { + delay: 5 + }); + await page.keyboard.press('Enter'); + // Autocorrect takes care of the closing tag + await page.keyboard.type(' tag await page.keyboard.type('', { delay: 5 }); await page.keyboard.press('Enter'); // Autocorrect takes care of the closing tag - await page.keyboard.type(''); await page.keyboard.press('Enter'); // find it - await page.keyboard.press('Escape'); // close search + // Close via the panel's own close button rather than Escape: Escape + // routes through CodeMirror's search keymap first, which only clears + // CM's internal search state and leaves this panel on-screen still + // holding focus, so subsequent keystrokes would land in the search + // field instead of the editor. + await page.getByRole('button', { name: 'close', exact: true }).click(); + await expect(page.locator('.cm-search-panel')).toBeHidden(); // Move to end of that line and add new line await page.keyboard.press('End'); @@ -386,7 +392,13 @@ test.describe('editor page', () => { await page.keyboard.press('ControlOrMeta+F'); await page.keyboard.type(''); await page.keyboard.press('Enter'); // find it - await page.keyboard.press('Escape'); // close search + // Close via the panel's own close button rather than Escape: Escape + // routes through CodeMirror's search keymap first, which only clears + // CM's internal search state and leaves this panel on-screen still + // holding focus, so subsequent keystrokes would land in the search + // field instead of the editor. + await page.getByRole('button', { name: 'close', exact: true }).click(); + await expect(page.locator('.cm-search-panel')).toBeHidden(); // Move to end of that line and add new line await page.keyboard.press('End'); From 1efc251bd2221e470ed878fd5acabeedfdec500a Mon Sep 17 00:00:00 2001 From: Srikrishnageeth Kuppa Date: Wed, 19 Aug 2026 22:38:03 +0530 Subject: [PATCH 5/7] use getByRole where accessible name allows it --- e2e/specs/editor.spec.ts | 199 ++++++++++++++++++++++++--------------- 1 file changed, 121 insertions(+), 78 deletions(-) diff --git a/e2e/specs/editor.spec.ts b/e2e/specs/editor.spec.ts index 8bb4ca2785..57fbc39de3 100644 --- a/e2e/specs/editor.spec.ts +++ b/e2e/specs/editor.spec.ts @@ -7,16 +7,18 @@ test.describe('editor page', () => { await page.goto('/'); // initial pageload check to fail fast: - await expect(page.locator('a.skip_link[href="#play-sketch"]')).toHaveText( - 'Skip to Play Sketch' - ); + await expect( + page.getByRole('link', { name: 'Skip to Play Sketch' }) + ).toBeVisible(); await dismissCookieBanner(page); // wait for page to fully load with all main IDE components: - await expect(page.locator('#play-sketch')).toBeVisible(); // play button - await expect(page.locator('iframe[title="sketch preview"]')).toBeVisible(); // sketch preview - await expect(page.locator('.preview-console')).toBeVisible(); // editor console + await expect( + page.getByRole('button', { name: 'Play only visual sketch' }) + ).toBeVisible(); // play button + await expect(page.getByTitle('sketch preview')).toBeVisible(); // sketch preview + await expect(page.locator('.preview-console')).toBeVisible(); // editor console -- no accessible name/role on this container to search by await expect(page.locator('.editor-holder')).toBeVisible(); // editor -- NOTE: .editor-holder .CodeMirror cannot be found on CI for some reason, so we are using .editor-holder instead. }); @@ -42,6 +44,9 @@ test.describe('editor page', () => { await page.keyboard.press('ControlOrMeta+A'); await page.keyboard.type(newCode, { delay: 5 }); // Purposely using .type instead of .insert (.insert does not work with the redux state management) + // Scoped to the console: LOG is also present verbatim in the editor's + // own rendered source (it's inside the console.log(...) call we just + // typed), so an unscoped getByText would match both. const logRow = page .locator('.preview-console__messages [data-method="log"]') .filter({ hasText: LOG }); @@ -50,12 +55,16 @@ test.describe('editor page', () => { await expect(logRow).toHaveCount(0); // Click Play (start the loop) - await page.locator('#play-sketch').click({ force: true }); + await page + .getByRole('button', { name: 'Play only visual sketch' }) + .click({ force: true }); // Wait for the sketch iframe src to confirm the sketch actually started - await expect( - page.locator('iframe[title="sketch preview"]') - ).toHaveAttribute('src', /9002/, { timeout: 10_000 }); + await expect(page.getByTitle('sketch preview')).toHaveAttribute( + 'src', + /9002/, + { timeout: 10_000 } + ); // Assert console output await expect(page.locator('.preview-console__messages')).toContainText( @@ -67,7 +76,7 @@ test.describe('editor page', () => { await page.waitForTimeout(2000); // Stop the sketch - await page.locator('[aria-label="Stop sketch"]').click(); + await page.getByRole('button', { name: 'Stop sketch' }).click(); const countDiv = logRow.last().locator('div').first(); const count = Number(await countDiv.textContent()); @@ -86,13 +95,15 @@ test.describe('editor page', () => { // Verify save option is disabled in File menu await page.getByRole('menuitem', { name: 'File' }).click(); - const saveButton = page.locator('#file-save'); + // While unauthenticated, MenubarItem wraps this item in a Tooltip whose + // content becomes its aria-label, overriding the default "Save" text — + // so the accessible name here is the tooltip copy, not "Save". + const saveButton = page.getByRole('menuitem', { + name: 'Log in to save your sketch', + exact: true + }); await expect(saveButton).toHaveAttribute('aria-disabled', 'true'); - await expect(saveButton).toHaveAttribute( - 'aria-label', - 'Log in to save your sketch' - ); // Close menu if needed await page.keyboard.press('Escape'); @@ -117,13 +128,13 @@ test.describe('editor page', () => { const expectHelpLinkOpensNewTab = async ( page: Page, context: BrowserContext, - locatorId: string, + menuItemName: string, urlSubstring: string ) => { await page.getByRole('menuitem', { name: 'Help' }).click(); const [newTab] = await Promise.all([ context.waitForEvent('page'), - page.locator(locatorId).click() + page.getByRole('menuitem', { name: menuItemName, exact: true }).click() ]); await newTab.waitForLoadState(); expect(newTab.url()).toContain(urlSubstring); @@ -132,13 +143,15 @@ test.describe('editor page', () => { test('File > Examples opens in the same tab', async ({ page }) => { await page.getByRole('menuitem', { name: 'File' }).click(); - await page.locator('#file-examples').click(); + await page + .getByRole('menuitem', { name: 'Examples', exact: true }) + .click(); await expect(page).toHaveURL(/\/p5\/sketches/, { timeout: 10_000 }); }); test('Help > About opens in the same tab', async ({ page }) => { await page.getByRole('menuitem', { name: 'Help' }).click(); - await page.locator('#help-about').click(); + await page.getByRole('menuitem', { name: 'About', exact: true }).click(); await expect(page).toHaveURL(/\/about/, { timeout: 10_000 }); }); @@ -146,7 +159,7 @@ test.describe('editor page', () => { await expectHelpLinkOpensNewTab( page, context, - '#help-reference', + 'Reference', 'p5js.org/reference' ); }); @@ -158,7 +171,7 @@ test.describe('editor page', () => { await expectHelpLinkOpensNewTab( page, context, - '#help-forum', + 'Post on the Forum', 'discourse.processing.org/c/p5js/10' ); }); @@ -168,16 +181,19 @@ test.describe('editor page', () => { page }) => { await page - .locator('button[aria-label="Open Sketch files navigation"]') + .getByRole('button', { name: 'Open Sketch files navigation' }) .click(); await page - .locator('button[aria-label="Toggle open/close sketch file options"]') + .getByRole('button', { + name: 'Toggle open/close sketch file options', + exact: true + }) .click(); - await page.locator('button[aria-label="add file"]').click(); + await page.getByRole('button', { name: 'add file', exact: true }).click(); - await page.locator('.new-file-form__name-input').fill('fileA.js'); + await page.getByRole('textbox', { name: 'Name:' }).fill('fileA.js'); await page.keyboard.press('Enter'); @@ -200,7 +216,7 @@ test.describe('editor page', () => { await page.waitForTimeout(1000); // Register the new file in index.html - await page.locator('button[aria-label="index.html"]').click(); + await page.getByRole('button', { name: 'index.html', exact: true }).click(); await page.locator('.editor-holder').click(); await page.keyboard.press('ControlOrMeta+F'); await page.keyboard.type(''); @@ -221,7 +237,7 @@ test.describe('editor page', () => { await page.waitForTimeout(1000); - await page.locator('button[aria-label="sketch.js"]').click(); + await page.getByRole('button', { name: 'sketch.js', exact: true }).click(); const sketchCode = [ 'function setup() {', @@ -241,12 +257,16 @@ test.describe('editor page', () => { await page.waitForTimeout(1000); // Click Play - await page.locator('#play-sketch').click({ force: true }); + await page + .getByRole('button', { name: 'Play only visual sketch' }) + .click({ force: true }); // Wait for the sketch iframe src to confirm the sketch actually started - await expect( - page.locator('iframe[title="sketch preview"]') - ).toHaveAttribute('src', /9002/, { timeout: 10_000 }); + await expect(page.getByTitle('sketch preview')).toHaveAttribute( + 'src', + /9002/, + { timeout: 10_000 } + ); // Assert console output await expect( @@ -255,28 +275,30 @@ test.describe('editor page', () => { page.on('dialog', (dialog) => dialog.accept()); - // Open the file options for fileA.js + // Open the file options for fileA.js — its file-tree button's parent is + // the item's container, giving us the same scope the old + // .file-item__content + filter({ has: ... }) combo did. const fileItem = page - .locator('.file-item__content') - .filter({ has: page.locator('button[aria-label="fileA.js"]') }); + .getByRole('button', { name: 'fileA.js', exact: true }) + .locator('..'); await fileItem.click(); await fileItem - .locator('button[aria-label="Toggle open/close file options"]') + .getByRole('button', { + name: 'Toggle open/close file options', + exact: true + }) .click(); // Delete the file - await fileItem - .locator('button.sidebar__file-item-option') - .filter({ hasText: 'Delete' }) - .click(); + await fileItem.getByRole('button', { name: 'Delete', exact: true }).click(); await expect( - page - .locator('.file-item__content') - .filter({ has: page.locator('button[aria-label="fileA.js"]') }) + page.getByRole('button', { name: 'fileA.js', exact: true }) ).toHaveCount(0); // Play sketch again and verify that the console output from the deleted file is gone - await page.locator('#play-sketch').click({ force: true }); + await page + .getByRole('button', { name: 'Play only visual sketch' }) + .click({ force: true }); // Check for the error message in the console indicating that fileA is not defined await expect( @@ -288,34 +310,40 @@ test.describe('editor page', () => { page }) => { await page - .locator('button[aria-label="Open Sketch files navigation"]') + .getByRole('button', { name: 'Open Sketch files navigation' }) .click(); await page - .locator('button[aria-label="Toggle open/close sketch file options"]') + .getByRole('button', { + name: 'Toggle open/close sketch file options', + exact: true + }) .click(); - await page.locator('button[aria-label="add folder"]').click(); + await page.getByRole('button', { name: 'add folder', exact: true }).click(); - await page.locator('.new-folder-form__name-input').fill('folderA'); + await page.getByRole('textbox', { name: 'Name:' }).fill('folderA'); await page.keyboard.press('Enter'); // Open folder options await page - .locator('button[aria-label="folderA"]') + .getByRole('button', { name: 'folderA', exact: true }) .locator('..') - .locator('button[aria-label="Toggle open/close file options"]') + .getByRole('button', { + name: 'Toggle open/close file options', + exact: true + }) .click(); // Click Create file await page - .locator('button[aria-label="folderA"]') + .getByRole('button', { name: 'folderA', exact: true }) .locator('..') - .locator('button[aria-label="add file"]') + .getByRole('button', { name: 'add file', exact: true }) .click(); - await page.locator('.new-file-form__name-input').fill('fileA.js'); + await page.getByRole('textbox', { name: 'Name:' }).fill('fileA.js'); await page.keyboard.press('Enter'); @@ -338,36 +366,45 @@ test.describe('editor page', () => { // resolvePathToFile() actually respecting folder boundaries (folderA // vs folderB), not just matching a filename found anywhere in the tree. await page - .locator('button[aria-label="Toggle open/close sketch file options"]') + .getByRole('button', { + name: 'Toggle open/close sketch file options', + exact: true + }) .click(); // "add folder" also exists (hidden) as folderA's own per-item option for // creating a subfolder inside it — scope to the visible one to avoid a // strict-mode ambiguity now that folderA exists. - await page.locator('button[aria-label="add folder"]:visible').click(); + await page + .getByRole('button', { name: 'add folder', exact: true }) + .and(page.locator(':visible')) + .click(); - await page.locator('.new-folder-form__name-input').fill('folderB'); + await page.getByRole('textbox', { name: 'Name:' }).fill('folderB'); await page.keyboard.press('Enter'); // The options button is only revealed on hover of the file item row - await page.locator('button[aria-label="folderB"]').hover(); + await page.getByRole('button', { name: 'folderB', exact: true }).hover(); // Open folder options for folderB await page - .locator('button[aria-label="folderB"]') + .getByRole('button', { name: 'folderB', exact: true }) .locator('..') - .locator('button[aria-label="Toggle open/close file options"]') + .getByRole('button', { + name: 'Toggle open/close file options', + exact: true + }) .click(); // Click Create file await page - .locator('button[aria-label="folderB"]') + .getByRole('button', { name: 'folderB', exact: true }) .locator('..') - .locator('button[aria-label="add file"]') + .getByRole('button', { name: 'add file', exact: true }) .click(); - await page.locator('.new-file-form__name-input').fill('fileB.js'); + await page.getByRole('textbox', { name: 'Name:' }).fill('fileB.js'); await page.keyboard.press('Enter'); @@ -386,7 +423,7 @@ test.describe('editor page', () => { await page.waitForTimeout(1000); // Register the new files in index.html - await page.locator('button[aria-label="index.html"]').click(); + await page.getByRole('button', { name: 'index.html', exact: true }).click(); await page.locator('.editor-holder').click(); await page.keyboard.press('ControlOrMeta+F'); @@ -435,7 +472,7 @@ test.describe('editor page', () => { '}' ].join(''); - await page.locator('button[aria-label="sketch.js"]').click(); + await page.getByRole('button', { name: 'sketch.js', exact: true }).click(); await page.locator('.editor-holder').click(); await page.keyboard.press('ControlOrMeta+A'); @@ -444,12 +481,16 @@ test.describe('editor page', () => { await page.waitForTimeout(1000); // Click Play - await page.locator('#play-sketch').click({ force: true }); + await page + .getByRole('button', { name: 'Play only visual sketch' }) + .click({ force: true }); // Wait for the sketch iframe src to confirm the sketch actually started - await expect( - page.locator('iframe[title="sketch preview"]') - ).toHaveAttribute('src', /9002/, { timeout: 10_000 }); + await expect(page.getByTitle('sketch preview')).toHaveAttribute( + 'src', + /9002/, + { timeout: 10_000 } + ); // Assert console output await expect( @@ -461,28 +502,30 @@ test.describe('editor page', () => { page.on('dialog', (dialog) => dialog.accept()); - // Open the file options for fileA.js + // Open the file options for folderA const folderItem = page - .locator('.file-item__content') - .filter({ has: page.locator('button[aria-label="folderA"]') }); + .getByRole('button', { name: 'folderA', exact: true }) + .locator('..'); await folderItem.hover(); await folderItem - .locator('button[aria-label="Toggle open/close file options"]') + .getByRole('button', { + name: 'Toggle open/close file options', + exact: true + }) .click(); // Delete the folder await folderItem - .locator('button.sidebar__file-item-option') - .filter({ hasText: 'Delete' }) + .getByRole('button', { name: 'Delete', exact: true }) .click(); await expect( - page - .locator('.file-item__content') - .filter({ has: page.locator('button[aria-label="folderA"]') }) + page.getByRole('button', { name: 'folderA', exact: true }) ).toHaveCount(0); // Play sketch again and verify that the console output from the deleted file is gone - await page.locator('#play-sketch').click({ force: true }); + await page + .getByRole('button', { name: 'Play only visual sketch' }) + .click({ force: true }); // Check for the error message in the console indicating that fileA is not defined await expect( From 03de2d5be1bca7a100c06a42e20cbefd16cee9ab Mon Sep 17 00:00:00 2001 From: Srikrishnageeth Kuppa Date: Sun, 23 Aug 2026 14:23:25 +0530 Subject: [PATCH 6/7] trim verbose comments --- e2e/specs/editor.spec.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/e2e/specs/editor.spec.ts b/e2e/specs/editor.spec.ts index 57fbc39de3..84e2d6493b 100644 --- a/e2e/specs/editor.spec.ts +++ b/e2e/specs/editor.spec.ts @@ -440,10 +440,7 @@ test.describe('editor page', () => { // Move to end of that line and add new line await page.keyboard.press('End'); await page.keyboard.press('Enter'); - // fileA.js and fileB.js live in separate folders, so each src needs its - // own folder-relative path — resolvePathToFile() (server/utils/filePath.js) - // walks from the sketch root by name at each path segment, it doesn't - // search recursively, so a bare filename won't resolve to a nested file. + // Each src needs its folder-relative path — resolvePathToFile() doesn't search recursively. // Autocorrect takes care of the closing tag await page.keyboard.type('', { delay: 5 @@ -456,10 +453,7 @@ test.describe('editor page', () => { await page.waitForTimeout(1000); - // fileB() is called first: later, after folderA is deleted, fileA() - // throws and halts setup() — calling it second means fileB() still - // runs and logs before that happens, so we can verify folderB's file - // survives folderA's deletion instead of never being reached at all. + // fileB() runs first so its log survives fileA() throwing after folderA is deleted. const sketchCode = [ 'function setup() {', ' createCanvas(400, 400);', From a2d5fe2f27d8ff701f060cf7fcb969445f9961a5 Mon Sep 17 00:00:00 2001 From: Srikrishnageeth Kuppa Date: Sun, 23 Aug 2026 14:29:18 +0530 Subject: [PATCH 7/7] trim search-panel close comment --- e2e/specs/editor.spec.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/e2e/specs/editor.spec.ts b/e2e/specs/editor.spec.ts index 84e2d6493b..cde490a45b 100644 --- a/e2e/specs/editor.spec.ts +++ b/e2e/specs/editor.spec.ts @@ -221,11 +221,7 @@ test.describe('editor page', () => { await page.keyboard.press('ControlOrMeta+F'); await page.keyboard.type(''); await page.keyboard.press('Enter'); // find it - // Close via the panel's own close button rather than Escape: Escape - // routes through CodeMirror's search keymap first, which only clears - // CM's internal search state and leaves this panel on-screen still - // holding focus, so subsequent keystrokes would land in the search - // field instead of the editor. + // Close via the panel's own button — Escape only clears CM's internal search state and leaves this panel focused. await page.getByRole('button', { name: 'close', exact: true }).click(); await expect(page.locator('.cm-search-panel')).toBeHidden(); @@ -429,11 +425,7 @@ test.describe('editor page', () => { await page.keyboard.press('ControlOrMeta+F'); await page.keyboard.type(''); await page.keyboard.press('Enter'); // find it - // Close via the panel's own close button rather than Escape: Escape - // routes through CodeMirror's search keymap first, which only clears - // CM's internal search state and leaves this panel on-screen still - // holding focus, so subsequent keystrokes would land in the search - // field instead of the editor. + // Close via the panel's own button — Escape only clears CM's internal search state and leaves this panel focused. await page.getByRole('button', { name: 'close', exact: true }).click(); await expect(page.locator('.cm-search-panel')).toBeHidden();