From b2fb6a4135854db5f2a31a021f5021405c2ac22e Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 17 Nov 2023 10:03:36 +0100 Subject: [PATCH 1/3] refactor: rename "render" hooks to "visit" hooks --- .storybook/test-runner.ts | 2 +- README.md | 46 +++++++---- playwright/jest-setup.js | 15 ++-- src/playwright/hooks.ts | 26 +++++- src/playwright/transformPlaywright.test.ts | 80 +++++++++---------- src/playwright/transformPlaywright.ts | 8 +- .../transformPlaywrightJson.test.ts | 80 +++++++++---------- src/test-storybook.ts | 24 ++++++ src/typings.d.ts | 4 +- 9 files changed, 172 insertions(+), 113 deletions(-) diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index f775fff1..bc7f0e9d 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -15,7 +15,7 @@ const config: TestRunnerConfig = { setup() { expect.extend({ toMatchImageSnapshot }); }, - async postRender(page, context) { + async postVisit(page, context) { // Get entire context of a story, including parameters, args, argTypes, etc. const { parameters } = await getStoryContext(page, context); diff --git a/README.md b/README.md index ce2ca99d..dec247da 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,10 @@ Storybook test runner turns all of your stories into executable tests. - [4 - Run tests with --shard flag](#4---run-tests-with---shard-flag) - [Test hooks API](#test-hooks-api) - [setup](#setup) - - [preRender](#prerender) - - [postRender](#postrender) + - [preRender (deprecated)](#prerender-deprecated) + - [preVisit](#previsit) + - [postRender (deprecated)](#postrender-deprecated) + - [postVisit](#postvisit) - [Render lifecycle](#render-lifecycle) - [prepare](#prepare) - [getHttpHeaders](#gethttpheaders) @@ -542,12 +544,12 @@ The test runner renders a story and executes its [play function](https://storybo To enable use cases like visual or DOM snapshots, the test runner exports test hooks that can be overridden globally. These hooks give you access to the test lifecycle before and after the story is rendered. -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. +There are three hooks: `setup`, `preVisit`, and `postVisit`. `setup` executes once before all the tests run. `preVisit` and `postVisit` execute within a test before and after a story is rendered. 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. +> The `preVisit` and `postVisit` functions will be executed for all stories. #### setup @@ -562,7 +564,12 @@ module.exports = { }; ``` -#### preRender +#### preRender (deprecated) + +> **Note** +> This hook is deprecated. It has been renamed to `preVisit`, please use it instead. + +#### preVisit 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. @@ -570,13 +577,18 @@ Executes within a test before the story is rendered. Useful for configuring the ```js // .storybook/test-runner.js module.exports = { - async preRender(page, context) { + async preVisit(page, context) { // execute whatever you like, before the story renders }, }; ``` -#### postRender +#### postRender (deprecated) + +> **Note** +> This hook is deprecated. It has been renamed to `postVisit`, please use it instead. + +#### postVisit 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. @@ -584,7 +596,7 @@ Executes within a test after a story is rendered. Useful for asserting things af ```js // .storybook/test-runner.js module.exports = { - async postRender(page, context) { + async postVisit(page, context) { // execute whatever you like, after the story renders }, }; @@ -609,13 +621,13 @@ it('button--basic', async () => { await page.goto(STORYBOOK_URL); // pre-render hook - if (preRender) await preRender(page, context); + if (preVisit) await preVisit(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); + if (postVisit) await postVisit(page, context); }); ``` @@ -698,7 +710,7 @@ You can access its context in a test hook like so: const { getStoryContext } = require('@storybook/test-runner'); module.exports = { - async postRender(page, context) { + async postVisit(page, context) { // Get entire context of a story, including parameters, args, argTypes, etc. const storyContext = await getStoryContext(page, context); if (storyContext.parameters.theme === 'dark') { @@ -721,7 +733,7 @@ The `waitForPageReady` utility is useful when you're executing [image snapshot t const { waitForPageReady } = require('@storybook/test-runner'); module.exports = { - async postRender(page, context) { + async postVisit(page, context) { // use the test-runner utility to wait for fonts to load, etc. await waitForPageReady(page); @@ -772,7 +784,7 @@ const { MINIMAL_VIEWPORTS } = require('@storybook/addon-viewport'); const DEFAULT_VIEWPORT_SIZE = { width: 1280, height: 720 }; module.exports = { - async preRender(page, story) { + async preVisit(page, story) { const context = await getStoryContext(page, story); const viewportName = context.parameters?.viewport?.defaultViewport; const viewportParameter = MINIMAL_VIEWPORTS[viewportName]; @@ -806,11 +818,11 @@ const { getStoryContext } = require('@storybook/test-runner'); const { injectAxe, checkA11y, configureAxe } = require('axe-playwright'); module.exports = { - async preRender(page, context) { + async preVisit(page, context) { // Inject Axe utilities in the page before the story renders await injectAxe(page); }, - async postRender(page, context) { + async postVisit(page, context) { // Get entire context of a story, including parameters, args, argTypes, etc. const storyContext = await getStoryContext(page, context); @@ -844,7 +856,7 @@ You can use [Playwright's built in APIs](https://playwright.dev/docs/test-snapsh ```js // .storybook/test-runner.js module.exports = { - async postRender(page, context) { + async postVisit(page, context) { // the #storybook-root element wraps the story. In Storybook 6.x, the selector is #root const elementHandler = await page.$('#storybook-root'); const innerHTML = await elementHandler.innerHTML(); @@ -903,7 +915,7 @@ module.exports = { setup() { expect.extend({ toMatchImageSnapshot }); }, - async postRender(page, context) { + async postVisit(page, context) { // use the test-runner utility to wait for fonts to load, etc. await waitForPageReady(page); diff --git a/playwright/jest-setup.js b/playwright/jest-setup.js index 364123c6..c61fa0ed 100644 --- a/playwright/jest-setup.js +++ b/playwright/jest-setup.js @@ -1,15 +1,20 @@ -const { getTestRunnerConfig, setPreRender, setPostRender, setupPage } = require('../dist'); +const { getTestRunnerConfig, setPreVisit, setPostVisit, setupPage } = require('../dist'); const testRunnerConfig = getTestRunnerConfig(process.env.STORYBOOK_CONFIG_DIR); if (testRunnerConfig) { + // hooks set up if (testRunnerConfig.setup) { testRunnerConfig.setup(); } - if (testRunnerConfig.preRender) { - setPreRender(testRunnerConfig.preRender); + + const preVisitFn = testRunnerConfig.preRender || testRunnerConfig.preVisit; + if (preVisitFn) { + setPreVisit(preVisitFn); } - if (testRunnerConfig.postRender) { - setPostRender(testRunnerConfig.postRender); + + const postVisitFn = testRunnerConfig.postRender || testRunnerConfig.postVisit; + if (postVisitFn) { + setPostVisit(postVisitFn); } } diff --git a/src/playwright/hooks.ts b/src/playwright/hooks.ts index dcc094ab..68222517 100644 --- a/src/playwright/hooks.ts +++ b/src/playwright/hooks.ts @@ -19,8 +19,26 @@ export type PrepareSetter = (context: PrepareContext) => Promise; export interface TestRunnerConfig { setup?: () => void; + /** + * @deprecated Use `preVisit` instead. + */ preRender?: TestHook; + /** + * @deprecated Use `postVisit` instead. + */ postRender?: TestHook; + /** + * Runs before each story is visited. By this point, the story is not rendered in the browser. + * This is useful for preparing the browser environment such as setting viewport size, etc. + * @see https://github.com/storybookjs/test-runner#previsit + */ + preVisit?: TestHook; + /** + * Runs after each story is visited. This means the story has finished rendering and running its play function. + * This is useful for taking screenshots, snapshots, accessibility tests, etc. + * @see https://github.com/storybookjs/test-runner#postvisit + */ + postVisit?: TestHook; /** * Adds http headers to the test-runner's requests. This is useful if you need to set headers such as `Authorization` for your Storybook instance. */ @@ -41,12 +59,12 @@ export interface TestRunnerConfig { }; } -export const setPreRender = (preRender: TestHook) => { - globalThis.__sbPreRender = preRender; +export const setPreVisit = (preVisit: TestHook) => { + globalThis.__sbPreVisit = preVisit; }; -export const setPostRender = (postRender: TestHook) => { - globalThis.__sbPostRender = postRender; +export const setPostVisit = (postVisit: TestHook) => { + globalThis.__sbPostVisit = postVisit; }; export const getStoryContext = async (page: Page, context: TestContext): Promise => { diff --git a/src/playwright/transformPlaywright.test.ts b/src/playwright/transformPlaywright.test.ts index f72e1aaa..afecce62 100644 --- a/src/playwright/transformPlaywright.test.ts +++ b/src/playwright/transformPlaywright.test.ts @@ -75,8 +75,8 @@ describe('Playwright', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -84,8 +84,8 @@ describe('Playwright', () => { }) => __test(id, hasPlayFn), { id: "example-foo-bar--a" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -129,8 +129,8 @@ describe('Playwright', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -138,8 +138,8 @@ describe('Playwright', () => { }) => __test(id, hasPlayFn), { id: "example-foo-bar--b" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -201,8 +201,8 @@ describe('Playwright', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -210,8 +210,8 @@ describe('Playwright', () => { }) => __test(id, hasPlayFn), { id: "example-foo-bar--b" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -273,8 +273,8 @@ describe('Playwright', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -282,8 +282,8 @@ describe('Playwright', () => { }) => __test(id, hasPlayFn), { id: "example-foo-bar--a" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -327,8 +327,8 @@ describe('Playwright', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -336,8 +336,8 @@ describe('Playwright', () => { }) => __test(id, hasPlayFn), { id: "example-foo-bar--b" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -408,8 +408,8 @@ describe('Playwright', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -417,8 +417,8 @@ describe('Playwright', () => { }) => __test(id, hasPlayFn), { id: "example-foo-bar--b" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -462,8 +462,8 @@ describe('Playwright', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -471,8 +471,8 @@ describe('Playwright', () => { }) => __test(id, hasPlayFn), { id: "example-foo-bar--c" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -548,8 +548,8 @@ describe('Playwright', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -557,8 +557,8 @@ describe('Playwright', () => { }) => __test(id, hasPlayFn), { id: "example-foo-bar--a" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -618,8 +618,8 @@ describe('Playwright', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -627,8 +627,8 @@ describe('Playwright', () => { }) => __test(id, hasPlayFn), { id: "example-foo-bar--a" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -688,8 +688,8 @@ describe('Playwright', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -697,8 +697,8 @@ describe('Playwright', () => { }) => __test(id, hasPlayFn), { id: "example-header--a" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); diff --git a/src/playwright/transformPlaywright.ts b/src/playwright/transformPlaywright.ts index 0e98402e..c3f9494c 100644 --- a/src/playwright/transformPlaywright.ts +++ b/src/playwright/transformPlaywright.ts @@ -24,16 +24,16 @@ export const testPrefixer = template( page.evaluate(({ id, err }) => __throwError(id, err), { id: %%id%%, err: err.message }); }); - if(globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if(globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, hasPlayFn }) => __test(id, hasPlayFn), { id: %%id%%, }); - if(globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if(globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if(globalThis.__sbCollectCoverage) { diff --git a/src/playwright/transformPlaywrightJson.test.ts b/src/playwright/transformPlaywrightJson.test.ts index 63a1a8ed..d644c330 100644 --- a/src/playwright/transformPlaywrightJson.test.ts +++ b/src/playwright/transformPlaywrightJson.test.ts @@ -55,8 +55,8 @@ describe('Playwright Json', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -64,8 +64,8 @@ describe('Playwright Json', () => { }) => __test(id, hasPlayFn), { id: "example-header--logged-in" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -109,8 +109,8 @@ describe('Playwright Json', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -118,8 +118,8 @@ describe('Playwright Json', () => { }) => __test(id, hasPlayFn), { id: "example-header--logged-out" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -165,8 +165,8 @@ describe('Playwright Json', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -174,8 +174,8 @@ describe('Playwright Json', () => { }) => __test(id, hasPlayFn), { id: "example-page--logged-in" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -268,8 +268,8 @@ describe('Playwright Json', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -277,8 +277,8 @@ describe('Playwright Json', () => { }) => __test(id, hasPlayFn), { id: "example-b" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -324,8 +324,8 @@ describe('Playwright Json', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -333,8 +333,8 @@ describe('Playwright Json', () => { }) => __test(id, hasPlayFn), { id: "example-c" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -405,8 +405,8 @@ describe('Playwright Json', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -414,8 +414,8 @@ describe('Playwright Json', () => { }) => __test(id, hasPlayFn), { id: "example-page--logged-in" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -514,8 +514,8 @@ describe('Playwright Json', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -523,8 +523,8 @@ describe('Playwright Json', () => { }) => __test(id, hasPlayFn), { id: "example-header--logged-in" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -568,8 +568,8 @@ describe('Playwright Json', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -577,8 +577,8 @@ describe('Playwright Json', () => { }) => __test(id, hasPlayFn), { id: "example-header--logged-out" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -624,8 +624,8 @@ describe('Playwright Json', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -633,8 +633,8 @@ describe('Playwright Json', () => { }) => __test(id, hasPlayFn), { id: "example-page--logged-in" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -718,8 +718,8 @@ describe('Playwright Json', () => { err: err.message }); }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -727,8 +727,8 @@ describe('Playwright Json', () => { }) => __test(id, hasPlayFn), { id: "example-page--logged-in" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); diff --git a/src/test-storybook.ts b/src/test-storybook.ts index 5332eb22..b5cd1879 100644 --- a/src/test-storybook.ts +++ b/src/test-storybook.ts @@ -275,6 +275,17 @@ function ejectConfiguration() { log('Configuration file successfully copied as test-runner-jest.config.js'); } +function warnOnce(msg: string) { + let warned = false; + return () => { + if (!warned) { + // here we specify the ansi code for yellow as jest is stripping the default color from console.warn + console.warn('\x1b[33m%s\x1b[0m', msg); + warned = true; + } + }; +} + const main = async () => { const { jestOptions, runnerOptions } = getCliOptions(); @@ -286,6 +297,19 @@ const main = async () => { process.env.STORYBOOK_CONFIG_DIR = runnerOptions.configDir; const testRunnerConfig = getTestRunnerConfig(runnerOptions.configDir) || ({} as TestRunnerConfig); + + // TODO: remove preRender and postRender hooks likely in 0.20.0 + if (testRunnerConfig.preRender) { + warnOnce( + '[Test-runner] The "preRender" hook is deprecated and will be removed in later versions. Please use "preVisit" instead in your test-runner config file.' + )(); + } + if (testRunnerConfig.postRender) { + warnOnce( + '[Test-runner] The "postRender" hook is deprecated and will be removed in later versions. Please use "postVisit" instead in your test-runner config file.' + )(); + } + if (testRunnerConfig.getHttpHeaders) { getHttpHeaders = testRunnerConfig.getHttpHeaders; } diff --git a/src/typings.d.ts b/src/typings.d.ts index 4bf84d6a..a756e2cc 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -1,7 +1,7 @@ import { TestHook } from './playwright/hooks'; declare global { - var __sbPreRender: TestHook; - var __sbPostRender: TestHook; + var __sbPreVisit: TestHook; + var __sbPostVisit: TestHook; var __getContext: (storyId: string) => any; } From 6db764ff40483c6a55ccd45b5a64e26b72a0ceac Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 17 Nov 2023 10:08:17 +0100 Subject: [PATCH 2/3] update documentation to use Typescript --- MIGRATION.test-runner.md | 4 +- README.md | 145 +++++++++++++++----------- playwright/test-runner-jest.config.js | 2 +- 3 files changed, 88 insertions(+), 63 deletions(-) diff --git a/MIGRATION.test-runner.md b/MIGRATION.test-runner.md index 0d5f8985..1db408fd 100644 --- a/MIGRATION.test-runner.md +++ b/MIGRATION.test-runner.md @@ -9,7 +9,7 @@ - [Storyshots x Test Runner Comparison table](#storyshots-x-test-runner-comparison-table) - [Migration Steps](#migration-steps) - [Replacing `@storybook/addon-storyshots` with `@storybook/test-runner`:](#replacing-storybookaddon-storyshots-with-storybooktest-runner) - - [Migrating storyshots features](#migrating-storyshots-features) + - [Migrating Storyshots features](#migrating-storyshots-features) - [Smoke testing](#smoke-testing) - [Accessibility testing](#accessibility-testing) - [Image snapshot testing](#image-snapshot-testing) @@ -135,7 +135,7 @@ import { getJestConfig } from '@storybook/test-runner'; const defaultConfig = getJestConfig(); const config = { - // The default configuration comes from @storybook/test-runner + // The default Jest configuration comes from @storybook/test-runner ...defaultConfig, snapshotResolver: './snapshot-resolver.js', }; diff --git a/README.md b/README.md index dec247da..5db88421 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ const testRunnerConfig = getJestConfig(); * @type {import('@jest/types').Config.InitialOptions} */ module.exports = { - // The default configuration comes from @storybook/test-runner + // The default Jest configuration comes from @storybook/test-runner ...testRunnerConfig, /** Add your own overrides below * @see https://jestjs.io/docs/configuration @@ -299,10 +299,13 @@ If your Storybook does not have a `stories.json` file, you can generate one, pro To enable `stories.json` in your Storybook, set the `buildStoriesJson` feature flag in `.storybook/main.js`: -```js -module.exports = { +```ts +// .storybook/main.ts +const config = { + // ... rest of the config features: { buildStoriesJson: true }, }; +export default config; ``` Once you have a valid `stories.json` file, your Storybook will be compatible with the "index.json mode". @@ -409,12 +412,13 @@ yarn add -D @storybook/addon-coverage And register it in your `.storybook/main.js` file: -```js -// .storybook/main.js -module.exports = { +```ts +// .storybook/main.ts +const config = { // ...rest of your code here addons: ['@storybook/addon-coverage'], }; +export default config; ``` The addon has default options that might suffice for your project, and it accepts an [options object for project-specific configuration](https://github.com/storybookjs/addon-coverage#configuring-the-addon). @@ -555,13 +559,16 @@ All three functions can be set up in the configuration file `.storybook/test-run 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 = { +```ts +// .storybook/test-runner.ts +import type { TestRunnerConfig } from '@storybook/test-runner'; + +const config: TestRunnerConfig = { async setup() { // execute whatever you like, in Node, once before all tests run }, }; +export default config; ``` #### preRender (deprecated) @@ -574,13 +581,16 @@ module.exports = { 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 = { +```ts +// .storybook/test-runner.ts +import type { TestRunnerConfig } from '@storybook/test-runner'; + +const config: TestRunnerConfig = { async preVisit(page, context) { // execute whatever you like, before the story renders }, }; +export default config; ``` #### postRender (deprecated) @@ -593,13 +603,16 @@ module.exports = { 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 = { +```ts +// .storybook/test-runner.ts +import type { TestRunnerConfig } from '@storybook/test-runner'; + +const config: TestRunnerConfig = { async postVisit(page, context) { // execute whatever you like, after the story renders }, }; +export default config; ``` > **Note** @@ -609,7 +622,7 @@ module.exports = { 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 +```ts // executed once, before the tests await setup(); @@ -620,13 +633,13 @@ it('button--basic', async () => { // playwright page https://playwright.dev/docs/pages await page.goto(STORYBOOK_URL); - // pre-render hook + // pre-visit hook if (preVisit) await preVisit(page, context); - // render the story and run its play function (if applicable) + // render the story and watch its play function (if applicable) await page.execute('render', context); - // post-render hook + // post-visit hook if (postVisit) await postVisit(page, context); }); ``` @@ -654,9 +667,11 @@ For reference, please use the [default `prepare`](https://github.com/storybookjs The test-runner makes a few `fetch` calls to check the status of a Storybook instance, and to get the index of the Storybook's stories. Additionally, it visits a page using Playwright. In all of these scenarios, it's possible, depending on where your Storybook is hosted, that you might need to set some HTTP headers. For example, if your Storybook is hosted behind a basic authentication, you might need to set the `Authorization` header. You can do so by passing a `getHttpHeaders` function to your test-runner config. That function receives the `url` of the fetch calls and page visits, and should return an object with the headers to be set. -```js -// .storybook/test-runner.js -module.exports = { +```ts +// .storybook/test-runner.ts +import type { TestRunnerConfig } from '@storybook/test-runner'; + +const config: TestRunnerConfig = { getHttpHeaders: async (url) => { const token = url.includes('prod') ? 'XYZ' : 'ABC'; return { @@ -664,21 +679,25 @@ module.exports = { }; }, }; +export default config; ``` #### tags The `tags` property contains three options: `include | exclude | skip`, each accepting an array of strings: -```js -// .storybook/test-runner.js -module.exports = { +```ts +// .storybook/test-runner.ts +import type { TestRunnerConfig } from '@storybook/test-runner'; + +const config: TestRunnerConfig = { tags: { include: [], // string array, e.g. ['test-only'] exclude: [], // string array, e.g. ['design', 'docs-only'] skip: [], // string array, e.g. ['design'] }, }; +export default config; ``` `tags` are used for filtering your tests. Learn more [here](#filtering-tests-experimental). @@ -693,7 +712,7 @@ While running tests using the hooks, you might want to get information from a st Suppose your story looks like this: -```js +```ts // ./Button.stories.ts export const Primary = { @@ -705,12 +724,12 @@ export const Primary = { You can access its context in a test hook like so: -```js -// .storybook/test-runner.js -const { getStoryContext } = require('@storybook/test-runner'); +```ts +// .storybook/test-runner.ts +import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner'; -module.exports = { - async postVisit(page, context) { +const config: TestRunnerConfig = { + 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') { @@ -720,6 +739,7 @@ module.exports = { } }, }; +export default config; ``` It's useful for skipping or enhancing use cases like [image snapshot testing](#image-snapshot), [accessibility testing](#accessibility-testing) and more. @@ -728,25 +748,26 @@ It's useful for skipping or enhancing use cases like [image snapshot testing](#i 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'); +```ts +// .storybook/test-runner.ts +import { TestRunnerConfig, waitForPageReady } from '@storybook/test-runner'; -module.exports = { - async postVisit(page, context) { +const config: TestRunnerConfig = { + async postRender(page, context) { // use the test-runner utility to wait for fonts to load, etc. await waitForPageReady(page); // by now, we know that the page is fully loaded }, }; +export default config; ``` #### StorybookTestRunner user agent 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 +```ts // At the render level, useful for dynamically rendering something based on the test-runner export const MyStory = { render: () => { @@ -776,14 +797,14 @@ Below you will find recipes that use both the hooks and the utility functions to You can use [Playwright's Page viewport utility](https://playwright.dev/docs/api/class-page#page-set-viewport-size) to programatically change the viewport size of your test. If you use [@storybook/addon-viewports](https://storybook.js.org/addons/@storybook/addon-viewport), you can reuse its parameters and make sure that the tests match in configuration. -```js -// .storybook/test-runner.js -const { getStoryContext } = require('@storybook/test-runner'); -const { MINIMAL_VIEWPORTS } = require('@storybook/addon-viewport'); +```ts +// .storybook/test-runner.ts +import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner'; +import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport'; const DEFAULT_VIEWPORT_SIZE = { width: 1280, height: 720 }; -module.exports = { +const config: TestRunnerConfig = { async preVisit(page, story) { const context = await getStoryContext(page, story); const viewportName = context.parameters?.viewport?.defaultViewport; @@ -805,6 +826,7 @@ module.exports = { } }, }; +export default config; ``` ### Accessibility testing @@ -812,12 +834,12 @@ module.exports = { You can install `axe-playwright` and use it in tandem with the test-runner to test the accessibility of your components. If you use [`@storybook/addon-a11y`](https://storybook.js.org/addons/@storybook/addon-a11y), you can reuse its parameters and make sure that the tests match in configuration, both in the accessibility addon panel and the test-runner. -```js -// .storybook/test-runner.js -const { getStoryContext } = require('@storybook/test-runner'); -const { injectAxe, checkA11y, configureAxe } = require('axe-playwright'); +```ts +// .storybook/test-runner.ts +import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner'; +import { injectAxe, checkA11y, configureAxe } from 'axe-playwright'; -module.exports = { +const config: TestRunnerConfig = { async preVisit(page, context) { // Inject Axe utilities in the page before the story renders await injectAxe(page); @@ -847,15 +869,18 @@ module.exports = { }); }, }; +export default config; ``` ### DOM snapshot (HTML) You can use [Playwright's built in APIs](https://playwright.dev/docs/test-snapshots) for DOM snapshot testing: -```js -// .storybook/test-runner.js -module.exports = { +```ts +// .storybook/test-runner.ts +import type { TestRunnerConfig } from '@storybook/test-runner'; + +const config: TestRunnerConfig = { async postVisit(page, context) { // the #storybook-root element wraps the story. In Storybook 6.x, the selector is #root const elementHandler = await page.$('#storybook-root'); @@ -863,11 +888,12 @@ module.exports = { expect(innerHTML).toMatchSnapshot(); }, }; +export default config; ``` 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: -```js +```ts // ./test-runner-jest.config.js const { getJestConfig } = require('@storybook/test-runner'); @@ -877,13 +903,13 @@ const testRunnerConfig = getJestConfig(); * @type {import('@jest/types').Config.InitialOptions} */ module.exports = { - // The default configuration comes from @storybook/test-runner + // The default Jest configuration comes from @storybook/test-runner ...testRunnerConfig, snapshotResolver: './snapshot-resolver.js', }; ``` -```js +```ts // ./snapshot-resolver.js const path = require('path'); @@ -904,14 +930,14 @@ module.exports = { Here's a slightly different recipe for image snapshot testing: -```js -// .storybook/test-runner.js -const { waitForPageReady } = require('@storybook/test-runner'); -const { toMatchImageSnapshot } = require('jest-image-snapshot'); +```ts +// .storybook/test-runner.ts +import { TestRunnerConfig, waitForPageReady } from '@storybook/test-runner'; +import { toMatchImageSnapshot } from 'jest-image-snapshot'; const customSnapshotsDir = `${process.cwd()}/__snapshots__`; -module.exports = { +const config: TestRunnerConfig = { setup() { expect.extend({ toMatchImageSnapshot }); }, @@ -928,10 +954,9 @@ module.exports = { }); }, }; +export default config; ``` -There is also an exported `TestRunnerConfig` type available for TypeScript users. - ## Troubleshooting #### The error output in the CLI is too short diff --git a/playwright/test-runner-jest.config.js b/playwright/test-runner-jest.config.js index 33949e5d..bd81baef 100644 --- a/playwright/test-runner-jest.config.js +++ b/playwright/test-runner-jest.config.js @@ -1,6 +1,6 @@ const { getJestConfig } = require('@storybook/test-runner'); -// The default configuration comes from @storybook/test-runner +// The default Jest configuration comes from @storybook/test-runner const testRunnerConfig = getJestConfig(); /** From e6df920bcc04abb51ad0cc165f9ebff8115acfbc Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Fri, 17 Nov 2023 10:21:51 +0100 Subject: [PATCH 3/3] throw an error when hooks are defined twice --- playwright/jest-setup.js | 4 ++-- src/test-storybook.ts | 21 +++++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/playwright/jest-setup.js b/playwright/jest-setup.js index c61fa0ed..7e319565 100644 --- a/playwright/jest-setup.js +++ b/playwright/jest-setup.js @@ -7,12 +7,12 @@ if (testRunnerConfig) { testRunnerConfig.setup(); } - const preVisitFn = testRunnerConfig.preRender || testRunnerConfig.preVisit; + const preVisitFn = testRunnerConfig.preVisit || testRunnerConfig.preRender; if (preVisitFn) { setPreVisit(preVisitFn); } - const postVisitFn = testRunnerConfig.postRender || testRunnerConfig.postVisit; + const postVisitFn = testRunnerConfig.postVisit || testRunnerConfig.postRender; if (postVisitFn) { setPostVisit(postVisitFn); } diff --git a/src/test-storybook.ts b/src/test-storybook.ts index b5cd1879..e46d2771 100644 --- a/src/test-storybook.ts +++ b/src/test-storybook.ts @@ -36,6 +36,7 @@ process.on('unhandledRejection', (err) => { }); const log = (message: string) => console.log(`[test-storybook] ${message}`); +const warn = (message: string) => console.warn('\x1b[33m%s\x1b[0m', `[test-storybook] ${message}`); const error = (err: { message: any; stack: any }) => { if (err instanceof Error) { console.error(`\x1b[31m[test-storybook]\x1b[0m ${err.message} \n\n${err.stack}`); @@ -275,12 +276,12 @@ function ejectConfiguration() { log('Configuration file successfully copied as test-runner-jest.config.js'); } -function warnOnce(msg: string) { +function warnOnce(message: string) { let warned = false; return () => { if (!warned) { // here we specify the ansi code for yellow as jest is stripping the default color from console.warn - console.warn('\x1b[33m%s\x1b[0m', msg); + warn(message); warned = true; } }; @@ -298,15 +299,27 @@ const main = async () => { const testRunnerConfig = getTestRunnerConfig(runnerOptions.configDir) || ({} as TestRunnerConfig); + if (testRunnerConfig.preVisit && testRunnerConfig.preRender) { + throw new Error( + 'You cannot use both preVisit and preRender hooks in your test-runner config file. Please use preVisit instead.' + ); + } + + if (testRunnerConfig.postVisit && testRunnerConfig.postRender) { + throw new Error( + 'You cannot use both postVisit and postRender hooks in your test-runner config file. Please use postVisit instead.' + ); + } + // TODO: remove preRender and postRender hooks likely in 0.20.0 if (testRunnerConfig.preRender) { warnOnce( - '[Test-runner] The "preRender" hook is deprecated and will be removed in later versions. Please use "preVisit" instead in your test-runner config file.' + 'The "preRender" hook is deprecated and will be removed in later versions. Please use "preVisit" instead in your test-runner config file.' )(); } if (testRunnerConfig.postRender) { warnOnce( - '[Test-runner] The "postRender" hook is deprecated and will be removed in later versions. Please use "postVisit" instead in your test-runner config file.' + 'The "postRender" hook is deprecated and will be removed in later versions. Please use "postVisit" instead in your test-runner config file.' )(); }