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('