diff --git a/e2e/specs/editor.spec.ts b/e2e/specs/editor.spec.ts
index 1227f4fc96..cde490a45b 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,9 +171,357 @@ test.describe('editor page', () => {
await expectHelpLinkOpensNewTab(
page,
context,
- '#help-forum',
+ 'Post on the Forum',
'discourse.processing.org/c/p5js/10'
);
});
});
+
+ test('User can create a functioning sketch with multiple files', async ({
+ page
+ }) => {
+ await page
+ .getByRole('button', { name: 'Open Sketch files navigation' })
+ .click();
+
+ await page
+ .getByRole('button', {
+ name: 'Toggle open/close sketch file options',
+ exact: true
+ })
+ .click();
+
+ await page.getByRole('button', { name: 'add file', exact: true }).click();
+
+ await page.getByRole('textbox', { name: 'Name:' }).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.getByRole('button', { name: 'index.html', exact: true }).click();
+ await page.locator('.editor-holder').click();
+ await page.keyboard.press('ControlOrMeta+F');
+ await page.keyboard.type('');
+ await page.keyboard.press('Enter'); // find it
+ // 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();
+
+ // 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
+ // 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();
+
+ // Move to end of that line and add new line
+ await page.keyboard.press('End');
+ await page.keyboard.press('Enter');
+ // 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
+ });
+ await page.keyboard.press('Enter');
+ // Autocorrect takes care of the closing tag
+ await page.keyboard.type('