From bb3301cd610950dea8be20223d5d1fdf1f650241 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 3 Sep 2024 21:49:36 +0200 Subject: [PATCH] add playwright eslint plugin, fix tests by rules --- code/.eslintrc.js | 23 +++++ code/e2e-tests/addon-actions.spec.ts | 8 +- code/e2e-tests/addon-controls.spec.ts | 8 +- code/e2e-tests/addon-docs.spec.ts | 41 ++++---- code/e2e-tests/addon-interactions.spec.ts | 24 ++--- code/e2e-tests/addon-toolbars.spec.ts | 2 +- code/e2e-tests/composition.spec.ts | 30 +++--- code/e2e-tests/framework-nextjs.spec.ts | 42 ++++---- code/e2e-tests/framework-svelte.spec.ts | 116 +++++++++++----------- code/e2e-tests/json-files.spec.ts | 2 +- code/e2e-tests/manager.spec.ts | 58 +++++------ code/e2e-tests/module-mocking.spec.ts | 4 +- code/e2e-tests/preview-api.spec.ts | 10 +- code/e2e-tests/save-from-controls.spec.ts | 20 ++-- code/e2e-tests/storybook-hooks.spec.ts | 2 + code/e2e-tests/util.ts | 18 ++-- code/package.json | 1 + code/yarn.lock | 18 +++- 18 files changed, 231 insertions(+), 196 deletions(-) diff --git a/code/.eslintrc.js b/code/.eslintrc.js index 6b27a23c57c6..e7294f412ede 100644 --- a/code/.eslintrc.js +++ b/code/.eslintrc.js @@ -177,5 +177,28 @@ module.exports = { 'local-rules/no-duplicated-error-codes': 'error', }, }, + { + files: ['./e2e-tests/*.ts'], + extends: ['plugin:playwright/recommended'], + rules: { + 'playwright/no-skipped-test': [ + 'warn', + { + allowConditional: true, + }, + ], + 'playwright/no-raw-locators': 'off', // TODO: enable this, requires the UI to actually be accessible + 'playwright/prefer-comparison-matcher': 'error', + 'playwright/prefer-equality-matcher': 'error', + 'playwright/prefer-hooks-on-top': 'error', + 'playwright/prefer-strict-equal': 'error', + 'playwright/prefer-to-be': 'error', + 'playwright/prefer-to-contain': 'error', + 'playwright/prefer-to-have-count': 'error', + 'playwright/prefer-to-have-length': 'error', + 'playwright/require-to-throw-message': 'error', + 'playwright/require-top-level-describe': 'error', + }, + }, ], }; diff --git a/code/e2e-tests/addon-actions.spec.ts b/code/e2e-tests/addon-actions.spec.ts index 6c3fa98034e6..501ed2c3a6a0 100644 --- a/code/e2e-tests/addon-actions.spec.ts +++ b/code/e2e-tests/addon-actions.spec.ts @@ -18,11 +18,11 @@ test.describe('addon-actions', () => { await sbPage.navigateToStory('example/button', 'primary'); const root = sbPage.previewRoot(); - const button = root.locator('button', { hasText: 'Button' }); + const button = root.getByRole('button', { name: 'Button' }); await button.click(); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content', { + const logItem = page.locator('#storybook-panel-root #panel-tab-content', { hasText: 'click', }); await expect(logItem).toBeVisible(); @@ -40,11 +40,11 @@ test.describe('addon-actions', () => { await sbPage.navigateToStory('addons/actions/spies', 'show-spy-on-in-actions'); const root = sbPage.previewRoot(); - const button = root.locator('button', { hasText: 'Button' }); + const button = root.getByRole('button', { name: 'Button' }); await button.click(); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content', { + const logItem = page.locator('#storybook-panel-root #panel-tab-content', { hasText: 'console.log', }); await expect(logItem).toBeVisible(); diff --git a/code/e2e-tests/addon-controls.spec.ts b/code/e2e-tests/addon-controls.spec.ts index 6499ef6c0742..add1ef866820 100644 --- a/code/e2e-tests/addon-controls.spec.ts +++ b/code/e2e-tests/addon-controls.spec.ts @@ -21,9 +21,7 @@ test.describe('addon-controls', () => { await expect(sbPage.previewRoot().locator('button')).toContainText('Hello world'); // Args in URL - await page.waitForTimeout(300); - const url = await page.url(); - await expect(url).toContain('args=label:Hello+world'); + await page.waitForURL((url) => url.search.includes('args=label:Hello+world')); // Boolean toggle: Primary/secondary await expect(sbPage.previewRoot().locator('button')).toHaveCSS( @@ -72,8 +70,8 @@ test.describe('addon-controls', () => { await expect(sbPage.previewRoot().locator('button')).toContainText('Hello world'); await sbPage.viewAddonPanel('Controls'); - const label = await sbPage.panelContent().locator('textarea[name=label]').inputValue(); - await expect(label).toEqual('Hello world'); + const label = sbPage.panelContent().locator('textarea[name=label]'); + await expect(label).toHaveValue('Hello world'); }); test('should set select option when value contains double spaces', async ({ page }) => { diff --git a/code/e2e-tests/addon-docs.spec.ts b/code/e2e-tests/addon-docs.spec.ts index ce3e281c2f26..c7486bc55923 100644 --- a/code/e2e-tests/addon-docs.spec.ts +++ b/code/e2e-tests/addon-docs.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable playwright/no-conditional-expect */ + +/* eslint-disable playwright/no-conditional-in-test */ import { expect, test } from '@playwright/test'; import process from 'process'; import { dedent } from 'ts-dedent'; @@ -94,14 +97,14 @@ test.describe('addon-docs', () => { const toggleCount = await toggles.count(); for (let i = 0; i < toggleCount; i += 1) { - const toggle = await toggles.nth(i); - await toggle.click({ force: true }); + const toggle = toggles.nth(i); + await toggle.click(); } const codes = root.locator('pre.prismjs'); const codeCount = await codes.count(); for (let i = 0; i < codeCount; i += 1) { - const code = await codes.nth(i); + const code = codes.nth(i); const text = await code.innerText(); await expect(text).not.toMatch(/^\(args\) => /); } @@ -132,13 +135,13 @@ test.describe('addon-docs', () => { const toggles = root.locator('.docblock-code-toggle'); // Open up the first and second code toggle (i.e the "Basic" story outside and inside the Stories block) - await (await toggles.nth(0)).click({ force: true }); - await (await toggles.nth(1)).click({ force: true }); + await toggles.nth(0).click(); + await toggles.nth(1).click(); // Check they both say "Basic" const codes = root.locator('pre.prismjs'); - const primaryCode = await codes.nth(0); - const storiesCode = await codes.nth(1); + const primaryCode = codes.nth(0); + const storiesCode = codes.nth(1); await expect(primaryCode).toContainText('Basic'); await expect(storiesCode).toContainText('Basic'); @@ -210,12 +213,12 @@ test.describe('addon-docs', () => { } // Arrange - Get the actual versions - const mdxReactVersion = await root.getByTestId('mdx-react'); - const mdxReactDomVersion = await root.getByTestId('mdx-react-dom'); - const mdxReactDomServerVersion = await root.getByTestId('mdx-react-dom-server'); - const componentReactVersion = await root.getByTestId('component-react'); - const componentReactDomVersion = await root.getByTestId('component-react-dom'); - const componentReactDomServerVersion = await root.getByTestId('component-react-dom-server'); + const mdxReactVersion = root.getByTestId('mdx-react'); + const mdxReactDomVersion = root.getByTestId('mdx-react-dom'); + const mdxReactDomServerVersion = root.getByTestId('mdx-react-dom-server'); + const componentReactVersion = root.getByTestId('component-react'); + const componentReactDomVersion = root.getByTestId('component-react-dom'); + const componentReactDomServerVersion = root.getByTestId('component-react-dom-server'); // Assert - The versions are in the expected range await expect(mdxReactVersion).toHaveText(expectedReactVersionRange); @@ -232,9 +235,9 @@ test.describe('addon-docs', () => { await sbPage.navigateToStory('addons/docs/docs2/resolvedreact', 'docs'); // Arrange - Get the actual versions - const autodocsReactVersion = await root.getByTestId('react'); - const autodocsReactDomVersion = await root.getByTestId('react-dom'); - const autodocsReactDomServerVersion = await root.getByTestId('react-dom-server'); + const autodocsReactVersion = root.getByTestId('react'); + const autodocsReactDomVersion = root.getByTestId('react-dom'); + const autodocsReactDomServerVersion = root.getByTestId('react-dom-server'); // Assert - The versions are in the expected range await expect(autodocsReactVersion).toHaveText(expectedReactVersionRange); @@ -247,9 +250,9 @@ test.describe('addon-docs', () => { await sbPage.navigateToStory('addons/docs/docs2/resolvedreact', 'story'); // Arrange - Get the actual versions - const storyReactVersion = await root.getByTestId('react'); - const storyReactDomVersion = await root.getByTestId('react-dom'); - const storyReactDomServerVersion = await root.getByTestId('react-dom-server'); + const storyReactVersion = root.getByTestId('react'); + const storyReactDomVersion = root.getByTestId('react-dom'); + const storyReactDomServerVersion = root.getByTestId('react-dom-server'); // Assert - The versions are in the expected range await expect(storyReactVersion).toHaveText(expectedReactVersionRange); diff --git a/code/e2e-tests/addon-interactions.spec.ts b/code/e2e-tests/addon-interactions.spec.ts index 4ff7a5b6f977..e2ec2e2ae61c 100644 --- a/code/e2e-tests/addon-interactions.spec.ts +++ b/code/e2e-tests/addon-interactions.spec.ts @@ -24,10 +24,10 @@ test.describe('addon-interactions', () => { await sbPage.navigateToStory('example/page', 'logged-in'); await sbPage.viewAddonPanel('Interactions'); - const welcome = await sbPage.previewRoot().locator('.welcome'); + const welcome = sbPage.previewRoot().locator('.welcome'); await expect(welcome).toContainText('Welcome, Jane Doe!', { timeout: 50000 }); - const interactionsTab = await page.locator('#tabbutton-storybook-interactions-panel'); + const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel'); await expect(interactionsTab).toContainText(/(\d)/); await expect(interactionsTab).toBeVisible(); @@ -36,7 +36,7 @@ test.describe('addon-interactions', () => { await expect(panel).toContainText(/userEvent.click/); await expect(panel).toBeVisible(); - const done = await panel.locator('[data-testid=icon-done]').nth(0); + const done = panel.locator('[data-testid=icon-done]').nth(0); await expect(done).toBeVisible(); }); @@ -57,16 +57,16 @@ test.describe('addon-interactions', () => { await sbPage.viewAddonPanel('Interactions'); // Test initial state - Interactions have run, count is correct and values are as expected - const formInput = await sbPage.previewRoot().locator('#interaction-test-form input'); + const formInput = sbPage.previewRoot().locator('#interaction-test-form input'); await expect(formInput).toHaveValue('final value', { timeout: 50000 }); - const interactionsTab = await page.locator('#tabbutton-storybook-interactions-panel'); + const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel'); await expect(interactionsTab.getByText('3')).toBeVisible(); await expect(interactionsTab).toBeVisible(); await expect(interactionsTab).toBeVisible(); const panel = sbPage.panelContent(); - const runStatusBadge = await panel.locator('[aria-label="Status of the test run"]'); + const runStatusBadge = panel.locator('[aria-label="Status of the test run"]'); await expect(runStatusBadge).toContainText(/Pass/); await expect(panel).toContainText(/"initial value"/); await expect(panel).toContainText(/clear/); @@ -74,18 +74,18 @@ test.describe('addon-interactions', () => { await expect(panel).toBeVisible(); // Test interactions debugger - Stepping through works, count is correct and values are as expected - const interactionsRow = await panel.locator('[aria-label="Interaction step"]'); + const interactionsRow = panel.locator('[aria-label="Interaction step"]'); await interactionsRow.first().isVisible(); - await expect(await interactionsRow.count()).toEqual(3); + await expect(interactionsRow).toHaveCount(3); const firstInteraction = interactionsRow.first(); await firstInteraction.click(); await expect(runStatusBadge).toContainText(/Runs/); await expect(formInput).toHaveValue('initial value'); - const goForwardBtn = await panel.locator('[aria-label="Go forward"]'); + const goForwardBtn = panel.locator('[aria-label="Go forward"]'); await goForwardBtn.click(); await expect(formInput).toHaveValue(''); await goForwardBtn.click(); @@ -94,7 +94,7 @@ test.describe('addon-interactions', () => { await expect(runStatusBadge).toContainText(/Pass/); // Test rerun state (from addon panel) - Interactions have rerun, count is correct and values are as expected - const rerunInteractionButton = await panel.locator('[aria-label="Rerun"]'); + const rerunInteractionButton = panel.locator('[aria-label="Rerun"]'); await rerunInteractionButton.click(); await expect(formInput).toHaveValue('final value'); @@ -107,7 +107,7 @@ test.describe('addon-interactions', () => { await expect(interactionsTab.getByText('3')).toBeVisible(); // Test remount state (from toolbar) - Interactions have rerun, count is correct and values are as expected - const remountComponentButton = await page.locator('[title="Remount component"]'); + const remountComponentButton = page.locator('[title="Remount component"]'); await remountComponentButton.click(); await interactionsRow.first().isVisible(); @@ -132,7 +132,7 @@ test.describe('addon-interactions', () => { await sbPage.deepLinkToStory(storybookUrl, 'addons/interactions/unhandled-errors', 'default'); await sbPage.viewAddonPanel('Interactions'); - const button = await sbPage.previewRoot().locator('button'); + const button = sbPage.previewRoot().locator('button'); await expect(button).toContainText('Button', { timeout: 50000 }); const panel = sbPage.panelContent(); diff --git a/code/e2e-tests/addon-toolbars.spec.ts b/code/e2e-tests/addon-toolbars.spec.ts index dea38ae7d720..0bcd779518b4 100644 --- a/code/e2e-tests/addon-toolbars.spec.ts +++ b/code/e2e-tests/addon-toolbars.spec.ts @@ -30,7 +30,7 @@ test.describe('addon-toolbars', () => { // Click on viewport button and select spanish await sbPage.navigateToStory('addons/toolbars/globals', 'override-locale'); await expect(sbPage.previewRoot()).toContainText('안녕하세요'); - const button = await sbPage.page.locator('[title="Internationalization locale"]'); + const button = sbPage.page.locator('[title="Internationalization locale"]'); await expect(button).toHaveAttribute('disabled', ''); }); diff --git a/code/e2e-tests/composition.spec.ts b/code/e2e-tests/composition.spec.ts index 48ce183b33c9..71b206010b46 100644 --- a/code/e2e-tests/composition.spec.ts +++ b/code/e2e-tests/composition.spec.ts @@ -3,48 +3,42 @@ import { expect, test } from '@playwright/test'; import { SbPage } from './util'; const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:6006'; - -// This is a slow test, and (presumably) framework independent, so only run it on one sandbox -const skipTest = process.env.STORYBOOK_TEMPLATE_NAME !== 'react-vite/default-ts'; +const templateName = process.env.STORYBOOK_TEMPLATE_NAME || ''; test.describe('composition', () => { + test.skip( + templateName !== 'react-vite/default-ts', + 'Slow, framework independent test, so only run it on in react-vite/default-ts' + ); + test.beforeEach(async ({ page }) => { - if (skipTest) { - return; - } await page.goto(storybookUrl); await new SbPage(page).waitUntilLoaded(); }); test('should correctly filter composed stories', async ({ page }) => { - if (skipTest) { - return; - } - - // Expect that composed Storybooks are visible - // Expect that composed Storybooks are visible - await expect(await page.getByTitle('Storybook 8.0.0')).toBeVisible(); - await expect(await page.getByTitle('Storybook 7.6.18')).toBeVisible(); + await expect(page.getByTitle('Storybook 8.0.0')).toBeVisible(); + await expect(page.getByTitle('Storybook 7.6.18')).toBeVisible(); // Expect composed stories to be available in the sidebar await page.locator('[id="storybook\\@8\\.0\\.0_components-badge"]').click(); await expect( - await page.locator('[id="storybook\\@8\\.0\\.0_components-badge--default"]') + page.locator('[id="storybook\\@8\\.0\\.0_components-badge--default"]') ).toBeVisible(); await page.locator('[id="storybook\\@7\\.6\\.18_components-badge"]').click(); await expect( - await page.locator('[id="storybook\\@7\\.6\\.18_components-badge--default"]') + page.locator('[id="storybook\\@7\\.6\\.18_components-badge--default"]') ).toBeVisible(); // Expect composed stories `to be available in the search await page.getByPlaceholder('Find components').fill('Button'); await expect( - await page.getByRole('option', { name: 'Button Storybook 8.0.0 / @blocks / examples' }) + page.getByRole('option', { name: 'Button Storybook 8.0.0 / @blocks / examples' }) ).toBeVisible(); await expect( - await page.getByRole('option', { name: 'Button Storybook 7.6.18 / @blocks / examples' }) + page.getByRole('option', { name: 'Button Storybook 7.6.18 / @blocks / examples' }) ).toBeVisible(); }); }); diff --git a/code/e2e-tests/framework-nextjs.spec.ts b/code/e2e-tests/framework-nextjs.spec.ts index c88d723ce465..982952f4cb40 100644 --- a/code/e2e-tests/framework-nextjs.spec.ts +++ b/code/e2e-tests/framework-nextjs.spec.ts @@ -27,7 +27,7 @@ test.describe('Next.js', () => { sbPage = new SbPage(page); }); - // TODO: Test is flaky, investigate why + // eslint-disable-next-line playwright/no-skipped-test -- test is flaky, investigate why test.skip('should lazy load images by default', async () => { await sbPage.navigateToStory('stories/frameworks/nextjs/Image', 'lazy'); @@ -36,7 +36,7 @@ test.describe('Next.js', () => { expect(await img.evaluate((image) => image.complete)).toBeFalsy(); }); - // TODO: Test is flaky, investigate why + // eslint-disable-next-line playwright/no-skipped-test -- test is flaky, investigate why test.skip('should eager load images when loading parameter is set to eager', async () => { await sbPage.navigateToStory('stories/frameworks/nextjs/Image', 'eager'); @@ -50,29 +50,29 @@ test.describe('Next.js', () => { let root: Locator; let sbPage: SbPage; + test.beforeEach(async ({ page }) => { + sbPage = new SbPage(page); + + await sbPage.navigateToStory( + 'stories/frameworks/nextjs-nextjs-default-ts/Navigation', + 'default' + ); + root = sbPage.previewRoot(); + }); + function testRoutingBehaviour(buttonText: string, action: string) { test(`should trigger ${action} action`, async ({ page }) => { const button = root.locator('button', { hasText: buttonText }); await button.click(); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content', { + const logItem = page.locator('#storybook-panel-root #panel-tab-content', { hasText: `useRouter().${action}`, }); await expect(logItem).toBeVisible(); }); } - test.beforeEach(async ({ page }) => { - sbPage = new SbPage(page); - - await sbPage.navigateToStory( - 'stories/frameworks/nextjs-nextjs-default-ts/Navigation', - 'default' - ); - root = sbPage.previewRoot(); - }); - testRoutingBehaviour('Go back', 'back'); testRoutingBehaviour('Go forward', 'forward'); testRoutingBehaviour('Prefetch', 'prefetch'); @@ -85,26 +85,26 @@ test.describe('Next.js', () => { let root: Locator; let sbPage: SbPage; + test.beforeEach(async ({ page }) => { + sbPage = new SbPage(page); + + await sbPage.navigateToStory('stories/frameworks/nextjs-nextjs-default-ts/Router', 'default'); + root = sbPage.previewRoot(); + }); + function testRoutingBehaviour(buttonText: string, action: string) { test(`should trigger ${action} action`, async ({ page }) => { const button = root.locator('button', { hasText: buttonText }); await button.click(); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content', { + const logItem = page.locator('#storybook-panel-root #panel-tab-content', { hasText: `useRouter().${action}`, }); await expect(logItem).toBeVisible(); }); } - test.beforeEach(async ({ page }) => { - sbPage = new SbPage(page); - - await sbPage.navigateToStory('stories/frameworks/nextjs-nextjs-default-ts/Router', 'default'); - root = sbPage.previewRoot(); - }); - testRoutingBehaviour('Go back', 'back'); testRoutingBehaviour('Go forward', 'forward'); testRoutingBehaviour('Prefetch', 'prefetch'); diff --git a/code/e2e-tests/framework-svelte.spec.ts b/code/e2e-tests/framework-svelte.spec.ts index a56bb2db5f0c..d5b3354fb5d0 100644 --- a/code/e2e-tests/framework-svelte.spec.ts +++ b/code/e2e-tests/framework-svelte.spec.ts @@ -6,13 +6,13 @@ import { SbPage } from './util'; const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:6006'; const templateName = process.env.STORYBOOK_TEMPLATE_NAME; -test.beforeEach(async ({ page }) => { - await page.goto(storybookUrl); - await new SbPage(page).waitUntilLoaded(); -}); - test.describe('Svelte', () => { - test.skip(!templateName?.includes('svelte'), 'Only run this test on Svelte'); + test.beforeEach(async ({ page }) => { + await page.goto(storybookUrl); + await new SbPage(page).waitUntilLoaded(); + }); + + test.skip(!templateName?.includes('svelte'), 'Only run these tests on Svelte'); test('JS story has auto-generated args table', async ({ page }) => { const sbPage = new SbPage(page); @@ -58,76 +58,76 @@ test.describe('Svelte', () => { await sbPage.navigateToStory('stories/renderers/svelte/decorators-runs-once', 'default'); expect(lines).toHaveLength(1); }); -}); -test.describe('SvelteKit', () => { - test.skip(!templateName?.includes('svelte-kit'), 'Only run this test on SvelteKit'); + test.describe('SvelteKit', () => { + test.skip(!templateName?.includes('svelte-kit'), 'Only run this test on SvelteKit'); - test('Links are logged in Actions panel', async ({ page }) => { - const sbPage = new SbPage(page); + test('Links are logged in Actions panel', async ({ page }) => { + const sbPage = new SbPage(page); - await sbPage.navigateToStory('stories/sveltekit/modules/hrefs', 'default-actions'); - const root = sbPage.previewRoot(); - const link = root.locator('a', { hasText: 'Link to /basic-href' }); - await link.click(); + await sbPage.navigateToStory('stories/sveltekit/modules/hrefs', 'default-actions'); + const root = sbPage.previewRoot(); + const link = root.locator('a', { hasText: 'Link to /basic-href' }); + await link.click(); - await sbPage.viewAddonPanel('Actions'); - const basicLogItem = await page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/basic-href`, - }); + await sbPage.viewAddonPanel('Actions'); + const basicLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/basic-href`, + }); - await expect(basicLogItem).toBeVisible(); - const complexLogItem = await page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/deep/nested`, + await expect(basicLogItem).toBeVisible(); + const complexLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/deep/nested`, + }); + await expect(complexLogItem).toBeVisible(); }); - await expect(complexLogItem).toBeVisible(); - }); - test('goto are logged in Actions panel', async ({ page }) => { - const sbPage = new SbPage(page); + test('goto are logged in Actions panel', async ({ page }) => { + const sbPage = new SbPage(page); - await sbPage.navigateToStory('stories/sveltekit/modules/navigation', 'default-actions'); - const root = sbPage.previewRoot(); - await sbPage.viewAddonPanel('Actions'); + await sbPage.navigateToStory('stories/sveltekit/modules/navigation', 'default-actions'); + const root = sbPage.previewRoot(); + await sbPage.viewAddonPanel('Actions'); - const goto = root.locator('button', { hasText: 'goto' }); - await goto.click(); + const goto = root.locator('button', { hasText: 'goto' }); + await goto.click(); - const gotoLogItem = page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/storybook-goto`, - }); - await expect(gotoLogItem).toBeVisible(); + const gotoLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/storybook-goto`, + }); + await expect(gotoLogItem).toBeVisible(); - const invalidate = root.getByRole('button', { name: 'invalidate', exact: true }); - await invalidate.click(); + const invalidate = root.getByRole('button', { name: 'invalidate', exact: true }); + await invalidate.click(); - const invalidateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/storybook-invalidate`, - }); - await expect(invalidateLogItem).toBeVisible(); + const invalidateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/storybook-invalidate`, + }); + await expect(invalidateLogItem).toBeVisible(); - const invalidateAll = root.getByRole('button', { name: 'invalidateAll' }); - await invalidateAll.click(); + const invalidateAll = root.getByRole('button', { name: 'invalidateAll' }); + await invalidateAll.click(); - const invalidateAllLogItem = page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `"invalidateAll"`, - }); - await expect(invalidateAllLogItem).toBeVisible(); + const invalidateAllLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `"invalidateAll"`, + }); + await expect(invalidateAllLogItem).toBeVisible(); - const replaceState = root.getByRole('button', { name: 'replaceState' }); - await replaceState.click(); + const replaceState = root.getByRole('button', { name: 'replaceState' }); + await replaceState.click(); - const replaceStateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/storybook-replace-state`, - }); - await expect(replaceStateLogItem).toBeVisible(); + const replaceStateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/storybook-replace-state`, + }); + await expect(replaceStateLogItem).toBeVisible(); - const pushState = root.getByRole('button', { name: 'pushState' }); - await pushState.click(); + const pushState = root.getByRole('button', { name: 'pushState' }); + await pushState.click(); - const pushStateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/storybook-push-state`, + const pushStateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/storybook-push-state`, + }); + await expect(pushStateLogItem).toBeVisible(); }); - await expect(pushStateLogItem).toBeVisible(); }); }); diff --git a/code/e2e-tests/json-files.spec.ts b/code/e2e-tests/json-files.spec.ts index 920b072c093a..9769fb4f7905 100644 --- a/code/e2e-tests/json-files.spec.ts +++ b/code/e2e-tests/json-files.spec.ts @@ -11,7 +11,7 @@ test.describe('JSON files', () => { test('should have index.json', async ({ page }) => { const json = await page.evaluate(() => fetch('/index.json').then((res) => res.json())); - expect(json).toEqual({ + expect(json).toStrictEqual({ v: expect.any(Number), entries: expect.objectContaining({ 'example-button--primary': expect.objectContaining({ diff --git a/code/e2e-tests/manager.spec.ts b/code/e2e-tests/manager.spec.ts index f863b1cddf2a..e0156786f11c 100644 --- a/code/e2e-tests/manager.spec.ts +++ b/code/e2e-tests/manager.spec.ts @@ -23,14 +23,14 @@ test.describe('Manager UI', () => { // toggle with keyboard shortcut await sbPage.page.locator('html').press('Alt+s'); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); await sbPage.page.locator('html').press('Alt+s'); await expect(sbPage.page.locator('.sidebar-container')).toBeVisible(); // toggle with menu item await sbPage.page.locator('[aria-label="Shortcuts"]').click(); await sbPage.page.locator('#list-item-S').click(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); // toggle with "show sidebar" button await sbPage.page.locator('[aria-label="Show sidebar"]').click(); @@ -40,8 +40,8 @@ test.describe('Manager UI', () => { test('Toolbar toggling', async ({ page }) => { const sbPage = new SbPage(page); const expectToolbarVisibility = async (visible: boolean) => { - expect(async () => { - const toolbar = await sbPage.page.locator(`[data-test-id="sb-preview-toolbar"]`); + await expect(async () => { + const toolbar = sbPage.page.locator(`[data-test-id="sb-preview-toolbar"]`); const marginTop = await toolbar.evaluate( (element) => window.getComputedStyle(element).marginTop ); @@ -73,13 +73,13 @@ test.describe('Manager UI', () => { // navigate to docs to hide panel await sbPage.navigateToStory('example/button', 'docs'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); // toggle with keyboard shortcut await sbPage.page.locator('html').press('Alt+a'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); await sbPage.page.locator('html').press('Alt+a'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); }); test('Toggling', async ({ page }) => { @@ -92,14 +92,14 @@ test.describe('Manager UI', () => { // toggle with keyboard shortcut await sbPage.page.locator('html').press('Alt+a'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); await sbPage.page.locator('html').press('Alt+a'); await expect(sbPage.page.locator('#storybook-panel-root')).toBeVisible(); // toggle with menu item await sbPage.page.locator('[aria-label="Shortcuts"]').click(); await sbPage.page.locator('#list-item-A').click(); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); // toggle with "show addons" button await sbPage.page.locator('[aria-label="Show addons"]').click(); @@ -121,7 +121,7 @@ test.describe('Manager UI', () => { // hide with keyboard shortcut await sbPage.page.locator('html').press('Alt+a'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); // toggling position should also show the panel again await sbPage.page.locator('html').press('Alt+d'); @@ -140,8 +140,8 @@ test.describe('Manager UI', () => { // toggle with keyboard shortcut await sbPage.page.locator('html').press('Alt+f'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); await sbPage.page.locator('html').press('Alt+f'); await expect(sbPage.page.locator('#storybook-panel-root')).toBeVisible(); @@ -150,8 +150,8 @@ test.describe('Manager UI', () => { // toggle with menu item await sbPage.page.locator('[aria-label="Shortcuts"]').click(); await sbPage.page.locator('#list-item-F').click(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); // toggle with "go/exit fullscreen" button await sbPage.page.locator('[aria-label="Exit full screen"]').click(); @@ -159,20 +159,20 @@ test.describe('Manager UI', () => { await expect(sbPage.page.locator('.sidebar-container')).toBeVisible(); await sbPage.page.locator('[aria-label="Go full screen"]').click(); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); // go fullscreen when sidebar is shown but panel is hidden await sbPage.page.locator('[aria-label="Show sidebar"]').click(); await sbPage.page.locator('[aria-label="Go full screen"]').click(); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); // go fullscreen when panel is shown but sidebar is hidden await sbPage.page.locator('[aria-label="Show addons"]').click(); await sbPage.page.locator('[aria-label="Go full screen"]').click(); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); }); test('Settings page', async ({ page }) => { @@ -182,7 +182,7 @@ test.describe('Manager UI', () => { await expect(sbPage.page.url()).toContain('/settings/about'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); await sbPage.page.locator('[title="Close settings page"]').click(); await expect(sbPage.page.url()).not.toContain('/settings/about'); @@ -202,12 +202,12 @@ test.describe('Manager UI', () => { test('Navigate to story', async ({ page }) => { const sbPage = new SbPage(page); - const closeNavigationButton = await sbPage.page.locator('[title="Close navigation menu"]'); - const mobileNavigationHeading = await sbPage.page.locator('[title="Open navigation menu"]'); + const closeNavigationButton = sbPage.page.locator('[title="Close navigation menu"]'); + const mobileNavigationHeading = sbPage.page.locator('[title="Open navigation menu"]'); // navigation menu is closed - await expect(closeNavigationButton).not.toBeVisible(); - await expect(sbPage.page.locator('#storybook-explorer-menu')).not.toBeVisible(); + await expect(closeNavigationButton).toBeHidden(); + await expect(sbPage.page.locator('#storybook-explorer-menu')).toBeHidden(); // open navigation menu await mobileNavigationHeading.click(); @@ -223,7 +223,7 @@ test.describe('Manager UI', () => { // navigation menu is closed await expect(mobileNavigationHeading).toHaveText('Example/Button/Secondary'); - await expect(sbPage.page.locator('#storybook-explorer-menu')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-explorer-menu')).toBeHidden(); // story has changed await expect(sbPage.page.url()).toContain('example-button--secondary'); }); @@ -231,13 +231,13 @@ test.describe('Manager UI', () => { test('Open and close addon panel', async ({ page }) => { const sbPage = new SbPage(page); - const mobileNavigationHeading = await sbPage.page.locator('[title="Open navigation menu"]'); + const mobileNavigationHeading = sbPage.page.locator('[title="Open navigation menu"]'); await mobileNavigationHeading.click(); await sbPage.navigateToStory('Example/Button', 'Secondary'); // panel is closed await expect(mobileNavigationHeading).toHaveText('Example/Button/Secondary'); - await expect(sbPage.page.locator('#tabbutton-addon-controls')).not.toBeVisible(); + await expect(sbPage.page.locator('#tabbutton-addon-controls')).toBeHidden(); // open panel await sbPage.page.locator('[title="Open addon panel"]').click(); @@ -250,7 +250,7 @@ test.describe('Manager UI', () => { // panel is closed await expect(mobileNavigationHeading).toHaveText('Example/Button/Secondary'); - await expect(sbPage.page.locator('#tabbutton-addon-controls')).not.toBeVisible(); + await expect(sbPage.page.locator('#tabbutton-addon-controls')).toBeHidden(); }); }); }); diff --git a/code/e2e-tests/module-mocking.spec.ts b/code/e2e-tests/module-mocking.spec.ts index 7a9048e6cc2b..1c8de1778e36 100644 --- a/code/e2e-tests/module-mocking.spec.ts +++ b/code/e2e-tests/module-mocking.spec.ts @@ -18,7 +18,7 @@ test.describe('module-mocking', () => { await sbPage.navigateToStory('lib/test/before-each', 'before-each-order'); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content'); + const logItem = page.locator('#storybook-panel-root #panel-tab-content'); await expect(logItem).toBeVisible(); const expectedTexts = [ @@ -42,7 +42,7 @@ test.describe('module-mocking', () => { await sbPage.navigateToStory('lib/test/module-mocking', 'basic'); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content', { + const logItem = page.locator('#storybook-panel-root #panel-tab-content', { hasText: 'foo: []', }); await expect(logItem).toBeVisible(); diff --git a/code/e2e-tests/preview-api.spec.ts b/code/e2e-tests/preview-api.spec.ts index bb7b3dc2be9a..5130e78e1694 100644 --- a/code/e2e-tests/preview-api.spec.ts +++ b/code/e2e-tests/preview-api.spec.ts @@ -27,16 +27,16 @@ test.describe('preview-api', () => { // wait for the play function to complete await sbPage.viewAddonPanel('Interactions'); - const interactionsTab = await page.locator('#tabbutton-storybook-interactions-panel'); + const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel'); await expect(interactionsTab).toBeVisible(); const panel = sbPage.panelContent(); - const runStatusBadge = await panel.locator('[aria-label="Status of the test run"]'); + const runStatusBadge = panel.locator('[aria-label="Status of the test run"]'); await expect(runStatusBadge).toContainText(/Pass/); // click outside, to remove focus from the input of the story, then press S to toggle sidebar await sbPage.previewRoot().click(); await sbPage.previewRoot().press('Alt+s'); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); }); test('should pass over shortcuts, but not from play functions, docs', async ({ page }) => { @@ -51,7 +51,7 @@ test.describe('preview-api', () => { await expect(sbPage.page.locator('.sidebar-container')).toBeVisible(); await sbPage.previewRoot().getByRole('button').getByText('Submit').first().press('Alt+s'); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); }); // if rerenders were interleaved the button would have rendered "Error: Interleaved loaders. Changed arg" @@ -61,7 +61,7 @@ test.describe('preview-api', () => { const root = sbPage.previewRoot(); - const labelControl = await sbPage.page.locator('#control-label'); + const labelControl = sbPage.page.locator('#control-label'); await expect(root.getByText('Loaded. Click me')).toBeVisible(); await expect(labelControl).toBeVisible(); diff --git a/code/e2e-tests/save-from-controls.spec.ts b/code/e2e-tests/save-from-controls.spec.ts index eeba0ab5ed4c..cf5ad28ef363 100644 --- a/code/e2e-tests/save-from-controls.spec.ts +++ b/code/e2e-tests/save-from-controls.spec.ts @@ -35,11 +35,11 @@ test.describe('save-from-controls', () => { await sbPage.panelContent().locator('[data-short-label="Unsaved changes"]').isVisible(); // update the story - await sbPage.panelContent().locator('button').getByText('Update story').click({ force: true }); + await sbPage.panelContent().locator('button').getByText('Update story').click(); // Assert the file is saved - const notification1 = await sbPage.page.waitForSelector('[title="Story saved"]'); - await notification1.isVisible(); + const notification1 = sbPage.page.getByTitle('Story saved'); + await expect(notification1).toBeVisible(); // dismiss await notification1.click(); @@ -52,21 +52,19 @@ test.describe('save-from-controls', () => { // Assert the footer is shown await sbPage.panelContent().locator('[data-short-label="Unsaved changes"]').isVisible(); - const buttons = await sbPage + const buttons = sbPage .panelContent() .locator('[aria-label="Create new story with these settings"]'); // clone the story - await buttons.click({ force: true }); + await buttons.click(); - const input = await sbPage.page.waitForSelector('[placeholder="Story export name"]'); - await input.fill('ClonedStory' + id); - const submit = await sbPage.page.waitForSelector('[type="submit"]'); - await submit.click(); + await sbPage.page.getByPlaceholder('Story export name').fill('ClonedStory' + id); + await sbPage.page.getByRole('button', { name: 'Create' }).click(); // Assert the file is saved - const notification2 = await sbPage.page.waitForSelector('[title="Story created"]'); - await notification2.isVisible(); + const notification2 = sbPage.page.getByTitle('Story created'); + await expect(notification2).toBeVisible(); await notification2.click(); // Assert the Button components is rendered in the preview diff --git a/code/e2e-tests/storybook-hooks.spec.ts b/code/e2e-tests/storybook-hooks.spec.ts index 4dbb0ccf6f1e..b9c1ed1be4d2 100644 --- a/code/e2e-tests/storybook-hooks.spec.ts +++ b/code/e2e-tests/storybook-hooks.spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable playwright/expect-expect */ + /* eslint-disable no-underscore-dangle */ import { promises as fs } from 'node:fs'; import { join } from 'node:path'; diff --git a/code/e2e-tests/util.ts b/code/e2e-tests/util.ts index da0ff313431b..00122ef595d2 100644 --- a/code/e2e-tests/util.ts +++ b/code/e2e-tests/util.ts @@ -43,9 +43,9 @@ export class SbPage { const titleId = toId(title); const storyId = toId(name); const storyLinkId = `#${titleId}--${storyId}`; - await this.page.waitForSelector(storyLinkId); + await this.page.locator(storyLinkId).waitFor(); const storyLink = this.page.locator('*', { has: this.page.locator(`> ${storyLinkId}`) }); - await storyLink.click({ force: true }); + await storyLink.click(); await this.page.waitForURL((url) => url.search.includes( @@ -53,8 +53,8 @@ export class SbPage { ) ); - const selected = await storyLink.getAttribute('data-selected'); - await expect(selected).toBe('true'); + const selected = storyLink; + await expect(selected).toHaveAttribute('data-selected', 'true'); await this.previewRoot(); } @@ -65,16 +65,16 @@ export class SbPage { const titleId = toId(title); const storyId = toId(name); const storyLinkId = `#${titleId}-${storyId}--docs`; - await this.page.waitForSelector(storyLinkId); + await this.page.locator(storyLinkId).waitFor(); const storyLink = this.page.locator('*', { has: this.page.locator(`> ${storyLinkId}`) }); - await storyLink.click({ force: true }); + await storyLink.click(); await this.page.waitForURL((url) => url.search.includes(`path=/docs/${titleId}-${storyId}--docs`) ); - const selected = await storyLink.getAttribute('data-selected'); - await expect(selected).toBe('true'); + const selected = storyLink; + await expect(selected).toHaveAttribute('data-selected', 'true'); await this.previewRoot(); } @@ -124,7 +124,7 @@ export class SbPage { } async viewAddonPanel(name: string) { - const tabs = await this.page.locator('[role=tablist] button[role=tab]'); + const tabs = this.page.locator('[role=tablist] button[role=tab]'); const tab = tabs.locator(`text=/^${name}/`); await tab.click(); } diff --git a/code/package.json b/code/package.json index 4090d4ac381c..40355bc18e1b 100644 --- a/code/package.json +++ b/code/package.json @@ -192,6 +192,7 @@ "eslint": "^8.56.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-local-rules": "portal:../scripts/eslint-plugin-local-rules", + "eslint-plugin-playwright": "^1.6.2", "eslint-plugin-storybook": "^0.8.0", "fs-extra": "^11.1.0", "github-release-from-changelog": "^2.1.1", diff --git a/code/yarn.lock b/code/yarn.lock index 9b31926f1155..83b4b2ba2bdc 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6785,6 +6785,7 @@ __metadata: eslint: "npm:^8.56.0" eslint-import-resolver-typescript: "npm:^3.6.1" eslint-plugin-local-rules: "portal:../scripts/eslint-plugin-local-rules" + eslint-plugin-playwright: "npm:^1.6.2" eslint-plugin-storybook: "npm:^0.8.0" fs-extra: "npm:^11.1.0" github-release-from-changelog: "npm:^2.1.1" @@ -14346,6 +14347,21 @@ __metadata: languageName: node linkType: soft +"eslint-plugin-playwright@npm:^1.6.2": + version: 1.6.2 + resolution: "eslint-plugin-playwright@npm:1.6.2" + dependencies: + globals: "npm:^13.23.0" + peerDependencies: + eslint: ">=8.40.0" + eslint-plugin-jest: ">=25" + peerDependenciesMeta: + eslint-plugin-jest: + optional: true + checksum: 10c0/0785b7031507699eac6a45fdcd90705d9759d5943a4033354b735ae856c9d71345ecacb6a7ff0c4cd0e24f523e9d59dee7081dc96c7b5c492fcbed77496a0a19 + languageName: node + linkType: hard + "eslint-plugin-prettier@npm:^5.1.3": version: 5.1.3 resolution: "eslint-plugin-prettier@npm:5.1.3" @@ -16157,7 +16173,7 @@ __metadata: languageName: node linkType: hard -"globals@npm:^13.19.0, globals@npm:^13.6.0": +"globals@npm:^13.19.0, globals@npm:^13.23.0, globals@npm:^13.6.0": version: 13.24.0 resolution: "globals@npm:13.24.0" dependencies: