diff --git a/README.md b/README.md index 19d20d25..87bd1e1f 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,22 @@ Storybook test runner turns all of your stories into executable tests. - [2 - Run tests with --coverage flag](#2---run-tests-with---coverage-flag) - [3 - Merging code coverage with coverage from other tools](#3---merging-code-coverage-with-coverage-from-other-tools) - [4 - Run tests with --shard flag](#4---run-tests-with---shard-flag) -- [Experimental test hook API](#experimental-test-hook-api) +- [Test hooks API](#test-hooks-api) + - [setup](#setup) + - [preRender](#prerender) + - [postRender](#postrender) + - [Render lifecycle](#render-lifecycle) - [prepare](#prepare) - [getHttpHeaders](#gethttpheaders) - - [DOM snapshot recipe](#dom-snapshot-recipe) - - [Image snapshot recipe](#image-snapshot-recipe) - - [Render lifecycle](#render-lifecycle) - [Utility functions](#utility-functions) - [getStoryContext](#getstorycontext) + - [waitForPageReady](#waitforpageready) - [StorybookTestRunner user agent](#storybooktestrunner-user-agent) +- [Recipes](#recipes) + - [Preconfiguring viewport size](#preconfiguring-viewport-size) + - [Accessibility testing](#accessibility-testing) + - [DOM snapshot (HTML)](#dom-snapshot-html) + - [Image snapshot](#image-snapshot) - [Troubleshooting](#troubleshooting) - [The error output in the CLI is too short](#the-error-output-in-the-cli-is-too-short) - [The test runner seems flaky and keeps timing out](#the-test-runner-seems-flaky-and-keeps-timing-out) @@ -494,7 +501,7 @@ report-coverage: - yarn nyc report --reporter=text -t merged-output --report-dir merged-output ``` -## Experimental test hook API +## Test hooks API The test runner renders a story and executes its [play function](https://storybook.js.org/docs/react/writing-stories/play-function) if one exists. However, there are certain behaviors that are not possible to achieve via the play function, which executes in the browser. For example, if you want the test runner to take visual snapshots for you, this is something that is possible via Playwright/Jest, but must be executed in Node. @@ -502,10 +509,83 @@ To enable use cases like visual or DOM snapshots, the test runner exports test h There are three hooks: `setup`, `preRender`, and `postRender`. `setup` executes once before all the tests run. `preRender` and `postRender` execute within a test before and after a story is rendered. -The render functions are async functions that receive a [Playwright Page](https://playwright.dev/docs/pages) and a context object with the current story's `id`, `title`, and `name`. They are globally settable by `@storybook/test-runner`'s `setPreRender` and `setPostRender` APIs. - All three functions can be set up in the configuration file `.storybook/test-runner.js` which can optionally export any of these functions. +> **Note** +> The `preRender` and `postRender` functions will be executed for all stories. + +#### setup + +Async function that executes once before all the tests run. Useful for setting node-related configuration, such as extending Jest global `expect` for accessibility matchers. + +```js +// .storybook/test-runner.js +module.exports = { + async setup() { + // execute whatever you like, in Node, once before all tests run + }, +}; +``` + +#### preRender + +Async function that receives a [Playwright Page](https://playwright.dev/docs/pages) and a context object with the current story's `id`, `title`, and `name`. +Executes within a test before the story is rendered. Useful for configuring the Page before the story renders, such as setting up the viewport size. + +```js +// .storybook/test-runner.js +module.exports = { + async preRender(page, context) { + // execute whatever you like, before the story renders + }, +}; +``` + +#### postRender + +Async function that receives a [Playwright Page](https://playwright.dev/docs/pages) and a context object with the current story's `id`, `title`, and `name`. +Executes within a test after a story is rendered. Useful for asserting things after the story is rendered, such as DOM and image snapshotting. + +```js +// .storybook/test-runner.js +module.exports = { + async postRender(page, context) { + // execute whatever you like, after the story renders + }, +}; +``` + +> **Note** +> Although you have access to Playwright's Page object, in some of these hooks, we encourage you to test as much as possible within the story's play function. + +#### Render lifecycle + +To visualize the test lifecycle with these hooks, consider a simplified version of the test code automatically generated for each story in your Storybook: + +```js +// executed once, before the tests +await setup(); + +it('button--basic', async () => { + // filled in with data for the current story + const context = { id: 'button--basic', title: 'Button', name: 'Basic' }; + + // playwright page https://playwright.dev/docs/pages + await page.goto(STORYBOOK_URL); + + // pre-render hook + if (preRender) await preRender(page, context); + + // render the story and run its play function (if applicable) + await page.execute('render', context); + + // post-render hook + if (postRender) await postRender(page, context); +}); +``` + +These hooks are very useful for a variety of use cases, which are described in the [recipes](#recipes) section further below. + Apart from these hooks, there are additional properties you can set in `.storybook/test-runner.js`: #### prepare @@ -539,107 +619,134 @@ module.exports = { }; ``` -> **Note** -> These test hooks are experimental and may be subject to breaking changes. We encourage you to test as much as possible within the story's play function. +### Utility functions -### DOM snapshot recipe +For more specific use cases, the test runner provides utility functions that could be useful to you. -The `postRender` function provides a [Playwright page](https://playwright.dev/docs/api/class-page) instance, of which you can use for DOM snapshot testing: +#### getStoryContext + +While running tests using the hooks, you might want to get information from a story, such as the parameters passed to it, or its args. The test runner now provides a `getStoryContext` utility function that fetches the story context for the current story: + +Suppose your story looks like this: ```js -// .storybook/test-runner.js -module.exports = { - async postRender(page, context) { - // the #root element wraps the story. From Storybook 7.0 onwards, the selector should be #storybook-root - const elementHandler = await page.$('#root'); - const innerHTML = await elementHandler.innerHTML(); - expect(innerHTML).toMatchSnapshot(); +// ./Button.stories.ts + +export const Primary = { + parameters: { + theme: 'dark', }, }; ``` -When running with `--stories-json`, tests get generated in a temporary folder and snapshots get stored alongside. You will need to `--eject` and configure a custom [`snapshotResolver`](https://jestjs.io/docs/configuration#snapshotresolver-string) to store them elsewhere, e.g. in your working directory: +You can access its context in a test hook like so: ```js -const path = require('path'); +// .storybook/test-runner.js +const { getStoryContext } = require('@storybook/test-runner'); module.exports = { - resolveSnapshotPath: (testPath, snapshotExtension) => - path.join(process.cwd(), '__snapshots__', path.basename(testPath) + snapshotExtension), - resolveTestPath: (snapshotFilePath, snapshotExtension) => - path.join(process.env.TEST_ROOT, path.basename(snapshotFilePath, snapshotExtension)), - testPathForConsistencyCheck: path.join(process.env.TEST_ROOT, 'example.test.js'), + async postRender(page, context) { + // Get entire context of a story, including parameters, args, argTypes, etc. + const storyContext = await getStoryContext(page, context); + if (storyContext.parameters.theme === 'dark') { + // do something + } else { + // do something else + } + }, }; ``` -### Image snapshot recipe +It's useful for skipping or enhancing use cases like [image snapshot testing](#image-snapshot), [accessibility testing](#accessibility-testing) and more. -Here's a slightly different recipe for image snapshot testing: +#### waitForPageReady + +The `waitForPageReady` utility is useful when you're executing [image snapshot testing](#image-snapshot) with the test-runner. It encapsulates a few assertions to make sure the browser has finished downloading assets. ```js // .storybook/test-runner.js const { waitForPageReady } = require('@storybook/test-runner'); -const { toMatchImageSnapshot } = require('jest-image-snapshot'); - -const customSnapshotsDir = `${process.cwd()}/__snapshots__`; module.exports = { - setup() { - expect.extend({ toMatchImageSnapshot }); - }, async postRender(page, context) { // use the test-runner utility to wait for fonts to load, etc. await waitForPageReady(page); - // If you want to take screenshot of multiple browsers, use - // page.context().browser().browserType().name() to get the browser name to prefix the file name - const image = await page.screenshot(); - expect(image).toMatchImageSnapshot({ - customSnapshotsDir, - customSnapshotIdentifier: context.id, - }); + // by now, we know that the page is fully loaded }, }; ``` -There is also an exported `TestRunnerConfig` type available for TypeScript users. - -### Render lifecycle +#### StorybookTestRunner user agent -To visualize the test lifecycle, consider a simplified version of the test code automatically generated for each story in your Storybook: +The test-runner adds a `StorybookTestRunner` entry to the browser's user agent. You can use it to determine if a story is rendering in the context of the test runner. This might be useful if you want to disable certain features in your stories when running in the test runner, though it's likely an edge case. ```js -it('button--basic', async () => { - // filled in with data for the current story - const context = { id: 'button--basic', title: 'Button', name: 'Basic' }; +// At the render level, useful for dynamically rendering something based on the test-runner +export const MyStory = { + render: () => { + const isTestRunner = window.navigator.userAgent.match(/StorybookTestRunner/); + return ( +
Is this story running in the test runner?
+{isTestRunner ? 'Yes' : 'No'}
+Is this story running in the test runner?
-{isTestRunner ? 'Yes' : 'No'}
-