From fd058dff348903a1a2545f31e4c997ce7a5d8faa Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Thu, 22 Sep 2022 00:21:55 +0900 Subject: [PATCH] Storybook: Set up local visual regression testing (#43393) * Storybook: Set up local visual regression testing * Alter for custom-written tests * Add custom FM animation waiter and some FontSizePicker specs * Use explicit wait for the animated elements * Use simpler handle getter waitForSelector implies element is visible * Add config to load CSS * Add support for decorators * Migrate example FontSizePicker test to new setup * Rename test files * Rename to avoid clashes * Improve readme Co-authored-by: Bart Kalisz --- .gitignore | 2 + package.json | 2 + .../src/font-size-picker/stories/e2e/index.js | 47 +++++++++++++++++++ .../src/popover/stories/e2e/index.tsx | 25 ++++++++++ test/storybook-playwright/README.md | 29 ++++++++++++ .../storybook-playwright/playwright.config.ts | 13 +++++ .../specs/font-size-picker.spec.ts | 37 +++++++++++++++ .../specs/popover.spec.ts | 25 ++++++++++ test/storybook-playwright/storybook/main.js | 14 ++++++ .../storybook-playwright/storybook/preview.js | 1 + .../storybook/webpack.config.js | 6 +++ test/storybook-playwright/utils.ts | 40 ++++++++++++++++ 12 files changed, 241 insertions(+) create mode 100644 packages/components/src/font-size-picker/stories/e2e/index.js create mode 100644 packages/components/src/popover/stories/e2e/index.tsx create mode 100644 test/storybook-playwright/README.md create mode 100644 test/storybook-playwright/playwright.config.ts create mode 100644 test/storybook-playwright/specs/font-size-picker.spec.ts create mode 100644 test/storybook-playwright/specs/popover.spec.ts create mode 100644 test/storybook-playwright/storybook/main.js create mode 100644 test/storybook-playwright/storybook/preview.js create mode 100644 test/storybook-playwright/storybook/webpack.config.js create mode 100644 test/storybook-playwright/utils.ts diff --git a/.gitignore b/.gitignore index 1f5680e2c6c2da..8121a1132e5b5d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ phpcs.xml phpunit.xml phpunit-watcher.yml .tool-versions +test/storybook-playwright/test-results +test/storybook-playwright/specs/__snapshots__ diff --git a/package.json b/package.json index dcb6a28f362184..fea75bd07cd06d 100755 --- a/package.json +++ b/package.json @@ -290,11 +290,13 @@ "storybook:build": "build-storybook -c ./storybook -o ./storybook/build", "prestorybook:dev": "npm run build:packages", "storybook:dev": "concurrently \"npm run dev:packages\" \"start-storybook -c ./storybook -p 50240\"", + "storybook:e2e:dev": "concurrently \"npm run dev:packages\" \"start-storybook -c test/storybook-playwright/storybook -p 50241\"", "test": "npm-run-all lint test:unit", "test:create-block": "bash ./bin/test-create-block.sh", "test:e2e": "wp-scripts test-e2e --config packages/e2e-tests/jest.config.js", "test:e2e:debug": "wp-scripts --inspect-brk test-e2e --config packages/e2e-tests/jest.config.js --puppeteer-devtools", "test:e2e:playwright": "playwright test --config test/e2e/playwright.config.ts", + "test:e2e:storybook": "playwright test --config test/storybook-playwright/playwright.config.ts", "test:e2e:watch": "npm run test:e2e -- --watch", "test:performance": "wp-scripts test-e2e --config packages/e2e-tests/jest.performance.config.js", "test:php": "npm-run-all lint:php test:unit:php", diff --git a/packages/components/src/font-size-picker/stories/e2e/index.js b/packages/components/src/font-size-picker/stories/e2e/index.js new file mode 100644 index 00000000000000..30c0140bcefbbd --- /dev/null +++ b/packages/components/src/font-size-picker/stories/e2e/index.js @@ -0,0 +1,47 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import FontSizePicker from '../..'; + +export default { + title: 'Components/FontSizePicker', + component: FontSizePicker, +}; + +const FontSizePickerWithState = ( { initialValue, ...props } ) => { + const [ fontSize, setFontSize ] = useState( initialValue ); + return ( + + ); +}; + +export const Default = FontSizePickerWithState.bind( {} ); +Default.args = { + fontSizes: [ + { + name: 'Small', + slug: 'small', + size: 12, + }, + { + name: 'Normal', + slug: 'normal', + size: 16, + }, + { + name: 'Big', + slug: 'big', + size: 26, + }, + ], + initialValue: 16, +}; diff --git a/packages/components/src/popover/stories/e2e/index.tsx b/packages/components/src/popover/stories/e2e/index.tsx new file mode 100644 index 00000000000000..da4a3813280265 --- /dev/null +++ b/packages/components/src/popover/stories/e2e/index.tsx @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import Popover from '../..'; + +export default { + title: 'Components/Popover', + component: Popover, +}; + +export const Default = () => { + const [ isVisible, setIsVisible ] = useState( false ); + + return ( + + ); +}; diff --git a/test/storybook-playwright/README.md b/test/storybook-playwright/README.md new file mode 100644 index 00000000000000..14dd94f5803dab --- /dev/null +++ b/test/storybook-playwright/README.md @@ -0,0 +1,29 @@ +# Storybook Playwright Tests + +This is currently set up for testing visual regressions in the `components` package. The tests do not run on CI, and is meant as a testing tool for local development. + +## How to run + +First, build and serve the E2E Storybook. + +```sh +npm run storybook:e2e:dev +``` + +You are now ready to run the tests. The first run will generate the reference images, and subsequent runs will compare against them. (On the first run, you may be prompted to first install Playwright. If so, follow the instructions.) + +```sh +npm run test:e2e:storybook +``` + +To update the reference images, pass the `--update-snapshots` flag. + +```sh +npm run test:e2e:storybook -- --update-snapshots +``` + +## How to write tests + +Any stories matching the glob patterns listed in the [E2E Storybook config](./storybook/main.js) will be included in the special build. Note that these are exclusive fixtures for our tests, and are separate from the stories included in the [main Storybook build](../../storybook/main.js) to be [published online](https://wordpress.github.io/gutenberg/). + +The Playwright test files live in the [`specs`](./specs/) folder. See the [E2E Tests README](../e2e/README.md) for general best practices. \ No newline at end of file diff --git a/test/storybook-playwright/playwright.config.ts b/test/storybook-playwright/playwright.config.ts new file mode 100644 index 00000000000000..c89f3780c7229e --- /dev/null +++ b/test/storybook-playwright/playwright.config.ts @@ -0,0 +1,13 @@ +/** + * External dependencies + */ +import { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + outputDir: 'test-results/output', + reporter: [ + [ 'html', { open: 'on-failure', outputFolder: 'test-results/report' } ], + ], +}; + +export default config; diff --git a/test/storybook-playwright/specs/font-size-picker.spec.ts b/test/storybook-playwright/specs/font-size-picker.spec.ts new file mode 100644 index 00000000000000..6aa11e89262c93 --- /dev/null +++ b/test/storybook-playwright/specs/font-size-picker.spec.ts @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { test, expect } from '@wordpress/e2e-test-utils-playwright'; + +/** + * Internal dependencies + */ +import { gotoStoryId } from '../utils'; + +const waitUntilButtonHighlightStable = async ( page ) => { + const handle = await page.waitForSelector( + '[aria-label="Font size"] > div[role=presentation]' + ); + + await handle?.waitForElementState( 'stable' ); + + return handle; +}; + +test.describe.parallel( 'FontSizePicker', () => { + test.beforeEach( async ( { page } ) => { + await gotoStoryId( page, 'components-fontsizepicker--default' ); + } ); + + // This isn't a meaningful test, just some example code to demonstrate a way to + // wait until a certain element has finished animating. + // We can remove it once we have real tests. + test( 'with value', async ( { page } ) => { + const button = await page.locator( 'button[aria-label="Normal"]' ); + + await waitUntilButtonHighlightStable( page ); + + expect( button ).toHaveAttribute( 'aria-checked', 'true' ); + expect( await page.screenshot() ).toMatchSnapshot(); + } ); +} ); diff --git a/test/storybook-playwright/specs/popover.spec.ts b/test/storybook-playwright/specs/popover.spec.ts new file mode 100644 index 00000000000000..f11a20538edfc0 --- /dev/null +++ b/test/storybook-playwright/specs/popover.spec.ts @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { test, expect } from '@wordpress/e2e-test-utils-playwright'; + +/** + * Internal dependencies + */ +import { gotoStoryId } from '../utils'; + +test.describe( 'Popover', () => { + // This isn't a meaningful test, just some example code. + // We can remove it once we have real tests. + test( 'should render', async ( { page } ) => { + await gotoStoryId( page, 'components-popover--default', { + decorators: { marginChecker: 'show' }, + } ); + + await page.click( 'role=button' ); + const popover = await page.waitForSelector( '.components-popover' ); + await popover.waitForElementState( 'stable' ); + + expect( await page.screenshot() ).toMatchSnapshot(); + } ); +} ); diff --git a/test/storybook-playwright/storybook/main.js b/test/storybook-playwright/storybook/main.js new file mode 100644 index 00000000000000..ba05d2ee1c5693 --- /dev/null +++ b/test/storybook-playwright/storybook/main.js @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +const baseConfig = require( '../../../storybook/main' ); + +const config = { + ...baseConfig, + addons: [ '@storybook/addon-toolbars' ], + stories: [ + '../../../packages/components/src/**/stories/e2e/*.@(js|tsx|mdx)', + ], +}; + +module.exports = config; diff --git a/test/storybook-playwright/storybook/preview.js b/test/storybook-playwright/storybook/preview.js new file mode 100644 index 00000000000000..911578742e33e1 --- /dev/null +++ b/test/storybook-playwright/storybook/preview.js @@ -0,0 +1 @@ +export * from '../../../storybook/preview'; diff --git a/test/storybook-playwright/storybook/webpack.config.js b/test/storybook-playwright/storybook/webpack.config.js new file mode 100644 index 00000000000000..1c6d2f700bc594 --- /dev/null +++ b/test/storybook-playwright/storybook/webpack.config.js @@ -0,0 +1,6 @@ +/** + * Internal dependencies + */ +const baseConfig = require( '../../../storybook/webpack.config' ); + +module.exports = baseConfig; diff --git a/test/storybook-playwright/utils.ts b/test/storybook-playwright/utils.ts new file mode 100644 index 00000000000000..80be7297c88437 --- /dev/null +++ b/test/storybook-playwright/utils.ts @@ -0,0 +1,40 @@ +/** + * External dependencies + */ +import type { Page } from '@playwright/test'; + +const STORYBOOK_PORT = '50241'; + +type Decorators = { + css?: 'none' | 'basic' | 'wordpress'; + direction?: 'ltr' | 'rtl'; + marginChecker?: 'show' | 'hide'; +}; +type Options = { decorators?: Decorators }; + +const buildDecoratorString = ( decorators: Decorators = {} ) => { + const decoratorParamStrings = Object.entries( decorators ).map( + ( keyValue ) => keyValue.join( ':' ) + ); + return decoratorParamStrings.join( ';' ); +}; + +export const gotoStoryId = ( + page: Page, + storyId: string, + { decorators }: Options = {} +) => { + const params = new URLSearchParams(); + const decoratorString = buildDecoratorString( decorators ); + + if ( decoratorString ) { + params.set( 'globals', decoratorString ); + } + + params.set( 'id', storyId ); + + page.goto( + `http://localhost:${ STORYBOOK_PORT }/iframe.html?${ params.toString() }`, + { waitUntil: 'load' } + ); +};