diff --git a/README.md b/README.md index 0df996b5..054a3c84 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,15 @@ Storybook test runner turns all of your stories into executable tests. - [Table of Contents](#table-of-contents) - [Features](#features) - [Getting started](#getting-started) + - [CLI Options](#cli-options) - [Configuration](#configuration) - [Running against a deployed Storybook](#running-against-a-deployed-storybook) - [Stories.json mode](#storiesjson-mode) - [Running in CI](#running-in-ci) - [1. Running against deployed Storybooks on Github Actions deployment](#1-running-against-deployed-storybooks-on-github-actions-deployment) - [2. Running against locally built Storybooks in CI](#2-running-against-locally-built-storybooks-in-ci) + - [Experimental test hook API](#experimental-test-hook-api) + - [Image snapshot recipe](#image-snapshot-recipe) - [Troubleshooting](#troubleshooting) - [The test runner seems flaky and keeps timing out](#the-test-runner-seems-flaky-and-keeps-timing-out) - [Adding the test runner to other CI environments](#adding-the-test-runner-to-other-ci-environments) @@ -98,16 +101,16 @@ yarn test-storybook Usage: test-storybook [options] ``` -| Options | Description | -| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--help` | Output usage information
`test-storybook --help` | -| `-s`, `--stories-json` | Run in stories json mode (requires a compatible Storybook)
`test-storybook --stories-json` | -| `-c`, `--config-dir [dir-name]` | Directory where to load Storybook configurations from
`test-storybook -c .storybook` | -| `--watch` | Run in watch mode
`test-storybook --watch` | -| `--maxWorkers [amount]` | Specifies the maximum number of workers the worker-pool will spawn for running tests
`test-storybook --maxWorkers=2` | -| `--no-cache` | Disable the cache
`test-storybook --no-cache` | -| `--clearCache` | Deletes the Jest cache directory and then exits without running tests
`test-storybook --clearCache` | -| `--verbose` | Display individual test results with the test suite hierarchy
`test-storybook --verbose` | +| Options | Description | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `--help` | Output usage information
`test-storybook --help` | +| `-s`, `--stories-json` | Run in stories json mode (requires a compatible Storybook)
`test-storybook --stories-json` | +| `-c`, `--config-dir [dir-name]` | Directory where to load Storybook configurations from
`test-storybook -c .storybook` | +| `--watch` | Run in watch mode
`test-storybook --watch` | +| `--maxWorkers [amount]` | Specifies the maximum number of workers the worker-pool will spawn for running tests
`test-storybook --maxWorkers=2` | +| `--no-cache` | Disable the cache
`test-storybook --no-cache` | +| `--clearCache` | Deletes the Jest cache directory and then exits without running tests
`test-storybook --clearCache` | +| `--verbose` | Display individual test results with the test suite hierarchy
`test-storybook --verbose` | ## Configuration @@ -245,6 +248,59 @@ jobs: > **_NOTE:_** Building Storybook locally makes it simple to test Storybooks that could be available remotely, but are under authentication layers. If you also deploy your Storybooks somewhere (e.g. Chromatic, Vercel, etc.), the Storybook URL can still be useful with the test-runner. You can pass it to the `REFERENCE_URL` environment variable when running the test-storybook command, and if a story fails, the test-runner will provide a helpful message with the link to the story in your published Storybook instead. +## Experimental test hook 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. + +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. + +The hooks, `preRender` and `postRender`, are functions that take a [Playwright Page](https://playwright.dev/docs/pages) and a context object with the current story `id`, `title`, and `name`. They are globally settable by `@storybook/test-runner`'s `setPreRender` and `setPostRender` APIs. + +> **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. + +To visualize the test lifecycle, consider a simplified version of the test code automatically generated for each story in your Storybook: + +```js +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); +}); +``` + +### Image snapshot recipe + +If you want to make the test runner take image snapshots, the following recipe uses test hooks in `jest-setup.js` to do it: + +```js +const { toMatchImageSnapshot } = require('jest-image-snapshot'); +const { setPostRender } = require('@storybook/test-runner'); + +expect.extend({ toMatchImageSnapshot }); + +// use custom directory/id to align CSF and stories.json mode outputs +const customSnapshotsDir = `${process.cwd()}/__snapshots__`; + +setPostRender(async (page, context) => { + const image = await page.screenshot(); + expect(image).toMatchImageSnapshot({ + customSnapshotsDir, + customSnapshotIdentifier: context.id, + }); +}); +``` + ## Troubleshooting #### The test runner seems flaky and keeps timing out diff --git a/__snapshots__/basic-button--demo-snap.png b/__snapshots__/basic-button--demo-snap.png new file mode 100644 index 00000000..a8d5b8b7 Binary files /dev/null and b/__snapshots__/basic-button--demo-snap.png differ diff --git a/__snapshots__/basic-button--find-by-snap.png b/__snapshots__/basic-button--find-by-snap.png new file mode 100644 index 00000000..ca404b3b Binary files /dev/null and b/__snapshots__/basic-button--find-by-snap.png differ diff --git a/__snapshots__/basic-button--primary-snap.png b/__snapshots__/basic-button--primary-snap.png new file mode 100644 index 00000000..7596eac7 Binary files /dev/null and b/__snapshots__/basic-button--primary-snap.png differ diff --git a/__snapshots__/basic-button--wait-for-element-to-be-removed-snap.png b/__snapshots__/basic-button--wait-for-element-to-be-removed-snap.png new file mode 100644 index 00000000..ca404b3b Binary files /dev/null and b/__snapshots__/basic-button--wait-for-element-to-be-removed-snap.png differ diff --git a/__snapshots__/basic-button--wait-for-snap.png b/__snapshots__/basic-button--wait-for-snap.png new file mode 100644 index 00000000..a8d5b8b7 Binary files /dev/null and b/__snapshots__/basic-button--wait-for-snap.png differ diff --git a/__snapshots__/basic-button--with-loaders-snap.png b/__snapshots__/basic-button--with-loaders-snap.png new file mode 100644 index 00000000..03f86b01 Binary files /dev/null and b/__snapshots__/basic-button--with-loaders-snap.png differ diff --git a/__snapshots__/example-header--logged-in-snap.png b/__snapshots__/example-header--logged-in-snap.png new file mode 100644 index 00000000..a6852616 Binary files /dev/null and b/__snapshots__/example-header--logged-in-snap.png differ diff --git a/__snapshots__/example-header--logged-out-snap.png b/__snapshots__/example-header--logged-out-snap.png new file mode 100644 index 00000000..4361d43d Binary files /dev/null and b/__snapshots__/example-header--logged-out-snap.png differ diff --git a/__snapshots__/example-page--logged-in-snap.png b/__snapshots__/example-page--logged-in-snap.png new file mode 100644 index 00000000..23b4632e Binary files /dev/null and b/__snapshots__/example-page--logged-in-snap.png differ diff --git a/__snapshots__/example-page--logged-out-snap.png b/__snapshots__/example-page--logged-out-snap.png new file mode 100644 index 00000000..6659063d Binary files /dev/null and b/__snapshots__/example-page--logged-out-snap.png differ diff --git a/jest-setup.js b/jest-setup.js new file mode 100644 index 00000000..99feca39 --- /dev/null +++ b/jest-setup.js @@ -0,0 +1,17 @@ +const { toMatchImageSnapshot } = require('jest-image-snapshot'); +const { setPostRender } = require('./dist/cjs'); + +expect.extend({ toMatchImageSnapshot }); + +// use custom directory/id to align CSF and stories.json mode outputs +const customSnapshotsDir = `${process.cwd()}/__snapshots__`; + +setPostRender(async (page, context) => { + const image = await page.screenshot(); + expect(image).toMatchImageSnapshot({ + customSnapshotsDir, + customSnapshotIdentifier: context.id, + failureThreshold: 0.03, + failureThresholdType: 'percent', + }); +}); diff --git a/package.json b/package.json index 5ca20aa3..1fc99d5b 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "dedent": "^0.7.0", "jest": "^27.0.6", "jest-environment-jsdom": "^27.0.6", + "jest-image-snapshot": "^4.5.1", "prettier": "^2.3.1", "prop-types": "^15.7.2", "react": "^17.0.1", @@ -107,6 +108,7 @@ "@storybook/csf": "0.0.2--canary.87bc651.0", "@storybook/csf-tools": "^6.4.14", "commander": "^9.0.0", + "global": "^4.4.0", "jest-playwright-preset": "^1.7.0", "node-fetch": "^2", "playwright": "^1.14.0", diff --git a/src/csf/transformCsf.ts b/src/csf/transformCsf.ts index 5e348d2c..8a65d1a2 100644 --- a/src/csf/transformCsf.ts +++ b/src/csf/transformCsf.ts @@ -13,8 +13,9 @@ export interface TestContext { title: t.Literal; id: t.Literal; } -type FilePrefixer = () => t.Statement[]; -type TestPrefixer = (context: TestContext) => t.Statement[]; +type TemplateResult = t.Statement | t.Statement[]; +type FilePrefixer = () => TemplateResult; +type TestPrefixer = (context: TestContext) => TemplateResult; interface TransformOptions { clearBody?: boolean; @@ -38,8 +39,7 @@ const prefixFunction = ( id: t.stringLiteral(toId(title, name)), }; - // instead, let's just make the prefixer return the function - const result = testPrefixer(context); + const result = makeArray(testPrefixer(context)); const stmt = result[1] as t.ExpressionStatement; return stmt.expression; }; @@ -69,6 +69,9 @@ const makeDescribe = (key: string, tests: t.Statement[]): t.Statement | null => ); }; +const makeArray = (templateResult: TemplateResult) => + Array.isArray(templateResult) ? templateResult : [templateResult]; + export const transformCsf = ( code: string, { @@ -110,7 +113,7 @@ export const transformCsf = ( // FIXME: insert between imports if (filePrefixer) { - const { code: prefixCode } = generate(t.program(filePrefixer()), {}); + const { code: prefixCode } = generate(t.program(makeArray(filePrefixer())), {}); result = `${prefixCode}\n`; } if (!clearBody) result = `${result}${code}\n`; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..c0807099 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from './playwright/hooks'; diff --git a/src/playwright/hooks.ts b/src/playwright/hooks.ts new file mode 100644 index 00000000..0b8b7d6d --- /dev/null +++ b/src/playwright/hooks.ts @@ -0,0 +1,18 @@ +import global from 'global'; +import type { Page } from 'playwright'; + +export type TestContext = { + id: string; + title: string; + name: string; +}; + +export type TestHook = (page: Page, context: TestContext) => Promise; + +export const setPreRender = (preRender: TestHook) => { + global.__sbPreRender = preRender; +}; + +export const setPostRender = (postRender: TestHook) => { + global.__sbPostRender = postRender; +}; diff --git a/src/playwright/transformPlaywright.test.ts b/src/playwright/transformPlaywright.test.ts index c93e791a..234ed667 100644 --- a/src/playwright/transformPlaywright.test.ts +++ b/src/playwright/transformPlaywright.test.ts @@ -48,10 +48,17 @@ describe('Playwright', () => { filename ) ).toMatchInlineSnapshot(` + import global from 'global'; + if (!require.main) { describe("foo/bar", () => { describe("A", () => { it("play-test", async () => { + const context = { + id: "foo-bar--a", + title: "foo/bar", + name: "A" + }; page.on('pageerror', err => { page.evaluate(({ id, @@ -61,11 +68,23 @@ describe('Playwright', () => { err: err.message }); }); - return page.evaluate(({ - id - }) => __test(id), { + + if (global.__sbPreRender) { + await global.__sbPreRender(page, context); + } + + const result = await page.evaluate(({ + id, + hasPlayFn + }) => __test(id, hasPlayFn), { id: "foo-bar--a" }); + + if (global.__sbPostRender) { + await global.__sbPostRender(page, context); + } + + return result; }); }); }); @@ -82,10 +101,17 @@ describe('Playwright', () => { filename ) ).toMatchInlineSnapshot(` + import global from 'global'; + if (!require.main) { describe("foo/bar", () => { describe("A", () => { it("smoke-test", async () => { + const context = { + id: "foo-bar--a", + title: "foo/bar", + name: "A" + }; page.on('pageerror', err => { page.evaluate(({ id, @@ -95,11 +121,23 @@ describe('Playwright', () => { err: err.message }); }); - return page.evaluate(({ - id - }) => __test(id), { + + if (global.__sbPreRender) { + await global.__sbPreRender(page, context); + } + + const result = await page.evaluate(({ + id, + hasPlayFn + }) => __test(id, hasPlayFn), { id: "foo-bar--a" }); + + if (global.__sbPostRender) { + await global.__sbPostRender(page, context); + } + + return result; }); }); }); @@ -117,10 +155,17 @@ describe('Playwright', () => { filename ) ).toMatchInlineSnapshot(` + import global from 'global'; + if (!require.main) { describe("Example/Header", () => { describe("A", () => { it("smoke-test", async () => { + const context = { + id: "example-header--a", + title: "Example/Header", + name: "A" + }; page.on('pageerror', err => { page.evaluate(({ id, @@ -130,11 +175,23 @@ describe('Playwright', () => { err: err.message }); }); - return page.evaluate(({ - id - }) => __test(id), { + + if (global.__sbPreRender) { + await global.__sbPreRender(page, context); + } + + const result = await page.evaluate(({ + id, + hasPlayFn + }) => __test(id, hasPlayFn), { id: "example-header--a" }); + + if (global.__sbPostRender) { + await global.__sbPostRender(page, context); + } + + return result; }); }); }); diff --git a/src/playwright/transformPlaywright.ts b/src/playwright/transformPlaywright.ts index fdf46c8d..25378905 100644 --- a/src/playwright/transformPlaywright.ts +++ b/src/playwright/transformPlaywright.ts @@ -6,17 +6,33 @@ import { autoTitle } from '@storybook/store'; import { getStorybookMain } from '../util/cli'; import { transformCsf } from '../csf/transformCsf'; +const filePrefixer = template(` + import global from 'global'; +`); + export const testPrefixer = template( ` console.log({ id: %%id%%, title: %%title%%, name: %%name%%, storyExport: %%storyExport%% }); async () => { + const context = { id: %%id%%, title: %%title%%, name: %%name%% }; + page.on('pageerror', (err) => { page.evaluate(({ id, err }) => __throwError(id, err), { id: %%id%%, err: err.message }); }); - return page.evaluate(({ id }) => __test(id), { - id: %%id%% + if(global.__sbPreRender) { + await global.__sbPreRender(page, context); + } + + const result = await page.evaluate(({ id, hasPlayFn }) => __test(id, hasPlayFn), { + id: %%id%%, }); + + if(global.__sbPostRender) { + await global.__sbPostRender(page, context); + } + + return result; } `, { @@ -46,6 +62,7 @@ const getDefaultTitle = (filename: string) => { export const transformPlaywright = (src: string, filename: string) => { const defaultTitle = getDefaultTitle(filename); const result = transformCsf(src, { + filePrefixer, // @ts-ignore testPrefixer, insertTestIfEmpty: true, diff --git a/src/playwright/transformPlaywrightJson.test.ts b/src/playwright/transformPlaywrightJson.test.ts index 8b69870a..6916f19f 100644 --- a/src/playwright/transformPlaywrightJson.test.ts +++ b/src/playwright/transformPlaywrightJson.test.ts @@ -52,6 +52,11 @@ Object { "example-header": "describe(\\"Example/Header\\", () => { describe(\\"Logged In\\", () => { it(\\"test\\", async () => { + const context = { + id: \\"example-header--logged-in\\", + title: \\"Example/Header\\", + name: \\"Logged In\\" + }; page.on('pageerror', err => { page.evaluate(({ id, @@ -61,15 +66,32 @@ Object { err: err.message }); }); - return page.evaluate(({ - id - }) => __test(id), { + + if (global.__sbPreRender) { + await global.__sbPreRender(page, context); + } + + const result = await page.evaluate(({ + id, + hasPlayFn + }) => __test(id, hasPlayFn), { id: \\"example-header--logged-in\\" }); + + if (global.__sbPostRender) { + await global.__sbPostRender(page, context); + } + + return result; }); }); describe(\\"Logged Out\\", () => { it(\\"test\\", async () => { + const context = { + id: \\"example-header--logged-out\\", + title: \\"Example/Header\\", + name: \\"Logged Out\\" + }; page.on('pageerror', err => { page.evaluate(({ id, @@ -79,17 +101,34 @@ Object { err: err.message }); }); - return page.evaluate(({ - id - }) => __test(id), { + + if (global.__sbPreRender) { + await global.__sbPreRender(page, context); + } + + const result = await page.evaluate(({ + id, + hasPlayFn + }) => __test(id, hasPlayFn), { id: \\"example-header--logged-out\\" }); + + if (global.__sbPostRender) { + await global.__sbPostRender(page, context); + } + + return result; }); }); });", "example-page": "describe(\\"Example/Page\\", () => { describe(\\"Logged In\\", () => { it(\\"test\\", async () => { + const context = { + id: \\"example-page--logged-in\\", + title: \\"Example/Page\\", + name: \\"Logged In\\" + }; page.on('pageerror', err => { page.evaluate(({ id, @@ -99,11 +138,23 @@ Object { err: err.message }); }); - return page.evaluate(({ - id - }) => __test(id), { + + if (global.__sbPreRender) { + await global.__sbPreRender(page, context); + } + + const result = await page.evaluate(({ + id, + hasPlayFn + }) => __test(id, hasPlayFn), { id: \\"example-page--logged-in\\" }); + + if (global.__sbPostRender) { + await global.__sbPostRender(page, context); + } + + return result; }); }); });", @@ -148,6 +199,11 @@ Object { "example-page": "describe(\\"Example/Page\\", () => { describe(\\"Logged In\\", () => { it(\\"test\\", async () => { + const context = { + id: \\"example-page--logged-in\\", + title: \\"Example/Page\\", + name: \\"Logged In\\" + }; page.on('pageerror', err => { page.evaluate(({ id, @@ -157,11 +213,23 @@ Object { err: err.message }); }); - return page.evaluate(({ - id - }) => __test(id), { + + if (global.__sbPreRender) { + await global.__sbPreRender(page, context); + } + + const result = await page.evaluate(({ + id, + hasPlayFn + }) => __test(id, hasPlayFn), { id: \\"example-page--logged-in\\" }); + + if (global.__sbPostRender) { + await global.__sbPostRender(page, context); + } + + return result; }); }); });", diff --git a/src/typings.d.ts b/src/typings.d.ts new file mode 100644 index 00000000..2f4eb9cf --- /dev/null +++ b/src/typings.d.ts @@ -0,0 +1 @@ +declare module 'global'; diff --git a/test-runner-jest.config.js b/test-runner-jest.config.js index 01cf581d..e6c0eec8 100644 --- a/test-runner-jest.config.js +++ b/test-runner-jest.config.js @@ -17,4 +17,5 @@ module.exports = { globalSetup: './playwright/global-setup.js', globalTeardown: './playwright/global-teardown.js', testEnvironment: './playwright/custom-environment.js', + setupFilesAfterEnv: ['/jest-setup.js'], }; diff --git a/yarn.lock b/yarn.lock index e1d74d8b..2a70ffd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3858,6 +3858,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -4779,9 +4784,9 @@ camelcase@^6.2.0: integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001219: - version "1.0.30001234" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001234.tgz#8fc2e709e3b0679d7af7f073a1c661155c39b975" - integrity sha512-a3gjUVKkmwLdNysa1xkUAwN2VfJUJyVW47rsi3aCbkRCtbHAfo+rOsCqVw29G6coQ8gzAPb5XBXwiGHwme3isA== + version "1.0.30001306" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001306.tgz" + integrity sha512-Wd1OuggRzg1rbnM5hv1wXs2VkxJH/AA+LuudlIqvZiCvivF+wJJe2mgBZC8gPMgI7D76PP5CTx8Luvaqc1V6OQ== capture-exit@^2.0.0: version "2.0.0" @@ -4800,6 +4805,17 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + chalk@^2.0.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -6058,7 +6074,7 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -6816,6 +6832,11 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== +get-stdin@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" + integrity sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g= + get-stream@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" @@ -6919,7 +6940,7 @@ global-prefix@^0.1.4: global@^4.4.0: version "4.4.0" - resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" + resolved "https://registry.npmjs.org/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w== dependencies: min-document "^2.19.0" @@ -6987,6 +7008,11 @@ globby@^9.2.0: pify "^4.0.1" slash "^2.0.0" +glur@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/glur/-/glur-1.1.2.tgz#f20ea36db103bfc292343921f1f91e83c3467689" + integrity sha1-8g6jbbEDv8KSNDkh8fkeg8NGdok= + good-listener@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" @@ -7011,6 +7037,13 @@ handlebars@^4.7.7: optionalDependencies: uglify-js "^3.1.4" +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + has-bigints@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" @@ -8177,6 +8210,21 @@ jest-haste-map@^27.0.6: optionalDependencies: fsevents "^2.3.2" +jest-image-snapshot@^4.5.1: + version "4.5.1" + resolved "https://registry.npmjs.org/jest-image-snapshot/-/jest-image-snapshot-4.5.1.tgz#79fe0419c7729eb1be6c873365307a7b60f5cda0" + integrity sha512-0YkgupgkkCx0wIZkxvqs/oNiUT0X0d2WTpUhaAp+Dy6CpqBUZMRTIZo4KR1f+dqmx6WXrLCvecjnHLIsLkI+gQ== + dependencies: + chalk "^1.1.3" + get-stdin "^5.0.1" + glur "^1.1.2" + lodash "^4.17.4" + mkdirp "^0.5.1" + pixelmatch "^5.1.0" + pngjs "^3.4.0" + rimraf "^2.6.2" + ssim.js "^3.1.1" + jest-jasmine2@^27.0.6: version "27.0.6" resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.0.6.tgz#fd509a9ed3d92bd6edb68a779f4738b100655b37" @@ -8800,7 +8848,7 @@ lodash.uniq@4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.x, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: +lodash@4.x, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -9958,6 +10006,13 @@ pirates@^4.0.0, pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" +pixelmatch@^5.1.0: + version "5.2.1" + resolved "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.2.1.tgz#9e4e4f4aa59648208a31310306a5bed5522b0d65" + integrity sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ== + dependencies: + pngjs "^4.0.1" + pkg-conf@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/pkg-conf/-/pkg-conf-2.1.0.tgz#2126514ca6f2abfebd168596df18ba57867f0058" @@ -10038,6 +10093,16 @@ playwright@^1.14.0: dependencies: playwright-core "=1.17.1" +pngjs@^3.4.0: + version "3.4.0" + resolved "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" + integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== + +pngjs@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe" + integrity sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg== + pngjs@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" @@ -10949,7 +11014,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3: +rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -11461,6 +11526,11 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= +ssim.js@^3.1.1: + version "3.5.0" + resolved "https://registry.npmjs.org/ssim.js/-/ssim.js-3.5.0.tgz#d7276b9ee99b57a5ff0db34035f02f35197e62df" + integrity sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g== + ssri@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" @@ -11719,6 +11789,11 @@ style-to-object@0.3.0, style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"