diff --git a/.babelrc.js b/.babelrc.js
index 25da7069..612f2a39 100644
--- a/.babelrc.js
+++ b/.babelrc.js
@@ -1,4 +1,4 @@
-module.exports = {
+export default {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts
index 01ac6d02..4f8fe04d 100644
--- a/.storybook/test-runner.ts
+++ b/.storybook/test-runner.ts
@@ -1,7 +1,7 @@
import { toMatchImageSnapshot } from 'jest-image-snapshot';
-import { getStoryContext, waitForPageReady } from '../dist';
-import type { TestRunnerConfig } from '../dist';
+import { getStoryContext, waitForPageReady } from '../dist/index.js';
+import type { TestRunnerConfig } from '../dist/index.js';
const snapshotsDir = process.env.SNAPSHOTS_DIR || '__snapshots__';
const customSnapshotsDir = `${process.cwd()}/${snapshotsDir}`;
diff --git a/README.md b/README.md
index 9a8e301c..19696761 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,9 @@
Storybook test runner turns all of your stories into executable tests.
+> [!WARNING]
+> If you're using Storybook in a Vite-based project, you might want to use [Storybook's Vitest integration](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon?ref=test-runner-migration) instead. It's faster, provides features out of the box such as a11y and coverage, and integrates well with all Storybook's latest features.
+
Table of Contents
- [Features](#features)
@@ -44,8 +47,10 @@ Storybook test runner turns all of your stories into executable tests.
- [StorybookTestRunner user agent](#storybooktestrunner-user-agent)
- [Recipes](#recipes)
- [Preconfiguring viewport size](#preconfiguring-viewport-size)
- - [Accessibility testing](#accessibility-testing)
+ - [With Storybook 10](#with-storybook-10)
- [With Storybook 9](#with-storybook-9)
+ - [Accessibility testing](#accessibility-testing)
+ - [With Storybook 9](#with-storybook-9-1)
- [With Storybook 8](#with-storybook-8)
- [DOM snapshot (HTML)](#dom-snapshot-html)
- [Image snapshot](#image-snapshot)
@@ -93,7 +98,8 @@ Use the following table to use the correct version of this package, based on the
| Test runner version | Storybook version |
| ------------------- | ----------------- |
-| ^0.19.0 | ^8.2.0 |
+| ^0.24.0 | ^10.0.0 |
+| ^0.19.0 | ^8.2.0 or ^9.0.0 |
| ~0.17.0 | ^8.0.0 |
| ~0.16.0 | ^7.0.0 |
| ~0.9.4 | ^6.4.0 |
@@ -158,7 +164,7 @@ Usage: test-storybook [options]
| `--url` | Define the URL to run tests in. Useful for custom Storybook URLs `test-storybook --url http://the-storybook-url-here.com` |
| `--browsers` | Define browsers to run tests in. One or multiple of: chromium, firefox, webkit `test-storybook --browsers firefox chromium` |
| `--maxWorkers [amount]` | Specifies the maximum number of workers the worker-pool will spawn for running tests `test-storybook --maxWorkers=2` |
-| `--testTimeout [number]` | This option sets the default timeouts of test cases `test-storybook --testTimeout=15_000` |
+| `--testTimeout [number]` | This option sets the timeout of each test case `test-storybook --testTimeout=15000` |
| `--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` |
@@ -852,6 +858,39 @@ 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.
+#### With Storybook 10
+
+```ts
+import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner';
+import { MINIMAL_VIEWPORTS } from 'storybook/viewport';
+
+const DEFAULT_VIEWPORT_SIZE = { width: 1280, height: 720 };
+
+const config: TestRunnerConfig = {
+ async preVisit(page, story) {
+ const context = await getStoryContext(page, story);
+ const viewportName = context.storyGlobals?.viewport?.value;
+ const viewportParameter = MINIMAL_VIEWPORTS[viewportName];
+
+ if (viewportParameter) {
+ const viewportSize = Object.fromEntries(
+ Object.entries(viewportParameter.styles).map(([screen, size]) => [
+ screen,
+ Number.parseInt(size),
+ ])
+ );
+
+ page.setViewportSize(viewportSize);
+ } else {
+ page.setViewportSize(DEFAULT_VIEWPORT_SIZE);
+ }
+ },
+};
+export default config;
+```
+
+#### With Storybook 9
+
```ts
// .storybook/test-runner.ts
import { TestRunnerConfig, getStoryContext } from '@storybook/test-runner';
@@ -1073,10 +1112,10 @@ In either way, to fix it you should limit the amount of workers that run in para
}
```
-Another option is trying to increase the test timeout by passing the [--testTimeout](https://jestjs.io/docs/cli#--testtimeoutnumber) option to your command (adding `--testTimeout=60_000` will increase test timeouts to 1 minute):
+Another option is trying to increase the test timeout by passing the [--testTimeout](https://jestjs.io/docs/cli#--testtimeoutnumber) option to your command (adding `--testTimeout=60000` will increase test timeouts to 1 minute):
```json
-"test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn build-storybook --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && yarn test-storybook --maxWorkers=2 --testTimeout=60_000\""
+"test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn build-storybook --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && yarn test-storybook --maxWorkers=2 --testTimeout=60000\""
```
#### The test runner reports "No tests found" running on a Windows CI
diff --git a/jest-preset.json b/jest-preset.json
new file mode 100644
index 00000000..e691c540
--- /dev/null
+++ b/jest-preset.json
@@ -0,0 +1,10 @@
+{
+ "globalSetup": "@storybook/test-runner/dist/jest-playwright-entries/setup.js",
+ "globalTeardown": "@storybook/test-runner/dist/jest-playwright-entries/teardown.js",
+ "testEnvironment": "@storybook/test-runner/dist/jest-playwright-entries",
+ "runner": "@storybook/test-runner/dist/jest-playwright-entries/runner.js",
+ "setupFilesAfterEnv": [
+ "expect-playwright",
+ "@storybook/test-runner/dist/jest-playwright-entries/lib/extends.js"
+ ]
+}
\ No newline at end of file
diff --git a/jest.config.js b/jest.config.js
index fec48d9a..3f466bbc 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,11 +1,11 @@
-module.exports = {
+export default {
testMatch: ['**/*.test.ts'],
moduleNameMapper: {
- '@storybook/test-runner/playwright/global-setup': '/playwright/global-setup',
- '@storybook/test-runner/playwright/global-teardown': '/playwright/global-teardown',
+ '@storybook/test-runner/playwright/global-setup': '/playwright/global-setup.js',
+ '@storybook/test-runner/playwright/global-teardown': '/playwright/global-teardown.js',
'@storybook/test-runner/playwright/custom-environment':
- '/playwright/custom-environment',
- '@storybook/test-runner/playwright/jest-setup': '/playwright/jest-setup',
- '@storybook/test-runner/playwright/transform': '/playwright/transform',
+ '/playwright/custom-environment.js',
+ '@storybook/test-runner/playwright/jest-setup': '/playwright/jest-setup.js',
+ '@storybook/test-runner/playwright/transform': '/playwright/transform.js',
},
};
diff --git a/package.json b/package.json
index 2b7b4d85..6b4d16a1 100644
--- a/package.json
+++ b/package.json
@@ -16,8 +16,9 @@
},
"license": "MIT",
"author": "shilman",
+ "type": "module",
"main": "dist/index.js",
- "module": "dist/index.mjs",
+ "module": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"test-storybook": "./dist/test-storybook.js"
@@ -26,6 +27,7 @@
"dist",
"README.md",
"playwright",
+ "jest-preset.json",
"*.d.ts"
],
"scripts": {
@@ -37,7 +39,7 @@
"release": "yarn build && auto shipit",
"start": "concurrently \"yarn build:watch\" \"yarn storybook -- --quiet\"",
"storybook": "storybook dev -p 6006",
- "test": "jest",
+ "test": "vitest",
"test-storybook": "node dist/test-storybook",
"test-storybook:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn build-storybook --quiet && npx serve storybook-static -l 6006\" \"wait-on tcp:6006 && yarn test-storybook\"",
"test-storybook:ci-coverage": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn build-storybook --quiet && npx serve storybook-static -l 6006\" \"wait-on tcp:6006 && yarn test-storybook --coverage\"",
@@ -55,20 +57,23 @@
"@babel/generator": "^7.22.5",
"@babel/template": "^7.22.5",
"@babel/types": "^7.22.5",
- "@jest/types": "^29.6.3",
+ "@jest/types": "^30.0.1",
"@swc/core": "^1.5.22",
- "@swc/jest": "^0.2.23",
+ "@swc/jest": "^0.2.38",
"expect-playwright": "^0.8.0",
- "jest": "^29.6.4",
- "jest-circus": "^29.6.4",
- "jest-environment-node": "^29.6.4",
+ "jest": "^30.0.4",
+ "jest-circus": "^30.0.4",
+ "jest-environment-node": "^30.0.4",
"jest-junit": "^16.0.0",
- "jest-playwright-preset": "^4.0.0",
- "jest-runner": "^29.6.4",
+ "jest-process-manager": "^0.4.0",
+ "jest-runner": "^30.0.4",
"jest-serializer-html": "^7.1.0",
- "jest-watch-typeahead": "^2.0.0",
+ "jest-watch-typeahead": "^3.0.1",
"nyc": "^15.1.0",
- "playwright": "^1.14.0"
+ "playwright": "^1.14.0",
+ "playwright-core": ">=1.2.0",
+ "rimraf": "^3.0.2",
+ "uuid": "^8.3.2"
},
"devDependencies": {
"@auto-it/released": "^11.1.6",
@@ -76,16 +81,19 @@
"@babel/preset-env": "^7.19.4",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
- "@storybook/addon-a11y": "^9.0.0",
+ "@storybook/addon-a11y": "^10.0.0",
"@storybook/addon-coverage": "^1.0.0",
- "@storybook/addon-docs": "^9.0.0",
- "@storybook/react-vite": "^9.0.0",
- "@types/jest": "^29.0.0",
- "@types/node": "^16.4.1",
- "@types/node-fetch": "^2.6.11",
+ "@storybook/addon-docs": "^10.0.0",
+ "@storybook/react-vite": "^10.0.0",
+ "@types/jest": "^30.0.0",
+ "@types/node": "^24.0.10",
+ "@types/rimraf": "^3.0.2",
+ "@types/uuid": "^8.3.4",
+ "@typescript-eslint/eslint-plugin": "5.30.7",
+ "@typescript-eslint/parser": "5.30.7",
"@vitejs/plugin-react": "^4.0.3",
+ "@vitest/coverage-v8": "^3.2.4",
"auto": "^11.1.6",
- "babel-jest": "^29.0.0",
"babel-loader": "^8.1.0",
"babel-plugin-istanbul": "^6.1.1",
"can-bind-to-host": "^1.1.1",
@@ -93,26 +101,34 @@
"concurrently": "^7.0.0",
"glob": "^10.2.2",
"husky": "^8.0.0",
+ "jest": "^30.0.0",
+ "jest-circus": "^30.0.0",
+ "jest-environment-node": "^30.0.0",
"jest-image-snapshot": "^6.2.0",
+ "jest-runner": "^30.0.0",
"lint-staged": "^13.0.3",
- "node-fetch": "^2",
+ "pathe": "^2.0.3",
"pkg-up": "^5.0.0",
+ "playwright": ">=1.24.0",
+ "playwright-chromium": ">=1.24.0",
"prettier": "^2.8.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"read-pkg-up": "^7.0.1",
- "storybook": "^9.0.0",
+ "storybook": "^10.0.0",
"tempy": "^1.0.1",
"ts-dedent": "^2.0.0",
- "ts-jest": "^29.0.0",
- "tsup": "^6.5.0",
- "typescript": "~4.9.4",
- "vite": "^6.3.2",
+ "ts-jest": "^29.4.0",
+ "tsup": "^8.5.0",
+ "typescript": "^5.8.3",
+ "vite": "^7.0.5",
+ "vitest": "^3.2.4",
"wait-on": "^7.2.0"
},
"peerDependencies": {
- "storybook": "^0.0.0-0 || ^8.2.0 || ^9.0.0 || ^9.1.0-0 || ^9.2.0-0"
+ "storybook": "^0.0.0-0 || ^10.0.0 || ^10.0.0-0"
},
+ "packageManager": "yarn@4.5.1",
"engines": {
"node": ">=20.0.0"
},
@@ -136,6 +152,5 @@
"react-native"
],
"icon": "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png"
- },
- "packageManager": "yarn@4.5.1"
+ }
}
diff --git a/playwright/custom-environment.js b/playwright/custom-environment.js
index fcf6632d..993e8757 100644
--- a/playwright/custom-environment.js
+++ b/playwright/custom-environment.js
@@ -1,6 +1,5 @@
-const { setupPage } = require('../dist');
-
-const PlaywrightEnvironment = require('jest-playwright-preset/lib/PlaywrightEnvironment').default;
+import { setupPage } from '../dist/index.js';
+import PlaywrightEnvironment from '../dist/jest-playwright-entries/test-environment.js';
class CustomEnvironment extends PlaywrightEnvironment {
async setup() {
@@ -17,4 +16,4 @@ class CustomEnvironment extends PlaywrightEnvironment {
}
}
-module.exports = CustomEnvironment;
+export default CustomEnvironment;
diff --git a/playwright/global-setup.js b/playwright/global-setup.js
deleted file mode 100644
index 4c7686f5..00000000
--- a/playwright/global-setup.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// global-setup.js
-const { globalSetup: playwrightGlobalSetup } = require('jest-playwright-preset');
-
-module.exports = async function globalSetup(globalConfig) {
- return playwrightGlobalSetup(globalConfig);
-};
diff --git a/playwright/global-teardown.js b/playwright/global-teardown.js
deleted file mode 100644
index f0335486..00000000
--- a/playwright/global-teardown.js
+++ /dev/null
@@ -1,7 +0,0 @@
-// global-teardown.js
-const { globalTeardown: playwrightGlobalTeardown } = require('jest-playwright-preset');
-
-module.exports = async function globalTeardown(globalConfig) {
- // Your global teardown
- await playwrightGlobalTeardown(globalConfig);
-};
diff --git a/playwright/jest-setup.js b/playwright/jest-setup.js
index 7e319565..492767b2 100644
--- a/playwright/jest-setup.js
+++ b/playwright/jest-setup.js
@@ -1,6 +1,6 @@
-const { getTestRunnerConfig, setPreVisit, setPostVisit, setupPage } = require('../dist');
+import { getTestRunnerConfig, setPreVisit, setPostVisit, setupPage } from '../dist/index.js';
-const testRunnerConfig = getTestRunnerConfig(process.env.STORYBOOK_CONFIG_DIR);
+const testRunnerConfig = await getTestRunnerConfig(process.env.STORYBOOK_CONFIG_DIR);
if (testRunnerConfig) {
// hooks set up
if (testRunnerConfig.setup) {
diff --git a/playwright/test-runner-jest.config.js b/playwright/test-runner-jest.config.js
index 297a9fb1..fe7a5445 100644
--- a/playwright/test-runner-jest.config.js
+++ b/playwright/test-runner-jest.config.js
@@ -1,4 +1,4 @@
-const { getJestConfig } = require('../dist');
+import { getJestConfig } from '../dist/index.js';
// The default Jest configuration comes from @storybook/test-runner
const testRunnerConfig = getJestConfig();
@@ -6,7 +6,7 @@ const testRunnerConfig = getJestConfig();
/**
* @type {import('@jest/types').Config.InitialOptions}
*/
-module.exports = {
+export default {
...testRunnerConfig,
/** Add your own overrides below, and make sure
* to merge testRunnerConfig properties with your own
diff --git a/playwright/transform.js b/playwright/transform.js
index b2e6b2f6..80951a4b 100644
--- a/playwright/transform.js
+++ b/playwright/transform.js
@@ -1,17 +1,33 @@
-const { transformSync: swcTransform } = require('@swc/core');
-const { transformPlaywright } = require('../dist');
+import { transform as swcTransform } from '@swc/core';
+import { transformPlaywright } from '../dist/index.js';
-module.exports = {
- process(src, filename) {
- const csfTest = transformPlaywright(src, filename);
-
- const result = swcTransform(csfTest, {
+// Only export async version - force Jest to use it
+async function processAsync(src, filename) {
+ try {
+ const csfTest = await transformPlaywright(src, filename);
+ // This swc transform might not be needed
+ const result = await swcTransform(csfTest, {
filename,
+ isModule: true,
module: {
- type: 'commonjs',
+ type: 'es6',
+ },
+ jsc: {
+ parser: {
+ syntax: 'typescript',
+ tsx: true,
+ },
+ target: 'es2015',
},
});
return { code: result ? result.code : src };
- },
+ } catch (error) {
+ console.error('Transform error:', error);
+ return { code: src };
+ }
+}
+
+export default {
+ processAsync,
};
diff --git a/src/config/jest-playwright.test.ts b/src/config/jest-playwright.test.ts
deleted file mode 100644
index b382a6da..00000000
--- a/src/config/jest-playwright.test.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { getJestConfig } from './jest-playwright';
-import path from 'path';
-
-describe('getJestConfig', () => {
- it('returns the correct configuration 1', () => {
- const jestConfig = getJestConfig();
-
- expect(jestConfig).toEqual({
- rootDir: process.cwd(),
- reporters: ['default'],
- testMatch: [],
- transform: {
- '^.+\\.(story|stories)\\.[jt]sx?$': `${path.dirname(
- require.resolve('@storybook/test-runner/playwright/transform')
- )}/transform.js`,
- '^.+\\.[jt]sx?$': path.resolve('../test-runner/node_modules/@swc/jest'),
- },
- snapshotSerializers: [path.resolve('../test-runner/node_modules/jest-serializer-html')],
- testEnvironmentOptions: {
- 'jest-playwright': {
- browsers: undefined,
- collectCoverage: false,
- exitOnPageError: false,
- },
- },
- watchPlugins: [
- require.resolve('jest-watch-typeahead/filename'),
- require.resolve('jest-watch-typeahead/testname'),
- ],
- watchPathIgnorePatterns: ['coverage', '.nyc_output', '.cache'],
- roots: undefined,
- runner: path.resolve('../test-runner/node_modules/jest-playwright-preset/runner.js'),
- globalSetup: path.resolve('playwright/global-setup.js'),
- globalTeardown: path.resolve('playwright/global-teardown.js'),
- testEnvironment: path.resolve('playwright/custom-environment.js'),
- setupFilesAfterEnv: [
- path.resolve('playwright/jest-setup.js'),
- path.resolve('../test-runner/node_modules/expect-playwright/lib'),
- path.resolve('../test-runner/node_modules/jest-playwright-preset/lib/extends.js'),
- ],
- });
- });
-
- it('parses TEST_BROWSERS environment variable correctly', () => {
- interface JestPlaywrightOptions {
- browsers?: string[];
- collectCoverage?: boolean;
- }
- process.env.TEST_BROWSERS = 'chromium, firefox, webkit';
-
- const jestConfig: {
- testEnvironmentOptions?: {
- 'jest-playwright'?: JestPlaywrightOptions;
- };
- } = getJestConfig();
-
- expect(jestConfig.testEnvironmentOptions?.['jest-playwright']?.browsers as string[]).toEqual([
- 'chromium',
- 'firefox',
- 'webkit',
- ]);
- });
-
- it('sets TEST_MATCH environment variable correctly', () => {
- process.env.TEST_MATCH = '**/*.test.js';
-
- const jestConfig = getJestConfig();
-
- expect(jestConfig.testMatch).toEqual(['**/*.test.js']);
- });
-
- it('returns the correct configuration 2', () => {
- process.env.STORYBOOK_JUNIT = 'true';
-
- const jestConfig = getJestConfig();
-
- expect(jestConfig.reporters).toEqual(['default', path.dirname(require.resolve('jest-junit'))]);
- expect(jestConfig).toMatchObject({
- rootDir: process.cwd(),
- roots: undefined,
- testMatch: ['**/*.test.js'],
- transform: {
- '^.+\\.(story|stories)\\.[jt]sx?$': `${path.dirname(
- require.resolve('@storybook/test-runner/playwright/transform')
- )}/transform.js`,
- '^.+\\.[jt]sx?$': path.dirname(require.resolve('@swc/jest')),
- },
- snapshotSerializers: [path.dirname(require.resolve('jest-serializer-html'))],
- testEnvironmentOptions: {
- 'jest-playwright': {
- browsers: ['chromium', 'firefox', 'webkit'],
- collectCoverage: false,
- },
- },
- watchPlugins: [
- require.resolve('jest-watch-typeahead/filename'),
- require.resolve('jest-watch-typeahead/testname'),
- ],
- watchPathIgnorePatterns: ['coverage', '.nyc_output', '.cache'],
- });
- });
-
- it('returns the correct configuration 3', () => {
- process.env.TEST_ROOT = 'test';
- process.env.STORYBOOK_STORIES_PATTERN = '**/*.stories.tsx';
-
- const jestConfig = getJestConfig();
-
- expect(jestConfig).toMatchObject({
- roots: ['test'],
- reporters: ['default', path.resolve('../test-runner/node_modules/jest-junit')],
- testMatch: ['**/*.test.js'],
- });
- });
-});
diff --git a/src/config/jest-playwright.ts b/src/config/jest-playwright.ts
index 4fab6d3f..d07215bb 100644
--- a/src/config/jest-playwright.ts
+++ b/src/config/jest-playwright.ts
@@ -1,42 +1,36 @@
-import path from 'path';
+import path from 'pathe';
import { getProjectRoot } from 'storybook/internal/common';
import type { Config } from '@jest/types';
-const getTestRunnerPath = () => process.env.STORYBOOK_TEST_RUNNER_PATH ?? '@storybook/test-runner';
+const getTestRunnerPath = () => {
+ return (
+ process.env.STORYBOOK_TEST_RUNNER_PATH ??
+ path.dirname(require.resolve('@storybook/test-runner/package.json'))
+ );
+};
/**
* IMPORTANT NOTE:
* Depending on the user's project and package manager, it's possible that jest-playwright-preset
* is going to be located in @storybook/test-runner/node_modules OR in the root node_modules
*
- * By setting `preset: 'jest-playwright-preset` the change of resolution issues is higher, because
+ * By setting `preset: 'jest-playwright-preset' the change of resolution issues is higher, because
* the lib might be installed inside of @storybook/test-runner/node_modules but references as if it was
* in the root node_modules.
*
- * This function does the same thing as `preset: 'jest-playwright-preset` but makes sure that the
+ * This function does the same thing as `preset: 'jest-playwright-preset' but makes sure that the
* necessary moving parts are all required within the correct path.
* */
const getJestPlaywrightConfig = (): Config.InitialOptions => {
- const TEST_RUNNER_PATH = getTestRunnerPath();
- const presetBasePath = path.dirname(
- require.resolve('jest-playwright-preset', {
- paths: [path.join(__dirname, '../node_modules')],
- })
- );
- const expectPlaywrightPath = path.dirname(
- require.resolve('expect-playwright', {
- paths: [path.join(__dirname, '../node_modules')],
- })
- );
+ const resolvedRunnerPath = getTestRunnerPath();
+ const expectPlaywrightPath = path.dirname(require.resolve('expect-playwright'));
return {
- runner: path.join(presetBasePath, 'runner.js'),
- globalSetup: require.resolve(`${TEST_RUNNER_PATH}/playwright/global-setup.js`),
- globalTeardown: require.resolve(`${TEST_RUNNER_PATH}/playwright/global-teardown.js`),
- testEnvironment: require.resolve(`${TEST_RUNNER_PATH}/playwright/custom-environment.js`),
+ runner: path.join(resolvedRunnerPath, 'dist/jest-playwright-entries/runner.js'),
+ testEnvironment: path.join(resolvedRunnerPath, 'playwright/custom-environment.js'),
setupFilesAfterEnv: [
- require.resolve(`${TEST_RUNNER_PATH}/playwright/jest-setup.js`),
+ path.join(resolvedRunnerPath, 'playwright/jest-setup.js'),
expectPlaywrightPath,
- path.join(presetBasePath, 'lib', 'extends.js'),
+ path.join(resolvedRunnerPath, 'dist/jest-playwright-entries/extends.js'),
],
};
};
@@ -51,23 +45,11 @@ export const getJestConfig = (): Config.InitialOptions => {
STORYBOOK_JUNIT,
} = process.env;
- const jestJunitPath = path.dirname(
- require.resolve('jest-junit', {
- paths: [path.join(__dirname, '../node_modules')],
- })
- );
+ const jestJunitPath = path.dirname(require.resolve('jest-junit'));
- const jestSerializerHtmlPath = path.dirname(
- require.resolve('jest-serializer-html', {
- paths: [path.join(__dirname, '../node_modules')],
- })
- );
+ const jestSerializerHtmlPath = path.dirname(require.resolve('jest-serializer-html'));
- const swcJestPath = path.dirname(
- require.resolve('@swc/jest', {
- paths: [path.join(__dirname, '../node_modules')],
- })
- );
+ const swcJestPath = path.dirname(require.resolve('@swc/jest'));
const reporters = STORYBOOK_JUNIT ? ['default', jestJunitPath] : ['default'];
@@ -81,10 +63,11 @@ export const getJestConfig = (): Config.InitialOptions => {
testMatch,
transform: {
'^.+\\.(story|stories)\\.[jt]sx?$': require.resolve(
- `${TEST_RUNNER_PATH}/playwright/transform`
+ `${TEST_RUNNER_PATH}/playwright/transform.js`
),
'^.+\\.[jt]sx?$': swcJestPath,
},
+ extensionsToTreatAsEsm: ['.jsx', '.ts', '.tsx'],
snapshotSerializers: [jestSerializerHtmlPath],
testEnvironmentOptions: {
'jest-playwright': {
diff --git a/src/csf/__snapshots__/transformCsf.test.ts.snap b/src/csf/__snapshots__/transformCsf.test.ts.snap
index 7fff8827..4e81dc86 100644
--- a/src/csf/__snapshots__/transformCsf.test.ts.snap
+++ b/src/csf/__snapshots__/transformCsf.test.ts.snap
@@ -1,6 +1,6 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`transformCsf calls the beforeEachPrefixer function once 1`] = `
+exports[`transformCsf > calls the beforeEachPrefixer function once 1`] = `
"
export default {
title: 'Button',
@@ -16,73 +16,71 @@ exports[`transformCsf calls the beforeEachPrefixer function once 1`] = `
export const Primary = () => '';
-if (!require.main) {
- describe("Button", () => {
- describe("Primary", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "button--primary",
- title: "Button",
- name: "Primary"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "button--primary"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
- }
- return result;
+describe("Button", () => {
+ describe("Primary", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "button--primary",
+ title: "Button",
+ name: "Primary"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "button--primary"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Button"}/\${"Primary"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+The code in this story is not instrumented, which means the coverage setup is likely not correct.
+More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
+ }
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Button"}/\${"Primary"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
});
});
-}"
+});"
`;
-exports[`transformCsf calls the testPrefixer function for each test 1`] = `
+exports[`transformCsf > calls the testPrefixer function for each test 1`] = `
"
export default {
title: 'Button',
@@ -98,141 +96,137 @@ exports[`transformCsf calls the testPrefixer function for each test 1`] = `
export const Primary = () => '';
-if (!require.main) {
- describe("Button", () => {
- describe("Primary", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "button--primary",
- title: "Button",
- name: "Primary"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "button--primary"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
- }
- return result;
+describe("Button", () => {
+ describe("Primary", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "button--primary",
+ title: "Button",
+ name: "Primary"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "button--primary"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Button"}/\${"Primary"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+The code in this story is not instrumented, which means the coverage setup is likely not correct.
+More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
+ }
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Button"}/\${"Primary"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
});
});
-}"
+});"
`;
-exports[`transformCsf clears the body if clearBody option is true 1`] = `
+exports[`transformCsf > clears the body if clearBody option is true 1`] = `
"
-if (!require.main) {
- describe("Button", () => {
- describe("Primary", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "button--primary",
- title: "Button",
- name: "Primary"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "button--primary"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
- }
- return result;
+describe("Button", () => {
+ describe("Primary", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "button--primary",
+ title: "Button",
+ name: "Primary"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "button--primary"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Button"}/\${"Primary"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+The code in this story is not instrumented, which means the coverage setup is likely not correct.
+More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
+ }
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Button"}/\${"Primary"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
});
});
-}"
+});"
`;
-exports[`transformCsf executes beforeEach code before each test 1`] = `
+exports[`transformCsf > executes beforeEach code before each test 1`] = `
"
export default {
title: 'Button',
@@ -248,19 +242,17 @@ exports[`transformCsf executes beforeEach code before each test 1`] = `
export const Primary = () => '';
-if (!require.main) {
- describe("Button", () => {
- beforeEach(beforeEach(() => {
- console.log("beforeEach called");
- }));
- describe("Primary", () => {
- it("smoke-test", async () => {});
- });
+describe("Button", () => {
+ beforeEach(beforeEach(() => {
+ console.log("beforeEach called");
+ }));
+ describe("Primary", () => {
+ it("smoke-test", async () => {});
});
-}"
+});"
`;
-exports[`transformCsf returns empty result if there are no stories 1`] = `
+exports[`transformCsf > returns empty result if there are no stories 1`] = `
"
export default {
title: 'Button',
diff --git a/src/csf/transformCsf.test.ts b/src/csf/transformCsf.test.ts
index f48379d3..8c3b3fe4 100644
--- a/src/csf/transformCsf.test.ts
+++ b/src/csf/transformCsf.test.ts
@@ -1,9 +1,10 @@
import { TestPrefixer, TransformOptions, transformCsf } from './transformCsf';
import { testPrefixer } from '../playwright/transformPlaywright';
import template from '@babel/template';
+import { describe, it, expect } from 'vitest';
-describe('transformCsf', () => {
- it('inserts a no-op test if there are no stories', () => {
+describe('transformCsf', async () => {
+ it('inserts a no-op test if there are no stories', async () => {
const csfCode = `
export default {
title: 'Button',
@@ -11,24 +12,24 @@ describe('transformCsf', () => {
`;
const expectedCode = `describe.skip('Button', () => { it('no-op', () => {}) });`;
- const result = transformCsf(csfCode, { insertTestIfEmpty: true } as TransformOptions);
+ const result = await transformCsf(csfCode, { insertTestIfEmpty: true } as TransformOptions);
expect(result).toEqual(expectedCode);
});
- it('returns empty result if there are no stories', () => {
+ it('returns empty result if there are no stories', async () => {
const csfCode = `
export default {
title: 'Button',
};
`;
- const result = transformCsf(csfCode, { testPrefixer });
+ const result = await transformCsf(csfCode, { testPrefixer });
expect(result).toMatchSnapshot();
});
- it('calls the testPrefixer function for each test', () => {
+ it('calls the testPrefixer function for each test', async () => {
const csfCode = `
export default {
title: 'Button',
@@ -44,12 +45,12 @@ describe('transformCsf', () => {
export const Primary = () => '';
`;
- const result = transformCsf(csfCode, { testPrefixer });
+ const result = await transformCsf(csfCode, { testPrefixer });
expect(result).toMatchSnapshot();
});
- it('calls the beforeEachPrefixer function once', () => {
+ it('calls the beforeEachPrefixer function once', async () => {
const csfCode = `
export default {
title: 'Button',
@@ -64,12 +65,12 @@ describe('transformCsf', () => {
};
export const Primary = () => '';
`;
- const result = transformCsf(csfCode, { testPrefixer, beforeEachPrefixer: undefined });
+ const result = await transformCsf(csfCode, { testPrefixer, beforeEachPrefixer: undefined });
expect(result).toMatchSnapshot();
});
- it('clears the body if clearBody option is true', () => {
+ it('clears the body if clearBody option is true', async () => {
const csfCode = `
export default {
title: 'Button',
@@ -85,12 +86,12 @@ describe('transformCsf', () => {
export const Primary = () => '';
`;
- const result = transformCsf(csfCode, { testPrefixer, clearBody: true });
+ const result = await transformCsf(csfCode, { testPrefixer, clearBody: true });
expect(result).toMatchSnapshot();
});
- it('executes beforeEach code before each test', () => {
+ it('executes beforeEach code before each test', async () => {
const code = `
export default {
title: 'Button',
@@ -114,7 +115,10 @@ describe('transformCsf', () => {
console.log({ id: %%id%%, title: %%title%%, name: %%name%%, storyExport: %%storyExport%% });
async () => {}`) as unknown as TestPrefixer;
- const result = transformCsf(code, { beforeEachPrefixer, testPrefixer } as TransformOptions);
+ const result = await transformCsf(code, {
+ beforeEachPrefixer,
+ testPrefixer,
+ } as TransformOptions);
expect(result).toMatchSnapshot();
});
diff --git a/src/csf/transformCsf.ts b/src/csf/transformCsf.ts
index c004a16e..bb11c7b9 100644
--- a/src/csf/transformCsf.ts
+++ b/src/csf/transformCsf.ts
@@ -2,11 +2,14 @@
import { toId, storyNameFromExport, combineTags } from 'storybook/internal/csf';
import { loadCsf } from 'storybook/internal/csf-tools';
import * as t from '@babel/types';
-import generate from '@babel/generator';
+import babelGenerate from '@babel/generator';
import dedent from 'ts-dedent';
import { getTagOptions } from '../util/getTagOptions';
+// Handle both ESM and CJS patterns
+const generate = (babelGenerate as any).default ?? babelGenerate;
+
export interface TestContext {
storyExport?: t.Identifier;
name: t.Literal;
@@ -100,7 +103,7 @@ const makeBeforeEach = (beforeEachPrefixer: FilePrefixer) => {
const makeArray = (templateResult: TemplateResult) =>
Array.isArray(templateResult) ? templateResult : [templateResult];
-export const transformCsf = (
+export const transformCsf = async (
code: string,
{
clearBody = false,
@@ -111,7 +114,7 @@ export const transformCsf = (
previewAnnotations = { tags: [] },
}: TransformOptions & { previewAnnotations?: Record }
) => {
- const { includeTags, excludeTags, skipTags } = getTagOptions();
+ const { includeTags, excludeTags, skipTags } = await getTagOptions();
const csf = loadCsf(code, { makeTitle: makeTitle ?? ((userTitle: string) => userTitle) });
csf.parse();
@@ -124,8 +127,7 @@ export const transformCsf = (
const annotations = csf._storyAnnotations[key];
acc[key] = {};
if (annotations?.play) {
- // @ts-expect-error type mismatch – check later
- acc[key].play = annotations.play;
+ acc[key].play = annotations.play as t.Node;
}
acc[key].tags = combineTags(
@@ -188,9 +190,7 @@ export const transformCsf = (
const { code: describeCode } = generate(describe, {});
result = dedent`
${result}
- if (!require.main) {
- ${describeCode}
- }
+ ${describeCode}
`;
} else if (insertTestIfEmpty) {
// When there are no tests at all, we skip. The reason is that the file already went through Jest's transformation,
diff --git a/src/jest-playwright-entries/extends.ts b/src/jest-playwright-entries/extends.ts
new file mode 100644
index 00000000..4784529b
--- /dev/null
+++ b/src/jest-playwright-entries/extends.ts
@@ -0,0 +1,117 @@
+/* global jestPlaywright, browserName, deviceName */
+/* eslint-disable @typescript-eslint/no-explicit-any*/
+import { getSkipFlag, deepMerge } from '../jest-playwright-preset/utils';
+import {
+ JestPlaywrightGlobal,
+ SkipOption,
+ TestPlaywrightConfigOptions,
+} from '../jest-playwright-preset/types';
+import { CONFIG_ENVIRONMENT_NAME, DEBUG_TIMEOUT } from '../jest-playwright-preset/constants';
+
+declare const global: JestPlaywrightGlobal;
+
+type TestType = 'it' | 'describe';
+
+const DEBUG_OPTIONS = {
+ launchOptions: {
+ headless: false,
+ devtools: true,
+ },
+};
+
+const runDebugTest = (jestTestType: jest.It, ...args: any[]) => {
+ const isConfigProvided = typeof args[0] === 'object';
+ const lastArg = args[args.length - 1];
+ const timer = typeof lastArg === 'number' ? lastArg : DEBUG_TIMEOUT;
+ // TODO Looks weird - need to be rewritten
+ let options = DEBUG_OPTIONS as TestPlaywrightConfigOptions;
+ if (isConfigProvided) {
+ options = deepMerge(DEBUG_OPTIONS, args[0]);
+ }
+
+ jestTestType(
+ args[isConfigProvided ? 1 : 0],
+ async () => {
+ const envArgs = await jestPlaywright.configSeparateEnv(options, true);
+ try {
+ await args[isConfigProvided ? 2 : 1](envArgs);
+ } finally {
+ await envArgs.browser!.close();
+ }
+ },
+ timer
+ );
+};
+
+// @ts-ignore
+it.jestPlaywrightDebug = (...args) => {
+ runDebugTest(it, ...args);
+};
+
+it.jestPlaywrightDebug.only = (...args: any[]) => {
+ runDebugTest(it.only, ...args);
+};
+
+it.jestPlaywrightDebug.skip = (...args: any[]) => {
+ runDebugTest(it.skip, ...args);
+};
+
+const runConfigTest = (
+ jestTypeTest: jest.It,
+ playwrightOptions: Partial,
+ ...args: any[]
+) => {
+ const lastArg = args[args.length - 1];
+ const timer = typeof lastArg === 'number' ? lastArg : global[CONFIG_ENVIRONMENT_NAME].testTimeout;
+ jestTypeTest(
+ args[0],
+ async () => {
+ const envArgs = await jestPlaywright.configSeparateEnv(playwrightOptions);
+ try {
+ await args[1](envArgs);
+ } finally {
+ await envArgs.browser!.close();
+ }
+ },
+ timer
+ );
+};
+
+//@ts-ignore
+it.jestPlaywrightConfig = (playwrightOptions, ...args) => {
+ runConfigTest(it, playwrightOptions, ...args);
+};
+
+it.jestPlaywrightConfig.only = (...args) => {
+ runConfigTest(it.only, ...args);
+};
+
+it.jestPlaywrightConfig.skip = (...args) => {
+ runConfigTest(it.skip, ...args);
+};
+
+const customSkip = (skipOption: SkipOption, type: TestType, ...args: any[]) => {
+ const skipFlag = getSkipFlag(skipOption, browserName, deviceName);
+ if (skipFlag) {
+ // @ts-ignore
+ global[type].skip(...args);
+ } else {
+ // @ts-ignore
+ global[type](...args);
+ }
+};
+
+it.jestPlaywrightSkip = (skipOption, ...args) => {
+ customSkip(skipOption, 'it', ...args);
+};
+
+//@ts-ignore
+describe.jestPlaywrightSkip = (skipOption: SkipOption, ...args) => {
+ customSkip(skipOption, 'describe', ...args);
+};
+
+beforeEach(async () => {
+ if (global[CONFIG_ENVIRONMENT_NAME].resetContextPerTest) {
+ await jestPlaywright.resetContext();
+ }
+});
diff --git a/src/jest-playwright-entries/runner.ts b/src/jest-playwright-entries/runner.ts
new file mode 100644
index 00000000..755ab00d
--- /dev/null
+++ b/src/jest-playwright-entries/runner.ts
@@ -0,0 +1,3 @@
+import PlaywrightRunner from '../jest-playwright-preset/PlaywrightRunner';
+
+export default PlaywrightRunner;
diff --git a/src/jest-playwright-entries/setup.ts b/src/jest-playwright-entries/setup.ts
new file mode 100644
index 00000000..57dfb02f
--- /dev/null
+++ b/src/jest-playwright-entries/setup.ts
@@ -0,0 +1,3 @@
+import { setup } from '../jest-playwright-preset/global';
+
+export default setup;
diff --git a/src/jest-playwright-entries/teardown.ts b/src/jest-playwright-entries/teardown.ts
new file mode 100644
index 00000000..f9b03eb0
--- /dev/null
+++ b/src/jest-playwright-entries/teardown.ts
@@ -0,0 +1,3 @@
+import { teardown } from '../jest-playwright-preset/global';
+
+export default teardown;
diff --git a/src/jest-playwright-entries/test-environment.ts b/src/jest-playwright-entries/test-environment.ts
new file mode 100644
index 00000000..9662a012
--- /dev/null
+++ b/src/jest-playwright-entries/test-environment.ts
@@ -0,0 +1,3 @@
+import PlaywrightEnvironment from '../jest-playwright-preset/PlaywrightEnvironment';
+
+export default PlaywrightEnvironment;
diff --git a/src/jest-playwright-preset/PlaywrightEnvironment.ts b/src/jest-playwright-preset/PlaywrightEnvironment.ts
new file mode 100644
index 00000000..fefe5627
--- /dev/null
+++ b/src/jest-playwright-preset/PlaywrightEnvironment.ts
@@ -0,0 +1,356 @@
+/* eslint-disable no-console, @typescript-eslint/no-unused-vars */
+import type { Browser, BrowserContext, BrowserContextOptions, Page } from 'playwright-core';
+import { Event } from 'jest-circus';
+import type { JestEnvironmentConfig } from '@jest/environment';
+import type {
+ BrowserType,
+ ConfigDeviceType,
+ ConfigParams,
+ GenericBrowser,
+ JestPlaywrightConfig,
+ JestPlaywrightProjectConfig,
+ Nullable,
+ Playwright,
+ TestPlaywrightConfigOptions,
+} from './types';
+import {
+ CHROMIUM,
+ CONFIG_ENVIRONMENT_NAME,
+ DEFAULT_CONFIG,
+ FIREFOX,
+ IMPORT_KIND_PLAYWRIGHT,
+ PERSISTENT,
+ LAUNCH,
+} from './constants';
+import {
+ checkDevice,
+ deepMerge,
+ formatError,
+ getBrowserOptions,
+ getBrowserType,
+ getDeviceBrowserType,
+ getPlaywrightInstance,
+} from './utils';
+import { saveCoverageOnPage, saveCoverageToFile } from './coverage';
+
+const handleError = (error: Error): void => {
+ process.emit('uncaughtException', error);
+};
+
+const getBrowserPerProcess = async (
+ playwrightInstance: GenericBrowser,
+ browserType: BrowserType,
+ config: JestPlaywrightConfig
+): Promise => {
+ const { launchType, userDataDir, launchOptions, connectOptions } = config;
+
+ if (launchType === LAUNCH || launchType === PERSISTENT) {
+ // https://github.com/playwright-community/jest-playwright/issues/42#issuecomment-589170220
+ if (browserType !== CHROMIUM && launchOptions?.args) {
+ launchOptions.args = launchOptions.args.filter((item: string) => item !== '--no-sandbox');
+ }
+
+ const options = getBrowserOptions(browserType, launchOptions);
+
+ if (launchType === LAUNCH) {
+ return playwrightInstance.launch(options);
+ }
+
+ if (launchType === PERSISTENT) {
+ return playwrightInstance.launchPersistentContext(userDataDir!, options);
+ }
+ }
+
+ const options = getBrowserOptions(browserType, connectOptions);
+ return options && 'endpointURL' in options
+ ? playwrightInstance.connectOverCDP(options)
+ : playwrightInstance.connect(options);
+};
+
+const getDeviceConfig = (
+ device: Nullable | undefined,
+ availableDevices: Playwright['devices']
+): BrowserContextOptions => {
+ if (device) {
+ if (typeof device === 'string') {
+ const { defaultBrowserType, ...deviceProps } = availableDevices[device];
+ return deviceProps;
+ } else {
+ const { name, defaultBrowserType, ...deviceProps } = device;
+ return deviceProps;
+ }
+ }
+ return {};
+};
+
+const getDeviceName = (device: Nullable): Nullable => {
+ let deviceName: Nullable = null;
+ if (device != null) {
+ if (typeof device === 'string') {
+ deviceName = device;
+ } else {
+ deviceName = device.name;
+ }
+ }
+ return deviceName;
+};
+
+export const getPlaywrightEnv = (): unknown => {
+ const RootEnv = require('jest-environment-node').default;
+
+ return class PlaywrightEnvironment extends RootEnv {
+ readonly _config: JestPlaywrightProjectConfig;
+ _jestPlaywrightConfig!: JestPlaywrightConfig;
+
+ constructor(config: JestEnvironmentConfig) {
+ super(config);
+ this._config = config.projectConfig as JestPlaywrightProjectConfig;
+ }
+
+ _getContextOptions(devices: Playwright['devices']): BrowserContextOptions {
+ const { browserName, device } = this._config;
+ const browserType = getBrowserType(browserName);
+ const { contextOptions } = this._jestPlaywrightConfig;
+ const deviceBrowserContextOptions = getDeviceConfig(device, devices);
+ const resultContextOptions = deepMerge(
+ deviceBrowserContextOptions,
+ getBrowserOptions(browserName, contextOptions)
+ );
+ if (browserType === FIREFOX && resultContextOptions.isMobile) {
+ console.warn(formatError(`isMobile is not supported in ${FIREFOX}.`));
+ delete resultContextOptions.isMobile;
+ }
+ return resultContextOptions;
+ }
+
+ _getSeparateEnvBrowserConfig(
+ isDebug: boolean,
+ config: TestPlaywrightConfigOptions
+ ): JestPlaywrightConfig {
+ const { debugOptions } = this._jestPlaywrightConfig;
+ const defaultBrowserConfig: JestPlaywrightConfig = {
+ ...DEFAULT_CONFIG,
+ launchType: LAUNCH,
+ };
+ let resultBrowserConfig: JestPlaywrightConfig = {
+ ...defaultBrowserConfig,
+ ...config,
+ };
+ if (isDebug) {
+ if (debugOptions) {
+ resultBrowserConfig = deepMerge(resultBrowserConfig, debugOptions);
+ }
+ } else {
+ resultBrowserConfig = deepMerge(this._jestPlaywrightConfig, resultBrowserConfig);
+ }
+ return resultBrowserConfig;
+ }
+
+ _getSeparateEnvContextConfig(
+ isDebug: boolean,
+ config: TestPlaywrightConfigOptions,
+ browserName: BrowserType,
+ devices: Playwright['devices']
+ ): BrowserContextOptions {
+ const { device, contextOptions } = config;
+ const { debugOptions } = this._jestPlaywrightConfig;
+ const deviceContextOptions: BrowserContextOptions = getDeviceConfig(device, devices);
+ let resultContextOptions: BrowserContextOptions = contextOptions || {};
+ if (isDebug) {
+ if (debugOptions?.contextOptions) {
+ resultContextOptions = deepMerge(resultContextOptions, debugOptions.contextOptions!);
+ }
+ } else {
+ resultContextOptions = deepMerge(
+ this._jestPlaywrightConfig.contextOptions!,
+ resultContextOptions
+ );
+ }
+ resultContextOptions = deepMerge(deviceContextOptions, resultContextOptions);
+ return getBrowserOptions(browserName, resultContextOptions);
+ }
+
+ async _setNewPageInstance(context = this.global.context) {
+ const { exitOnPageError } = this._jestPlaywrightConfig;
+ const page = await context.newPage();
+ if (exitOnPageError) {
+ page.on('pageerror', handleError);
+ }
+ return page;
+ }
+
+ async _setCollectCoverage(context: BrowserContext) {
+ await context.exposeFunction('reportCodeCoverage', (coverage: unknown) => {
+ if (coverage) saveCoverageToFile(coverage);
+ });
+ await context.addInitScript(() =>
+ window.addEventListener('beforeunload', () => {
+ // @ts-ignore
+ reportCodeCoverage(window.__coverage__);
+ })
+ );
+ }
+
+ async setup(): Promise {
+ const { wsEndpoint, browserName, testEnvironmentOptions } = this._config;
+ this._jestPlaywrightConfig = testEnvironmentOptions[
+ CONFIG_ENVIRONMENT_NAME
+ ] as JestPlaywrightConfig;
+ const { connectOptions, collectCoverage, selectors, launchType, skipInitialization } =
+ this._jestPlaywrightConfig;
+ if (wsEndpoint) {
+ this._jestPlaywrightConfig.connectOptions = {
+ ...connectOptions,
+ wsEndpoint,
+ };
+ }
+ const browserType = getBrowserType(browserName);
+ const device = this._config.device;
+ const deviceName: Nullable = getDeviceName(device);
+ const { name, instance: playwrightInstance, devices } = getPlaywrightInstance(browserType);
+ const contextOptions = this._getContextOptions(devices);
+
+ if (name === IMPORT_KIND_PLAYWRIGHT && selectors) {
+ const playwright = require('playwright');
+ await Promise.all(
+ selectors.map(({ name, script }) =>
+ playwright.selectors.register(name, script).catch((e: Error): void => {
+ if (!e.toString().includes('has been already')) {
+ throw e;
+ }
+ })
+ )
+ );
+ }
+
+ this.global.browserName = browserType;
+ this.global.deviceName = deviceName;
+ if (!skipInitialization) {
+ const browserOrContext = await getBrowserPerProcess(
+ playwrightInstance as GenericBrowser,
+ browserType,
+ this._jestPlaywrightConfig
+ );
+ this.global.browser = launchType === PERSISTENT ? null : browserOrContext;
+ this.global.context =
+ launchType === PERSISTENT
+ ? browserOrContext
+ : await this.global.browser.newContext(contextOptions);
+ if (collectCoverage) {
+ await this._setCollectCoverage(this.global.context as BrowserContext);
+ }
+ this.global.page = await this._setNewPageInstance();
+ }
+ this.global.jestPlaywright = {
+ configSeparateEnv: async (
+ config: TestPlaywrightConfigOptions,
+ isDebug = false
+ ): Promise => {
+ const { device } = config;
+ const browserName =
+ config.useDefaultBrowserType && device
+ ? getDeviceBrowserType(device, devices) || CHROMIUM
+ : config.browser || browserType;
+ const deviceName = device ? getDeviceName(device) : null;
+ checkDevice(deviceName, devices);
+ const resultBrowserConfig: JestPlaywrightConfig = this._getSeparateEnvBrowserConfig(
+ isDebug,
+ config
+ );
+ const resultContextOptions: BrowserContextOptions = this._getSeparateEnvContextConfig(
+ isDebug,
+ config,
+ browserName,
+ devices
+ );
+ const { instance } = getPlaywrightInstance(browserName);
+ const browser = await getBrowserPerProcess(
+ instance as GenericBrowser,
+ browserName,
+ resultBrowserConfig
+ );
+ const context = await (browser as Browser)!.newContext(resultContextOptions);
+ const page = await context!.newPage();
+ return { browserName, deviceName, browser, context, page };
+ },
+ resetPage: async (): Promise => {
+ await this.global.page?.close();
+ this.global.page = await this._setNewPageInstance();
+ },
+ resetContext: async (newOptions?: BrowserContextOptions): Promise => {
+ const { browser, context } = this.global;
+ await context?.close();
+
+ const newContextOptions = newOptions
+ ? deepMerge(contextOptions, newOptions)
+ : contextOptions;
+
+ this.global.context = await browser.newContext(newContextOptions);
+ this.global.page = await this._setNewPageInstance();
+ },
+ resetBrowser: async (newOptions?: BrowserContextOptions): Promise => {
+ const { browser } = this.global;
+ await browser?.close();
+
+ this.global.browser = await getBrowserPerProcess(
+ playwrightInstance as GenericBrowser,
+ browserType,
+ this._jestPlaywrightConfig
+ );
+
+ const newContextOptions = newOptions
+ ? deepMerge(contextOptions, newOptions)
+ : contextOptions;
+
+ this.global.context = await this.global.browser.newContext(newContextOptions);
+ this.global.page = await this._setNewPageInstance();
+ },
+ saveCoverage: async (page: Page): Promise =>
+ saveCoverageOnPage(page, collectCoverage),
+ };
+ }
+
+ async handleTestEvent(event: Event) {
+ const { browserName } = this._config;
+ const { collectCoverage, haveSkippedTests } = this._jestPlaywrightConfig;
+ const browserType = getBrowserType(browserName);
+ const { instance, devices } = getPlaywrightInstance(browserType);
+ const contextOptions = this._getContextOptions(devices);
+ if (haveSkippedTests && event.name === 'run_start') {
+ this.global.browser = await getBrowserPerProcess(
+ instance as GenericBrowser,
+ browserType,
+ this._jestPlaywrightConfig
+ );
+ this.global.context = await this.global.browser.newContext(contextOptions);
+ if (collectCoverage) {
+ await this._setCollectCoverage(this.global.context);
+ }
+ this.global.page = await this._setNewPageInstance();
+ }
+ }
+
+ async teardown(): Promise {
+ const { browser, context, page } = this.global;
+ const { collectCoverage } = this._jestPlaywrightConfig;
+ page?.removeListener('pageerror', handleError);
+ if (collectCoverage) {
+ await Promise.all(
+ (context as BrowserContext).pages().map((p) =>
+ p.close({
+ runBeforeUnload: true,
+ })
+ )
+ );
+ // wait until coverage data was sent successfully to the exposed function
+ await new Promise((resolve) => setTimeout(resolve, 10));
+ }
+
+ await browser?.close();
+
+ await super.teardown();
+ }
+ };
+};
+
+export default getPlaywrightEnv();
diff --git a/src/jest-playwright-preset/PlaywrightRunner.ts b/src/jest-playwright-preset/PlaywrightRunner.ts
new file mode 100644
index 00000000..9637b76b
--- /dev/null
+++ b/src/jest-playwright-preset/PlaywrightRunner.ts
@@ -0,0 +1,203 @@
+import JestRunner from 'jest-runner';
+import type { BrowserServer } from 'playwright-core';
+import type { Test, TestRunnerContext, TestWatcher, TestRunnerOptions } from 'jest-runner';
+import type { Config as JestConfig } from '@jest/types';
+import type {
+ BrowserType,
+ BrowserTest,
+ DeviceType,
+ WsEndpointType,
+ JestPlaywrightTest,
+ JestPlaywrightConfig,
+ ConfigDeviceType,
+ Playwright,
+} from './types';
+import {
+ checkBrowserEnv,
+ checkDevice,
+ getDisplayName,
+ readConfig,
+ getPlaywrightInstance,
+ getBrowserOptions,
+ getBrowserType,
+ getDeviceBrowserType,
+ deepMerge,
+ generateKey,
+} from './utils';
+import {
+ DEBUG_TIMEOUT,
+ DEFAULT_TEST_PLAYWRIGHT_TIMEOUT,
+ CONFIG_ENVIRONMENT_NAME,
+ SERVER,
+ LAUNCH,
+} from './constants';
+import { setupCoverage, mergeCoverage } from './coverage';
+import { GenericBrowser } from './types';
+
+const getBrowserTest = ({
+ test,
+ config,
+ browser,
+ wsEndpoint,
+ device,
+ testTimeout,
+}: BrowserTest): JestPlaywrightTest => {
+ const { displayName, testEnvironmentOptions } = test.context.config;
+ const playwrightDisplayName = getDisplayName(config.displayName || browser, device);
+ return {
+ ...test,
+ context: {
+ ...test.context,
+ config: {
+ ...test.context.config,
+ testEnvironmentOptions: {
+ ...testEnvironmentOptions,
+ [CONFIG_ENVIRONMENT_NAME]: { ...config, testTimeout },
+ },
+ browserName: browser,
+ wsEndpoint,
+ device,
+ displayName: {
+ name: displayName
+ ? `${playwrightDisplayName} ${displayName.name || displayName}`
+ : playwrightDisplayName,
+ color: displayName?.color || 'yellow',
+ },
+ },
+ },
+ };
+};
+
+const getDevices = (
+ devices: JestPlaywrightConfig['devices'],
+ availableDevices: Playwright['devices']
+) => {
+ let resultDevices: ConfigDeviceType[] = [];
+
+ if (devices) {
+ if (devices instanceof RegExp) {
+ resultDevices = Object.keys(availableDevices).filter((item) => item.match(devices));
+ } else {
+ resultDevices = devices;
+ }
+ }
+
+ return resultDevices;
+};
+
+const getJestTimeout = (configTimeout?: number) => {
+ if (configTimeout) {
+ return configTimeout;
+ }
+ return process.env.PWDEBUG ? DEBUG_TIMEOUT : DEFAULT_TEST_PLAYWRIGHT_TIMEOUT;
+};
+
+class PlaywrightRunner extends JestRunner {
+ browser2Server: Partial>;
+ config: JestConfig.GlobalConfig;
+ constructor(globalConfig: JestConfig.GlobalConfig, context: TestRunnerContext) {
+ const config = { ...globalConfig };
+ // Set testTimeout
+ config.testTimeout = getJestTimeout(config.testTimeout);
+ super(config, context);
+ this.browser2Server = {};
+ this.config = config;
+ }
+
+ async launchServer(
+ config: JestPlaywrightConfig,
+ wsEndpoint: WsEndpointType,
+ browser: BrowserType,
+ key: string,
+ instance: GenericBrowser
+ ): Promise {
+ const { launchType, launchOptions, skipInitialization } = config;
+ if (!skipInitialization && launchType === SERVER && wsEndpoint === null) {
+ if (!this.browser2Server[key]) {
+ const options = getBrowserOptions(browser, launchOptions);
+ this.browser2Server[key] = await instance.launchServer(options);
+ }
+ }
+ return wsEndpoint || this.browser2Server[key]?.wsEndpoint() || null;
+ }
+
+ async getTests(tests: Test[], config: JestPlaywrightConfig): Promise {
+ const { browsers, devices, connectOptions, useDefaultBrowserType } = config;
+ const pwTests: Test[] = [];
+ for (const test of tests) {
+ for (const browser of browsers) {
+ const browserType = getBrowserType(typeof browser === 'string' ? browser : browser.name);
+ const browserConfig =
+ typeof browser === 'string' ? config : deepMerge(config, browser || {});
+ checkBrowserEnv(browserType);
+ const { devices: availableDevices, instance } = getPlaywrightInstance(browserType);
+ const resultDevices = getDevices(devices, availableDevices);
+ const key =
+ typeof browser === 'string' ? browser : generateKey(browser.name, browserConfig);
+ const browserOptions = getBrowserOptions(browserType, connectOptions);
+ const wsEndpoint: WsEndpointType = await this.launchServer(
+ browserConfig,
+ 'wsEndpoint' in browserOptions ? browserOptions.wsEndpoint : null,
+ browserType,
+ key,
+ instance as GenericBrowser
+ );
+
+ const browserTest = {
+ test: test as JestPlaywrightTest,
+ config: browserConfig,
+ wsEndpoint,
+ browser: browserType,
+ testTimeout: this.config.testTimeout,
+ };
+
+ if (resultDevices.length) {
+ resultDevices.forEach((device: DeviceType) => {
+ checkDevice(device, availableDevices);
+ if (useDefaultBrowserType) {
+ const deviceBrowser = getDeviceBrowserType(device!, availableDevices);
+ if (deviceBrowser !== null && deviceBrowser !== browser) return;
+ }
+ pwTests.push(getBrowserTest({ ...browserTest, device }));
+ });
+ } else {
+ pwTests.push(getBrowserTest({ ...browserTest, device: null }));
+ }
+ }
+ }
+
+ return pwTests;
+ }
+
+ async runTests(
+ tests: Array,
+ watcher: TestWatcher,
+ options: TestRunnerOptions
+ ): Promise {
+ const { rootDir, testEnvironmentOptions } = tests[0].context.config;
+ const config = await readConfig(
+ rootDir,
+ testEnvironmentOptions[CONFIG_ENVIRONMENT_NAME] as JestPlaywrightConfig
+ );
+ if (this.config.testNamePattern) {
+ config.launchType = LAUNCH;
+ config.skipInitialization = true;
+ config.haveSkippedTests = true;
+ }
+ const browserTests = await this.getTests(tests, config);
+ if (config.collectCoverage) {
+ await setupCoverage();
+ }
+
+ await super.runTests(browserTests, watcher, options);
+
+ for (const key in this.browser2Server) {
+ await this.browser2Server[key]!.close();
+ }
+ if (config.collectCoverage) {
+ await mergeCoverage();
+ }
+ }
+}
+
+export default PlaywrightRunner;
diff --git a/src/jest-playwright-preset/constants.ts b/src/jest-playwright-preset/constants.ts
new file mode 100644
index 00000000..18900dbd
--- /dev/null
+++ b/src/jest-playwright-preset/constants.ts
@@ -0,0 +1,29 @@
+import type { JestPlaywrightConfig } from './types';
+
+export const IMPORT_KIND_PLAYWRIGHT = 'playwright';
+
+export const CONFIG_ENVIRONMENT_NAME = 'jest-playwright';
+
+export const CHROMIUM = 'chromium';
+export const FIREFOX = 'firefox';
+export const WEBKIT = 'webkit';
+
+export const LAUNCH = 'LAUNCH';
+export const PERSISTENT = 'PERSISTENT';
+export const SERVER = 'SERVER';
+
+export const DEFAULT_CONFIG: JestPlaywrightConfig = {
+ launchType: SERVER,
+ launchOptions: {},
+ connectOptions: {} as JestPlaywrightConfig['connectOptions'],
+ contextOptions: {},
+ browsers: [CHROMIUM],
+ exitOnPageError: true,
+ collectCoverage: false,
+};
+
+export const DEFAULT_TEST_PLAYWRIGHT_TIMEOUT = 15000;
+// Set timeout to 4 days
+export const DEBUG_TIMEOUT = 4 * 24 * 60 * 60 * 1000;
+
+export const PACKAGE_NAME = '@storybook/test-runner';
diff --git a/src/jest-playwright-preset/coverage.ts b/src/jest-playwright-preset/coverage.ts
new file mode 100644
index 00000000..6e180111
--- /dev/null
+++ b/src/jest-playwright-preset/coverage.ts
@@ -0,0 +1,55 @@
+import * as uuid from 'uuid';
+import path from 'path';
+import fs from 'fs';
+import type { Page } from 'playwright-core';
+import { promisify } from 'util';
+import rimraf from 'rimraf';
+const fsAsync = fs.promises;
+
+// @ts-ignore
+import NYC from 'nyc';
+import { PACKAGE_NAME } from './constants';
+
+const NYC_DIR = '.nyc_output';
+const COV_MERGE_DIR = path.join(NYC_DIR, 'merge');
+
+const cleanMergeFiles = async (): Promise => {
+ await promisify(rimraf)(COV_MERGE_DIR);
+};
+
+export const setupCoverage = async (): Promise => {
+ if (!fs.existsSync(NYC_DIR)) {
+ await fsAsync.mkdir(NYC_DIR);
+ }
+ await cleanMergeFiles();
+ await fsAsync.mkdir(COV_MERGE_DIR);
+};
+
+export const saveCoverageToFile = async (coverage: unknown): Promise => {
+ await fsAsync.writeFile(path.join(COV_MERGE_DIR, `${uuid.v4()}.json`), JSON.stringify(coverage));
+};
+
+export const saveCoverageOnPage = async (page: Page, collectCoverage = false): Promise => {
+ if (!collectCoverage) {
+ console.warn(
+ `${PACKAGE_NAME}: saveCoverage was called but collectCoverage is not true in config file`
+ );
+ return;
+ }
+ const coverage = await page.evaluate(`window.__coverage__`);
+ if (coverage) {
+ await saveCoverageToFile(coverage);
+ }
+};
+
+export const mergeCoverage = async (): Promise => {
+ const nyc = new NYC({
+ _: ['merge'],
+ });
+ const map = await nyc.getCoverageMapFromAllCoverageFiles(COV_MERGE_DIR);
+ const outputFile = path.join(NYC_DIR, 'coverage.json');
+ const content = JSON.stringify(map, null, 2);
+ await fsAsync.writeFile(outputFile, content);
+ console.info(`Coverage file (${content.length} bytes) written to ${outputFile}`);
+ await cleanMergeFiles();
+};
diff --git a/src/jest-playwright-preset/global.ts b/src/jest-playwright-preset/global.ts
new file mode 100644
index 00000000..26cde75b
--- /dev/null
+++ b/src/jest-playwright-preset/global.ts
@@ -0,0 +1,57 @@
+/* eslint-disable no-console */
+import { setup as setupServer, teardown as teardownServer } from 'jest-process-manager';
+import { readConfig } from './utils';
+import type { Config as JestConfig } from '@jest/types';
+import { ServerOptions } from './types';
+
+let didAlreadyRunInWatchMode = false;
+
+const ERROR_TIMEOUT = 'ERROR_TIMEOUT';
+const ERROR_NO_COMMAND = 'ERROR_NO_COMMAND';
+
+const logMessage = ({ message, action }: { message: string; action: string }): void => {
+ console.log('');
+ console.error(message);
+ console.error(`\n☝️ You ${action} in jest-playwright.config.js`);
+ process.exit(1);
+};
+
+export async function setup(jestConfig: JestConfig.GlobalConfig): Promise {
+ // TODO It won't work if config doesn't exist in root directory or in jest.config.js file
+ const config = await readConfig(jestConfig.rootDir);
+
+ // If we are in watch mode - only setupServer() once.
+ if (jestConfig.watch || jestConfig.watchAll) {
+ if (didAlreadyRunInWatchMode) return;
+ didAlreadyRunInWatchMode = true;
+ }
+
+ if (config.serverOptions) {
+ try {
+ await setupServer(config.serverOptions);
+ } catch (error) {
+ if (!(error instanceof Error)) throw error;
+ if ((error as NodeJS.ErrnoException).code === ERROR_TIMEOUT) {
+ logMessage({
+ message: error.message,
+ action: 'can set "serverOptions.launchTimeout"',
+ });
+ }
+ if ((error as NodeJS.ErrnoException).code === ERROR_NO_COMMAND) {
+ logMessage({
+ message: error.message,
+ action: 'must set "serverOptions.command"',
+ });
+ }
+ throw error;
+ }
+ }
+}
+
+export async function teardown(jestConfig: JestConfig.GlobalConfig): Promise {
+ const { serverOptions } = await readConfig(jestConfig.rootDir);
+
+ if (!jestConfig.watch && !jestConfig.watchAll) {
+ await teardownServer((serverOptions as ServerOptions)?.teardown);
+ }
+}
diff --git a/src/jest-playwright-preset/types.ts b/src/jest-playwright-preset/types.ts
new file mode 100644
index 00000000..30166f11
--- /dev/null
+++ b/src/jest-playwright-preset/types.ts
@@ -0,0 +1,236 @@
+import {
+ Browser,
+ BrowserContext,
+ Page,
+ BrowserContextOptions,
+ LaunchOptions,
+ ConnectOptions,
+ ConnectOverCDPOptions,
+ BrowserType as PlaywrightBrowserType,
+ ViewportSize,
+ ChromiumBrowser,
+ FirefoxBrowser,
+ WebKitBrowser,
+ devices,
+} from 'playwright-core';
+import { Test } from 'jest-runner';
+import { JestProcessManagerOptions } from 'jest-process-manager';
+
+// TODO Find out flex ways to reuse constants
+declare const IMPORT_KIND_PLAYWRIGHT = 'playwright';
+
+declare const CONFIG_ENVIRONMENT_NAME = 'jest-playwright';
+
+declare const CHROMIUM = 'chromium';
+declare const FIREFOX = 'firefox';
+declare const WEBKIT = 'webkit';
+
+declare const LAUNCH = 'LAUNCH';
+declare const PERSISTENT = 'PERSISTENT';
+declare const SERVER = 'SERVER';
+
+export type BrowserType = typeof CHROMIUM | typeof FIREFOX | typeof WEBKIT;
+
+export type SkipOption = {
+ browsers: BrowserType[];
+ devices?: string[] | RegExp;
+};
+
+type Global = typeof globalThis;
+
+export interface JestPlaywrightGlobal extends Global {
+ [CONFIG_ENVIRONMENT_NAME]: JestPlaywrightConfig;
+}
+
+export interface TestPlaywrightConfigOptions extends JestPlaywrightConfig {
+ browser?: BrowserType;
+ device?: ConfigDeviceType;
+}
+
+export type GenericBrowser = PlaywrightBrowserType<
+ WebKitBrowser | ChromiumBrowser | FirefoxBrowser
+>;
+
+export type Nullable = T | null;
+
+interface JestPlaywright {
+ /**
+ * Reset global.page
+ *
+ * ```ts
+ * it('should reset page', async () => {
+ * await jestPlaywright.resetPage()
+ * })
+ * ```
+ */
+ resetPage: () => Promise;
+ /**
+ * Reset global.context
+ *
+ * ```ts
+ * it('should reset context', async () => {
+ * await jestPlaywright.resetContext()
+ * })
+ * ```
+ */
+ resetContext: (newOptions?: BrowserContextOptions) => Promise;
+ /**
+ * Reset global.browser, global.context, and global.page
+ *
+ * ```ts
+ * it('should reset page', async () => {
+ * await jestPlaywright.resetBrowser()
+ * })
+ * ```
+ */
+ resetBrowser: (newOptions?: BrowserContextOptions) => Promise;
+ /**
+ * Saves the coverage to the disk which will only work if `collectCoverage`
+ * in `jest-playwright.config.js` file is set to true. The merged coverage file
+ * is then available in `.nyc_output/coverage.json`. Mostly its needed in the
+ * `afterEach` handler like that:
+ *
+ * ```ts
+ * afterEach(async () => {
+ * await jestPlaywright.saveCoverage(page)
+ * })
+ * ```
+ */
+ saveCoverage: (page: Page) => Promise;
+ configSeparateEnv: (
+ config: Partial,
+ isDebug?: boolean
+ ) => Promise;
+}
+
+interface JestParams {
+ (options: T, name: string, fn?: jest.ProvidesCallback, timeout?: number): void;
+}
+
+type ProvidesCallback = (cb: ConfigParams) => void;
+
+interface JestParamsWithConfigParams {
+ (options: Partial, name: string, fn?: ProvidesCallback, timeout?: number): void;
+}
+
+interface JestPlaywrightTestDebug extends JestParamsWithConfigParams {
+ (name: string, fn?: ProvidesCallback, timeout?: number): void;
+ skip: JestParamsWithConfigParams | JestPlaywrightTestDebug;
+ only: JestParamsWithConfigParams | JestPlaywrightTestDebug;
+}
+
+interface JestPlaywrightTestConfig extends JestParamsWithConfigParams {
+ skip: JestParamsWithConfigParams | JestPlaywrightTestConfig;
+ only: JestParamsWithConfigParams | JestPlaywrightTestConfig;
+}
+
+declare global {
+ const browserName: BrowserType;
+ const deviceName: Nullable;
+ const page: Page;
+ const browser: Browser;
+ const context: BrowserContext;
+ const jestPlaywright: JestPlaywright;
+ namespace jest {
+ interface It {
+ jestPlaywrightSkip: JestParams;
+ jestPlaywrightDebug: JestPlaywrightTestDebug;
+ jestPlaywrightConfig: JestPlaywrightTestConfig;
+ }
+ interface Describe {
+ jestPlaywrightSkip: JestParams;
+ }
+ }
+}
+
+type DeviceDescriptor = {
+ viewport: Nullable;
+ userAgent: string;
+ deviceScaleFactor: number;
+ isMobile: boolean;
+ hasTouch: boolean;
+ defaultBrowserType: BrowserType;
+};
+
+export type CustomDeviceType = Partial & {
+ name: string;
+};
+
+export type ConfigDeviceType = CustomDeviceType | string;
+
+export type DeviceType = Nullable;
+
+export type WsEndpointType = Nullable;
+
+export type SelectorType = {
+ script: string | Function | { path?: string; content?: string };
+ name: string;
+};
+
+export type PlaywrightRequireType = BrowserType | typeof IMPORT_KIND_PLAYWRIGHT;
+
+export interface Playwright {
+ name: PlaywrightRequireType;
+ instance: GenericBrowser | Record;
+ devices: typeof devices;
+}
+
+type LaunchType = typeof LAUNCH | typeof SERVER | typeof PERSISTENT;
+
+export type Options = T & Partial>;
+
+export type ServerOptions = JestProcessManagerOptions & {
+ teardown?: string;
+};
+
+export interface JestPlaywrightConfig {
+ haveSkippedTests?: boolean;
+ skipInitialization?: boolean;
+ resetContextPerTest?: boolean;
+ testTimeout?: number;
+ debugOptions?: JestPlaywrightConfig;
+ launchType?: LaunchType;
+ launchOptions?: Options;
+ connectOptions?: Options<(ConnectOptions & { wsEndpoint: string }) | ConnectOverCDPOptions>;
+ contextOptions?: Options;
+ userDataDir?: string;
+ exitOnPageError?: boolean;
+ displayName?: string;
+ browsers: (BrowserType | (JestPlaywrightConfig & { name: BrowserType }))[];
+ devices?: ConfigDeviceType[] | RegExp;
+ useDefaultBrowserType?: boolean;
+ serverOptions?: ServerOptions | ServerOptions[];
+ selectors?: SelectorType[];
+ collectCoverage?: boolean;
+}
+
+export type JestPlaywrightProjectConfig = Test['context']['config'] & {
+ browserName: BrowserType;
+ wsEndpoint: WsEndpointType;
+ device: DeviceType;
+};
+
+export type JestPlaywrightContext = Omit & {
+ config: JestPlaywrightProjectConfig;
+};
+
+export type JestPlaywrightTest = Omit & {
+ context: JestPlaywrightContext;
+};
+
+export interface BrowserTest {
+ test: JestPlaywrightTest;
+ config: JestPlaywrightConfig;
+ browser: BrowserType;
+ wsEndpoint: WsEndpointType;
+ device: DeviceType;
+ testTimeout?: number;
+}
+
+export type ConfigParams = {
+ browserName: BrowserType;
+ deviceName: Nullable;
+ browser: Nullable;
+ context: BrowserContext;
+ page: Page;
+};
diff --git a/src/jest-playwright-preset/utils.ts b/src/jest-playwright-preset/utils.ts
new file mode 100644
index 00000000..8abab53f
--- /dev/null
+++ b/src/jest-playwright-preset/utils.ts
@@ -0,0 +1,209 @@
+import fs from 'fs';
+import path from 'path';
+import type {
+ BrowserType,
+ ConfigDeviceType,
+ DeviceType,
+ JestPlaywrightConfig,
+ Playwright,
+ PlaywrightRequireType,
+ SkipOption,
+ Options,
+ Nullable,
+} from './types';
+import {
+ CHROMIUM,
+ DEFAULT_CONFIG,
+ FIREFOX,
+ IMPORT_KIND_PLAYWRIGHT,
+ WEBKIT,
+ PACKAGE_NAME,
+ CONFIG_ENVIRONMENT_NAME,
+} from './constants';
+
+const fsPromises = fs.promises;
+const BROWSERS = [CHROMIUM, FIREFOX, WEBKIT];
+
+class PlaywrightError extends Error {
+ constructor(message: string) {
+ super(formatError(message));
+ this.name = 'PlaywrightError';
+ }
+}
+
+export const checkBrowserEnv = (param: BrowserType): void => {
+ if (!BROWSERS.includes(param)) {
+ throw new PlaywrightError(
+ `Wrong browser type. Should be one of [${BROWSERS.join(', ')}], but got ${param}`
+ );
+ }
+};
+
+/* eslint-disable @typescript-eslint/no-explicit-any*/
+const isObject = (item: any) => {
+ return item && typeof item === 'object' && !Array.isArray(item);
+};
+
+export const deepMerge = >(target: T, source: T): T => {
+ let output = { ...target };
+ const keys: (keyof T)[] = Object.keys(source);
+ if (isObject(target) && isObject(source)) {
+ keys.forEach((key) => {
+ if (Array.isArray(source[key]) && Array.isArray(target[key])) {
+ output = { ...output, [key]: [...source[key], ...target[key]] };
+ } else if (isObject(source[key])) {
+ if (!(key in target)) {
+ output = { ...output, [key]: source[key] };
+ } else {
+ output[key] = deepMerge(target[key], source[key]);
+ }
+ } else {
+ output = { ...output, [key]: source[key] };
+ }
+ });
+ }
+ return output;
+};
+
+export const checkDeviceEnv = (device: string, availableDevices: string[]): void => {
+ if (!availableDevices.includes(device)) {
+ throw new PlaywrightError(
+ `Wrong device. Should be one of [${availableDevices}], but got ${device}`
+ );
+ }
+};
+
+export const checkDevice = (device: DeviceType, availableDevices: Playwright['devices']): void => {
+ if (typeof device === 'string') {
+ const availableDeviceNames = Object.keys(availableDevices);
+ checkDeviceEnv(device, availableDeviceNames);
+ }
+};
+
+export const getDisplayName = (browser: string, device: DeviceType): string => {
+ const result = `browser: ${browser}`;
+ if (device !== null) {
+ if (typeof device === 'string') {
+ return `${result} device: ${device}`;
+ }
+ if (device.name) {
+ return `${result} device: ${device.name}`;
+ }
+ }
+ return result;
+};
+
+export const getBrowserType = (browser?: BrowserType): BrowserType => {
+ return browser || CHROMIUM;
+};
+
+export const generateKey = (browser: BrowserType, config: JestPlaywrightConfig): string =>
+ `${browser}${JSON.stringify(config)}`;
+
+export const getDeviceBrowserType = (
+ device: ConfigDeviceType,
+ availableDevices: Playwright['devices']
+): Nullable => {
+ if (typeof device === 'string') {
+ return availableDevices[device].defaultBrowserType as BrowserType;
+ }
+
+ return device?.defaultBrowserType || null;
+};
+
+export const getPlaywrightInstance = (browserName?: BrowserType): Playwright => {
+ let pw;
+ let name: PlaywrightRequireType;
+ if (!browserName) {
+ pw = require(IMPORT_KIND_PLAYWRIGHT);
+ name = IMPORT_KIND_PLAYWRIGHT;
+ return {
+ name,
+ instance: pw,
+ devices: pw['devices'],
+ };
+ }
+ try {
+ pw = require(`${IMPORT_KIND_PLAYWRIGHT}-${browserName}`);
+ name = browserName;
+ } catch (e) {
+ try {
+ pw = require(IMPORT_KIND_PLAYWRIGHT);
+ name = IMPORT_KIND_PLAYWRIGHT;
+ } catch (e) {
+ throw new PlaywrightError(`Cannot find playwright package to use ${browserName}`);
+ }
+ }
+ if (!pw[browserName]) {
+ throw new PlaywrightError(`Cannot find playwright package to use ${browserName}`);
+ }
+ return {
+ name,
+ instance: pw[browserName],
+ devices: pw['devices'],
+ };
+};
+
+export function getBrowserOptions>(
+ browserName: BrowserType,
+ options?: Options
+): T {
+ let result: Options = options ? { ...options } : ({} as Options);
+ if (result[browserName]) {
+ result = deepMerge(result, result[browserName]!);
+ }
+ BROWSERS.forEach((browser) => {
+ delete result![browser as BrowserType];
+ });
+ return result as T;
+}
+
+export const getSkipFlag = (
+ skipOptions: SkipOption,
+ browserName: BrowserType,
+ deviceName: Nullable
+): boolean => {
+ const { browsers, devices } = skipOptions;
+ const isBrowserIncluded = browsers.includes(browserName);
+ if (!devices) {
+ return isBrowserIncluded;
+ } else {
+ if (devices instanceof RegExp) {
+ return isBrowserIncluded && devices.test(deviceName!);
+ }
+ return isBrowserIncluded && devices.includes(deviceName!);
+ }
+};
+
+export const readConfig = async (
+ rootDir = process.cwd(),
+ jestEnvConfig?: JestPlaywrightConfig
+): Promise => {
+ if (jestEnvConfig) {
+ return { ...DEFAULT_CONFIG, ...jestEnvConfig };
+ }
+ const { JEST_PLAYWRIGHT_CONFIG, npm_package_type } = process.env;
+ const fileExtension = npm_package_type === 'module' ? 'cjs' : 'js';
+ const configPath = JEST_PLAYWRIGHT_CONFIG || `${CONFIG_ENVIRONMENT_NAME}.config.${fileExtension}`;
+ const absConfigPath = path.resolve(rootDir, configPath);
+ try {
+ await fsPromises.access(absConfigPath);
+ } catch (e) {
+ if (JEST_PLAYWRIGHT_CONFIG) {
+ throw new PlaywrightError(
+ `Can't find a root directory while resolving a config file path.\nProvided path to resolve: ${configPath}`
+ );
+ } else {
+ return DEFAULT_CONFIG;
+ }
+ }
+
+ const localConfig = await require(absConfigPath);
+ if (typeof localConfig === 'function') {
+ const config = await localConfig();
+ return { ...DEFAULT_CONFIG, ...config };
+ }
+ return { ...DEFAULT_CONFIG, ...localConfig };
+};
+
+export const formatError = (error: string): string => `${PACKAGE_NAME}: ${error}`;
diff --git a/src/playwright/hooks.test.ts b/src/playwright/hooks.test.ts
index 673f445e..65923056 100644
--- a/src/playwright/hooks.test.ts
+++ b/src/playwright/hooks.test.ts
@@ -6,13 +6,14 @@ import {
TestRunnerConfig,
waitForPageReady,
} from './hooks';
+import { describe, it, expect, beforeEach, Mock, vi } from 'vitest';
-type MockPage = Page & { evaluate: jest.Mock };
+type MockPage = Page & { evaluate: Mock };
describe('test-runner', () => {
describe('setPreVisit', () => {
it('sets the preVisit function', () => {
- const preVisit = jest.fn();
+ const preVisit = vi.fn();
setPreVisit(preVisit);
expect(globalThis.__sbPreVisit).toBe(preVisit);
});
@@ -20,7 +21,7 @@ describe('test-runner', () => {
describe('setPostVisit', () => {
it('sets the postVisit function', () => {
- const postVisit = jest.fn();
+ const postVisit = vi.fn();
setPostVisit(postVisit);
expect(globalThis.__sbPostVisit).toBe(postVisit);
});
@@ -28,11 +29,11 @@ describe('test-runner', () => {
describe('getStoryContext', () => {
const page = {
- evaluate: jest.fn(),
+ evaluate: vi.fn(),
} as MockPage;
beforeEach(() => {
- jest.clearAllMocks();
+ vi.clearAllMocks();
});
it('calls page.evaluate with the correct arguments', async () => {
@@ -54,7 +55,7 @@ describe('test-runner', () => {
const storyContext = { kind: 'kind', name: 'name' };
// Mock globalThis.__getContext
- globalThis.__getContext = jest.fn();
+ globalThis.__getContext = vi.fn();
page.evaluate.mockImplementation(async (func) => {
// Call the function passed to page.evaluate
@@ -81,8 +82,8 @@ describe('test-runner', () => {
beforeEach(() => {
page = {
- waitForLoadState: jest.fn(),
- evaluate: jest.fn(),
+ waitForLoadState: vi.fn(),
+ evaluate: vi.fn(),
} as unknown as Page;
});
@@ -96,8 +97,8 @@ describe('test-runner', () => {
it('calls page.evaluate with () => document.fonts.ready', async () => {
const page = {
- waitForLoadState: jest.fn(),
- evaluate: jest.fn(),
+ waitForLoadState: vi.fn(),
+ evaluate: vi.fn(),
} as unknown as MockPage;
// Mock document.fonts.ready
diff --git a/src/playwright/index.ts b/src/playwright/index.ts
index e24e8811..53a364ee 100644
--- a/src/playwright/index.ts
+++ b/src/playwright/index.ts
@@ -1,14 +1,19 @@
-import { transformSync as swcTransform } from '@swc/core';
+import { transform as swcTransform } from '@swc/core';
import { transformPlaywright } from './transformPlaywright';
-export const process = (src: string, filename: string) => {
- const csfTest = transformPlaywright(src, filename);
+export const processAsync = async (src: string, filename: string) => {
+ const csfTest = await transformPlaywright(src, filename);
- const result = swcTransform(csfTest, {
+ const result = await swcTransform(csfTest, {
filename,
+ isModule: true,
module: {
- type: 'commonjs',
+ type: 'es6',
},
});
return result ? result.code : src;
};
+
+export const process = (src: string) => {
+ return src;
+};
diff --git a/src/playwright/transformPlaywright.test.ts b/src/playwright/transformPlaywright.test.ts
index 09006641..feaf007e 100644
--- a/src/playwright/transformPlaywright.test.ts
+++ b/src/playwright/transformPlaywright.test.ts
@@ -1,13 +1,19 @@
import dedent from 'ts-dedent';
import path from 'path';
import * as storybookMain from '../util/getStorybookMain';
+import { describe, beforeEach, expect, it, vi } from 'vitest';
import { transformPlaywright } from './transformPlaywright';
-jest.mock('storybook/internal/common', () => ({
- ...jest.requireActual('storybook/internal/common'),
- getProjectRoot: jest.fn(() => '/foo/bar'),
- normalizeStories: jest.fn(() => [
+vi.mock('storybook/internal/preview-api', async (importOriginal) => ({
+ ...(await importOriginal()),
+ userOrAutoTitle: vi.fn(() => 'Example/Header'),
+}));
+
+vi.mock('storybook/internal/common', async (importOriginal) => ({
+ ...(await importOriginal()),
+ getProjectRoot: vi.fn(() => '/stories/basic'),
+ normalizeStories: vi.fn(() => [
{
titlePrefix: 'Example',
files: '**/*.stories.@(mdx|tsx|ts|jsx|js)',
@@ -18,7 +24,7 @@ jest.mock('storybook/internal/common', () => ({
]),
}));
-jest.mock('../util/getTestRunnerConfig');
+vi.mock('../util/getTestRunnerConfig');
expect.addSnapshotSerializer({
print: (val: unknown) => (typeof val === 'string' ? val.trim() : String(val)),
@@ -28,16 +34,18 @@ expect.addSnapshotSerializer({
describe('Playwright', () => {
const filename = './stories/basic/Header.stories.js';
beforeEach(() => {
- const relativeSpy = jest.spyOn(path, 'relative');
+ const relativeSpy = vi.spyOn(path, 'relative');
relativeSpy.mockReturnValueOnce('stories/basic/Header.stories.js');
- jest.spyOn(storybookMain, 'getStorybookMain').mockImplementation(() => ({
- stories: [
- {
- directory: '../stories/basic',
- titlePrefix: 'Example',
- },
- ],
- }));
+ vi.spyOn(storybookMain, 'getStorybookMain').mockImplementation(() =>
+ Promise.resolve({
+ stories: [
+ {
+ directory: '../stories/basic',
+ titlePrefix: 'Example',
+ },
+ ],
+ })
+ );
delete process.env.STORYBOOK_INCLUDE_TAGS;
delete process.env.STORYBOOK_EXCLUDE_TAGS;
@@ -46,8 +54,8 @@ describe('Playwright', () => {
});
describe('tag filtering mechanism', () => {
- it('should include all stories when there is no tag filtering', () => {
- expect(
+ it('should include all stories when there is no tag filtering', async () => {
+ await expect(
transformPlaywright(
dedent`
export default { title: 'foo/bar', component: Button };
@@ -56,136 +64,134 @@ describe('Playwright', () => {
`,
filename
)
- ).toMatchInlineSnapshot(`
- if (!require.main) {
- describe("Example/foo/bar", () => {
- describe("A", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--a",
- title: "Example/foo/bar",
- name: "A"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--a"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
- }
- return result;
+ ).resolves.toMatchInlineSnapshot(`
+ describe("Example/Header", () => {
+ describe("A", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--a",
+ title: "Example/Header",
+ name: "A"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--a"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"A"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
- });
- describe("B", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--b",
- title: "Example/foo/bar",
- name: "B"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--b"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
}
- return result;
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"A"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
+ });
+ });
+ describe("B", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--b",
+ title: "Example/Header",
+ name: "B"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--b"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"B"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
+ }
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"B"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
});
});
- }
+ });
`);
});
- it('should exclude stories when excludeTags matches', () => {
+ it('should exclude stories when excludeTags matches', async () => {
process.env.STORYBOOK_EXCLUDE_TAGS = 'exclude-test';
- expect(
+ await expect(
transformPlaywright(
dedent`
export default { title: 'foo/bar', component: Button };
@@ -194,76 +200,74 @@ describe('Playwright', () => {
`,
filename
)
- ).toMatchInlineSnapshot(`
- if (!require.main) {
- describe("Example/foo/bar", () => {
- describe("B", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--b",
- title: "Example/foo/bar",
- name: "B"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--b"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
- }
- return result;
+ ).resolves.toMatchInlineSnapshot(`
+ describe("Example/Header", () => {
+ describe("B", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--b",
+ title: "Example/Header",
+ name: "B"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--b"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"B"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
+ }
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"B"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
});
});
- }
+ });
`);
});
- it('should skip stories when skipTags matches', () => {
+ it('should skip stories when skipTags matches', async () => {
process.env.STORYBOOK_SKIP_TAGS = 'skip-test';
- expect(
+ await expect(
transformPlaywright(
dedent`
export default { title: 'foo/bar', component: Button };
@@ -272,134 +276,132 @@ describe('Playwright', () => {
`,
filename
)
- ).toMatchInlineSnapshot(`
- if (!require.main) {
- describe("Example/foo/bar", () => {
- describe("A", () => {
- it.skip("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--a",
- title: "Example/foo/bar",
- name: "A"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--a"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
- }
- return result;
+ ).resolves.toMatchInlineSnapshot(`
+ describe("Example/Header", () => {
+ describe("A", () => {
+ it.skip("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--a",
+ title: "Example/Header",
+ name: "A"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--a"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"A"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
- });
- describe("B", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--b",
- title: "Example/foo/bar",
- name: "B"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--b"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
}
- return result;
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"A"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
+ });
+ });
+ describe("B", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--b",
+ title: "Example/Header",
+ name: "B"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--b"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"B"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
+ }
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"B"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
});
});
- }
+ });
`);
});
- it('should work in conjunction with includeTags, excludeTags and skipTags', () => {
+ it('should work in conjunction with includeTags, excludeTags and skipTags', async () => {
process.env.STORYBOOK_INCLUDE_TAGS = 'play,design,global-tag';
process.env.STORYBOOK_SKIP_TAGS = 'skip';
process.env.STORYBOOK_EXCLUDE_TAGS = 'exclude';
@@ -411,7 +413,7 @@ describe('Playwright', () => {
// - C being included
// - D being included
// - E being excluded
- expect(
+ await expect(
transformPlaywright(
dedent`
export default { title: 'foo/bar', component: Button };
@@ -423,511 +425,276 @@ describe('Playwright', () => {
`,
filename
)
- ).toMatchInlineSnapshot(`
- if (!require.main) {
- describe("Example/foo/bar", () => {
- describe("B", () => {
- it.skip("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--b",
- title: "Example/foo/bar",
- name: "B"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--b"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
- }
- return result;
+ ).resolves.toMatchInlineSnapshot(`
+ describe("Example/Header", () => {
+ describe("B", () => {
+ it.skip("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--b",
+ title: "Example/Header",
+ name: "B"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--b"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"B"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
- });
- describe("C", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--c",
- title: "Example/foo/bar",
- name: "C"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--c"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
}
- return result;
- };
- try {
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"B"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
await testFn();
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"C"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
- } else {
- throw err;
- }
+ } else {
+ throw err;
}
- });
+ }
});
- describe("D", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--d",
- title: "Example/foo/bar",
- name: "D"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--d"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
- }
- return result;
+ });
+ describe("C", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--c",
+ title: "Example/Header",
+ name: "C"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--c"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"D"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
- });
- describe("E", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--e",
- title: "Example/foo/bar",
- name: "E"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--e"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
}
- return result;
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"C"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
+ });
+ });
+ describe("D", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--d",
+ title: "Example/Header",
+ name: "D"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--d"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"E"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
- });
- });
- }
- `);
- });
- it('should work with tag negation', () => {
- process.env.STORYBOOK_INCLUDE_TAGS = 'play,test';
- process.env.STORYBOOK_PREVIEW_TAGS = '!test';
- // Should result in:
- // - A being included
- // - B being excluded because it has no play nor test tag (removed by negation in preview tags)
- // - C being included because it has test tag (overwritten via story tags)
- expect(
- transformPlaywright(
- dedent`
- export default { title: 'foo/bar', component: Button, tags: ['play'] };
- export const A = { };
- export const B = { tags: ['!play'] };
- export const C = { tags: ['!play', 'test'] };
- `,
- filename
- )
- ).toMatchInlineSnapshot(`
- if (!require.main) {
- describe("Example/foo/bar", () => {
- describe("A", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--a",
- title: "Example/foo/bar",
- name: "A"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--a"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
}
- return result;
- };
- try {
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"D"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
await testFn();
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"A"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
- } else {
- throw err;
- }
+ } else {
+ throw err;
}
- });
+ }
});
- describe("C", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--c",
- title: "Example/foo/bar",
- name: "C"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--c"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
- }
- return result;
+ });
+ describe("E", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--e",
+ title: "Example/Header",
+ name: "E"
};
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
try {
- await testFn();
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--e"
+ });
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"C"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
+ throw err;
} else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
throw err;
}
}
- });
- });
- });
- }
- `);
- });
- it('should include "test" tag by default', () => {
- // Should result in:
- // - A being included
- // - B being excluded
- expect(
- transformPlaywright(
- dedent`
- export default { title: 'foo/bar', component: Button };
- export const A = { };
- export const B = { tags: ['!test'] };
- `,
- filename
- )
- ).toMatchInlineSnapshot(`
- if (!require.main) {
- describe("Example/foo/bar", () => {
- describe("A", () => {
- it("smoke-test", async () => {
- const testFn = async () => {
- const context = {
- id: "example-foo-bar--a",
- title: "Example/foo/bar",
- name: "A"
- };
- if (globalThis.__sbPreVisit) {
- await globalThis.__sbPreVisit(page, context);
- }
- let result;
- try {
- result = await page.evaluate(({
- id,
- hasPlayFn
- }) => __test(id, hasPlayFn), {
- id: "example-foo-bar--a"
- });
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- throw err;
- } else {
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, {
- ...context,
- hasFailure: true
- });
- }
- throw err;
- }
- }
- if (globalThis.__sbPostVisit) {
- await globalThis.__sbPostVisit(page, context);
- }
- if (globalThis.__sbCollectCoverage) {
- const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
- if (!isCoverageSetupCorrectly) {
- throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
- The code in this story is not instrumented, which means the coverage setup is likely not correct.
- More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
- }
- await jestPlaywright.saveCoverage(page);
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
}
- return result;
- };
- try {
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"E"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
await testFn();
- } catch (err) {
- if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"A"}". Retrying...\`);
- await jestPlaywright.resetPage();
- await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
- await testFn();
- } else {
- throw err;
- }
+ } else {
+ throw err;
}
- });
+ }
});
});
- }
+ });
`);
});
- it('should no op when includeTags is passed but not matched', () => {
- process.env.STORYBOOK_INCLUDE_TAGS = 'play';
- expect(
+ it('should work with tag negation', async () => {
+ process.env.STORYBOOK_INCLUDE_TAGS = 'play,test';
+ process.env.STORYBOOK_PREVIEW_TAGS = '!test';
+ // Should result in:
+ // - A being included
+ // - B being excluded because it has no play nor test tag (removed by negation in preview tags)
+ // - C being included because it has test tag (overwritten via story tags)
+ await expect(
transformPlaywright(
dedent`
- export default { title: 'foo/bar', component: Button };
- export const A = () => {};
- A.play = () => {};
+ export default { title: 'foo/bar', component: Button, tags: ['play'] };
+ export const A = { };
+ export const B = { tags: ['!play'] };
+ export const C = { tags: ['!play', 'test'] };
`,
filename
)
- ).toMatchInlineSnapshot(`describe.skip('Example/foo/bar', () => { it('no-op', () => {}) });`);
- });
- });
-
- it('should generate a play test when the story has a play function', () => {
- expect(
- transformPlaywright(
- dedent`
- export default { title: 'foo/bar', component: Button };
- export const A = () => {};
- A.play = () => {};
- `,
- filename
- )
- ).toMatchInlineSnapshot(`
- if (!require.main) {
- describe("Example/foo/bar", () => {
+ ).resolves.toMatchInlineSnapshot(`
+ describe("Example/Header", () => {
describe("A", () => {
- it("play-test", async () => {
+ it("smoke-test", async () => {
const testFn = async () => {
const context = {
- id: "example-foo-bar--a",
- title: "Example/foo/bar",
+ id: "example-header--a",
+ title: "Example/Header",
name: "A"
};
if (globalThis.__sbPreVisit) {
@@ -939,7 +706,7 @@ describe('Playwright', () => {
id,
hasPlayFn
}) => __test(id, hasPlayFn), {
- id: "example-foo-bar--a"
+ id: "example-header--a"
});
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
@@ -972,7 +739,7 @@ describe('Playwright', () => {
await testFn();
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"A"}". Retrying...\`);
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"A"}". Retrying...\`);
await jestPlaywright.resetPage();
await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
await testFn();
@@ -982,29 +749,13 @@ describe('Playwright', () => {
}
});
});
- });
- }
- `);
- });
- it('should generate a smoke test when story does not have a play function', () => {
- expect(
- transformPlaywright(
- dedent`
- export default { title: 'foo/bar' };
- export const A = () => {};
- `,
- filename
- )
- ).toMatchInlineSnapshot(`
- if (!require.main) {
- describe("Example/foo/bar", () => {
- describe("A", () => {
+ describe("C", () => {
it("smoke-test", async () => {
const testFn = async () => {
const context = {
- id: "example-foo-bar--a",
- title: "Example/foo/bar",
- name: "A"
+ id: "example-header--c",
+ title: "Example/Header",
+ name: "C"
};
if (globalThis.__sbPreVisit) {
await globalThis.__sbPreVisit(page, context);
@@ -1015,7 +766,7 @@ describe('Playwright', () => {
id,
hasPlayFn
}) => __test(id, hasPlayFn), {
- id: "example-foo-bar--a"
+ id: "example-header--c"
});
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
@@ -1048,7 +799,7 @@ describe('Playwright', () => {
await testFn();
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
- console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"A"}". Retrying...\`);
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"C"}". Retrying...\`);
await jestPlaywright.resetPage();
await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
await testFn();
@@ -1059,20 +810,22 @@ describe('Playwright', () => {
});
});
});
- }
- `);
- });
- it('should generate a smoke test with auto title', () => {
- expect(
- transformPlaywright(
- dedent`
- export default { component: Button };
- export const A = () => {};
+ `);
+ });
+ it('should include "test" tag by default', async () => {
+ // Should result in:
+ // - A being included
+ // - B being excluded
+ await expect(
+ transformPlaywright(
+ dedent`
+ export default { title: 'foo/bar', component: Button };
+ export const A = { };
+ export const B = { tags: ['!test'] };
`,
- filename
- )
- ).toMatchInlineSnapshot(`
- if (!require.main) {
+ filename
+ )
+ ).resolves.toMatchInlineSnapshot(`
describe("Example/Header", () => {
describe("A", () => {
it("smoke-test", async () => {
@@ -1135,7 +888,246 @@ describe('Playwright', () => {
});
});
});
- }
+ `);
+ });
+ it('should no op when includeTags is passed but not matched', async () => {
+ process.env.STORYBOOK_INCLUDE_TAGS = 'play';
+ await expect(
+ transformPlaywright(
+ dedent`
+ export default { title: 'foo/bar', component: Button };
+ export const A = () => {};
+ A.play = () => {};
+ `,
+ filename
+ )
+ ).resolves.toMatchInlineSnapshot(
+ `describe.skip('Example/Header', () => { it('no-op', () => {}) });`
+ );
+ });
+ });
+
+ it('should generate a play test when the story has a play function', async () => {
+ await expect(
+ transformPlaywright(
+ dedent`
+ export default { title: 'foo/bar', component: Button };
+ export const A = () => {};
+ A.play = () => {};
+ `,
+ filename
+ )
+ ).resolves.toMatchInlineSnapshot(`
+ describe("Example/Header", () => {
+ describe("A", () => {
+ it("play-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--a",
+ title: "Example/Header",
+ name: "A"
+ };
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
+ try {
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--a"
+ });
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ throw err;
+ } else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
+ throw err;
+ }
+ }
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
+ }
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"A"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
+ });
+ });
+ });
+ `);
+ });
+ it('should generate a smoke test when story does not have a play function', async () => {
+ await expect(
+ transformPlaywright(
+ dedent`
+ export default { title: 'foo/bar' };
+ export const A = () => {};
+ `,
+ filename
+ )
+ ).resolves.toMatchInlineSnapshot(`
+ describe("Example/Header", () => {
+ describe("A", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--a",
+ title: "Example/Header",
+ name: "A"
+ };
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
+ try {
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--a"
+ });
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ throw err;
+ } else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
+ throw err;
+ }
+ }
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
+ }
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"A"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
+ });
+ });
+ });
+ `);
+ });
+ it('should generate a smoke test with auto title', async () => {
+ await expect(
+ transformPlaywright(
+ dedent`
+ export default { component: Button };
+ export const A = () => {};
+ `,
+ filename
+ )
+ ).resolves.toMatchInlineSnapshot(`
+ describe("Example/Header", () => {
+ describe("A", () => {
+ it("smoke-test", async () => {
+ const testFn = async () => {
+ const context = {
+ id: "example-header--a",
+ title: "Example/Header",
+ name: "A"
+ };
+ if (globalThis.__sbPreVisit) {
+ await globalThis.__sbPreVisit(page, context);
+ }
+ let result;
+ try {
+ result = await page.evaluate(({
+ id,
+ hasPlayFn
+ }) => __test(id, hasPlayFn), {
+ id: "example-header--a"
+ });
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ throw err;
+ } else {
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, {
+ ...context,
+ hasFailure: true
+ });
+ }
+ throw err;
+ }
+ }
+ if (globalThis.__sbPostVisit) {
+ await globalThis.__sbPostVisit(page, context);
+ }
+ if (globalThis.__sbCollectCoverage) {
+ const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
+ if (!isCoverageSetupCorrectly) {
+ throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
+ The code in this story is not instrumented, which means the coverage setup is likely not correct.
+ More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
+ }
+ await jestPlaywright.saveCoverage(page);
+ }
+ return result;
+ };
+ try {
+ await testFn();
+ } catch (err) {
+ if (err.toString().includes('Execution context was destroyed')) {
+ console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/Header"}/\${"A"}". Retrying...\`);
+ await jestPlaywright.resetPage();
+ await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
+ await testFn();
+ } else {
+ throw err;
+ }
+ }
+ });
+ });
+ });
`);
});
});
diff --git a/src/playwright/transformPlaywright.ts b/src/playwright/transformPlaywright.ts
index 818296c3..f15a3a76 100644
--- a/src/playwright/transformPlaywright.ts
+++ b/src/playwright/transformPlaywright.ts
@@ -1,9 +1,12 @@
import { relative } from 'path';
-import template from '@babel/template';
+import babelTemplate from '@babel/template';
+
+// Handle both ESM and CJS patterns
+const template = (babelTemplate as any).default ?? babelTemplate;
import { userOrAutoTitle } from 'storybook/internal/preview-api';
import dedent from 'ts-dedent';
-import { getStorybookMetadata } from '../util';
+import { getStorybookMetadata } from '../util/getStorybookMetadata';
import { transformCsf } from '../csf/transformCsf';
import type { TestPrefixer } from '../csf/transformCsf';
@@ -79,24 +82,24 @@ export const testPrefixer: TestPrefixer = (context) => {
)({ ...context });
};
-const makeTitleFactory = (filename: string) => {
- const { workingDir, normalizedStoriesEntries } = getStorybookMetadata();
+const makeTitleFactory = async (filename: string) => {
+ const { workingDir, normalizedStoriesEntries } = await getStorybookMetadata();
const filePath = `./${relative(workingDir, filename)}`;
return (userTitle: string) =>
userOrAutoTitle(filePath, normalizedStoriesEntries, userTitle) as string;
};
-export const transformPlaywright = (src: string, filename: string) => {
+export const transformPlaywright = async (src: string, filename: string) => {
const tags = process.env.STORYBOOK_PREVIEW_TAGS?.split(',') ?? [];
const transformOptions = {
testPrefixer,
insertTestIfEmpty: true,
clearBody: true,
- makeTitle: makeTitleFactory(filename),
+ makeTitle: await makeTitleFactory(filename),
previewAnnotations: { tags },
};
- const result = transformCsf(src, transformOptions);
+ const result = await transformCsf(src, transformOptions);
return result;
};
diff --git a/src/playwright/transformPlaywrightJson.test.ts b/src/playwright/transformPlaywrightJson.test.ts
index 0a1b398d..8a4d47d5 100644
--- a/src/playwright/transformPlaywrightJson.test.ts
+++ b/src/playwright/transformPlaywrightJson.test.ts
@@ -5,9 +5,10 @@ import {
makeDescribe,
transformPlaywrightJson,
} from './transformPlaywrightJson';
+import { describe, it, expect, beforeEach, vi } from 'vitest';
import * as t from '@babel/types';
-jest.mock('../util/getTestRunnerConfig');
+vi.mock('../util/getTestRunnerConfig');
describe('Playwright Json', () => {
describe('v4 indexes', () => {
@@ -17,7 +18,7 @@ describe('Playwright Json', () => {
delete process.env.STORYBOOK_SKIP_TAGS;
});
- it('should generate a test for each story', () => {
+ it('should generate a test for each story', async () => {
const input = {
v: 4,
entries: {
@@ -41,7 +42,7 @@ describe('Playwright Json', () => {
},
},
} satisfies V4Index;
- expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(`
+ await expect(transformPlaywrightJson(input)).resolves.toMatchInlineSnapshot(`
{
"example-header": "describe("Example/Header", () => {
describe("Logged In", () => {
@@ -231,7 +232,7 @@ describe('Playwright Json', () => {
`);
});
- it('should respect include, exclude and skip tags', () => {
+ it('should respect include, exclude and skip tags', async () => {
process.env.STORYBOOK_INCLUDE_TAGS = 'play,design';
process.env.STORYBOOK_SKIP_TAGS = 'skip';
process.env.STORYBOOK_EXCLUDE_TAGS = 'exclude';
@@ -272,7 +273,7 @@ describe('Playwright Json', () => {
// - B being included, but skipped
// - C being included
// - D being excluded
- expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(`
+ await expect(transformPlaywrightJson(input)).resolves.toMatchInlineSnapshot(`
{
"example-header": "describe("Example/Header", () => {
describe("Logged Out", () => {
@@ -402,7 +403,7 @@ describe('Playwright Json', () => {
`);
});
- it('should skip docs entries', () => {
+ it('should skip docs entries', async () => {
const input = {
v: 4,
entries: {
@@ -419,7 +420,7 @@ describe('Playwright Json', () => {
},
},
} satisfies V4Index;
- expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(`
+ await expect(transformPlaywrightJson(input)).resolves.toMatchInlineSnapshot(`
{
"example-page": "describe("Example/Page", () => {
describe("Logged In", () => {
@@ -489,7 +490,7 @@ describe('Playwright Json', () => {
});
describe('v3 indexes', () => {
- it('should generate a test for each story', () => {
+ it('should generate a test for each story', async () => {
const input = {
v: 3,
stories: {
@@ -525,7 +526,7 @@ describe('Playwright Json', () => {
},
},
} satisfies V3StoriesIndex;
- expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(`
+ await expect(transformPlaywrightJson(input)).resolves.toMatchInlineSnapshot(`
{
"example-header": "describe("Example/Header", () => {
describe("Logged In", () => {
@@ -715,7 +716,7 @@ describe('Playwright Json', () => {
`);
});
- it('should skip docs-only stories', () => {
+ it('should skip docs-only stories', async () => {
const input = {
v: 3,
stories: {
@@ -741,7 +742,7 @@ describe('Playwright Json', () => {
},
},
} satisfies V3StoriesIndex;
- expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(`
+ await expect(transformPlaywrightJson(input)).resolves.toMatchInlineSnapshot(`
{
"example-page": "describe("Example/Page", () => {
describe("Logged In", () => {
@@ -809,7 +810,7 @@ describe('Playwright Json', () => {
`);
});
- it('should include "test" tag by default', () => {
+ it('should include "test" tag by default', async () => {
process.env.STORYBOOK_INCLUDE_TAGS = 'test';
const input = {
v: 3,
@@ -826,7 +827,7 @@ describe('Playwright Json', () => {
},
},
} satisfies V3StoriesIndex;
- expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(`
+ await expect(transformPlaywrightJson(input)).resolves.toMatchInlineSnapshot(`
{
"example-page": "describe("Example/Page", () => {
describe("Logged In", () => {
@@ -897,9 +898,9 @@ describe('Playwright Json', () => {
});
describe('unsupported index', () => {
- it('throws an error for unsupported versions', () => {
+ it('throws an error for unsupported versions', async () => {
const unsupportedVersion = { v: 1 } satisfies UnsupportedVersion;
- expect(() => transformPlaywrightJson(unsupportedVersion)).toThrowError(
+ await expect(transformPlaywrightJson(unsupportedVersion)).rejects.toThrowError(
`Unsupported version ${unsupportedVersion.v}`
);
});
diff --git a/src/playwright/transformPlaywrightJson.ts b/src/playwright/transformPlaywrightJson.ts
index 9a8b1238..4c2f4a0a 100644
--- a/src/playwright/transformPlaywrightJson.ts
+++ b/src/playwright/transformPlaywrightJson.ts
@@ -1,5 +1,8 @@
import * as t from '@babel/types';
-import generate from '@babel/generator';
+import babelGenerate from '@babel/generator';
+
+// Handle both ESM and CJS patterns
+const generate = (babelGenerate as any).default ?? babelGenerate;
import { ComponentTitle, StoryId, StoryName, toId } from 'storybook/internal/csf';
import { testPrefixer } from './transformPlaywright';
@@ -140,7 +143,9 @@ function groupByTitleId(entries: T[]) {
* Generate one test file per component so that Jest can
* run them in parallel.
*/
-export const transformPlaywrightJson = (index: V3StoriesIndex | V4Index | UnsupportedVersion) => {
+export const transformPlaywrightJson = async (
+ index: V3StoriesIndex | V4Index | UnsupportedVersion
+) => {
let titleIdToEntries: Record;
if (index.v === 3) {
const titleIdToStories = groupByTitleId(
@@ -157,7 +162,7 @@ export const transformPlaywrightJson = (index: V3StoriesIndex | V4Index | Unsupp
throw new Error(`Unsupported version ${index.v}`);
}
- const { includeTags, excludeTags, skipTags } = getTagOptions();
+ const { includeTags, excludeTags, skipTags } = await getTagOptions();
const titleIdToTest = Object.entries(titleIdToEntries).reduce>(
(acc, [titleId, entries]) => {
diff --git a/src/setup-page.ts b/src/setup-page.ts
index ab9cd03a..296f3316 100644
--- a/src/setup-page.ts
+++ b/src/setup-page.ts
@@ -4,7 +4,8 @@ import { pkgUp } from 'pkg-up';
import { PrepareContext } from './playwright/hooks';
import { getTestRunnerConfig } from './util/getTestRunnerConfig';
import { readFile } from 'node:fs/promises';
-import path from 'node:path';
+import path, { dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
/**
* This is a default prepare function which can be overridden by the user.
@@ -36,7 +37,10 @@ export const setupPage = async (page: Page, browserContext: BrowserContext) => {
const failOnConsole = process.env.TEST_CHECK_CONSOLE;
const viewMode = process.env.VIEW_MODE ?? 'story';
- const renderedEvent = viewMode === 'docs' ? 'globalThis.__STORYBOOK_MODULE_CORE_EVENTS__.DOCS_RENDERED' : 'globalThis.__STORYBOOK_MODULE_CORE_EVENTS__.STORY_FINISHED ?? globalThis.__STORYBOOK_MODULE_CORE_EVENTS__.STORY_RENDERED';
+ const renderedEvent =
+ viewMode === 'docs'
+ ? 'globalThis.__STORYBOOK_MODULE_CORE_EVENTS__.DOCS_RENDERED'
+ : 'globalThis.__STORYBOOK_MODULE_CORE_EVENTS__.STORY_FINISHED ?? globalThis.__STORYBOOK_MODULE_CORE_EVENTS__.STORY_RENDERED';
const { packageJson } = (await readPackageUp()) as NormalizedReadResult;
const { version: testRunnerVersion } = packageJson;
@@ -51,7 +55,7 @@ export const setupPage = async (page: Page, browserContext: BrowserContext) => {
);
}
- const testRunnerConfig = getTestRunnerConfig() || {};
+ const testRunnerConfig = (await getTestRunnerConfig()) || {};
if (testRunnerConfig?.prepare) {
await testRunnerConfig.prepare({ page, browserContext, testRunnerConfig });
} else {
@@ -69,16 +73,18 @@ export const setupPage = async (page: Page, browserContext: BrowserContext) => {
return message;
});
+ const cwd = typeof __dirname === 'string' ? __dirname : dirname(fileURLToPath(import.meta.url));
+
const finalStorybookUrl = referenceURL ?? targetURL ?? '';
- const testRunnerPackageLocation = await pkgUp({ cwd: __dirname });
+ const testRunnerPackageLocation = await pkgUp({ cwd });
if (!testRunnerPackageLocation) throw new Error('Could not find test-runner package location');
const scriptLocation = path.join(
path.dirname(testRunnerPackageLocation),
'dist',
- 'setup-page-script.mjs'
+ 'setup-page-script.js'
);
- // read the content of setup-page-script.mjs and replace the placeholders with the actual values
+ // read the content of setup-page-script.js and replace the placeholders with the actual values
const content = (await readFile(scriptLocation, 'utf-8'))
.replaceAll('{{storybookUrl}}', finalStorybookUrl)
.replaceAll('{{failOnConsole}}', failOnConsole ?? 'false')
diff --git a/src/test-storybook.ts b/src/test-storybook.ts
index 8004e530..bc215630 100644
--- a/src/test-storybook.ts
+++ b/src/test-storybook.ts
@@ -1,22 +1,28 @@
#!/usr/bin/env node
-import fs from 'fs';
-import { execSync } from 'child_process';
-import fetch from 'node-fetch';
-import canBindToHost from 'can-bind-to-host';
-import dedent from 'ts-dedent';
-import path, { join, resolve } from 'path';
-import tempy from 'tempy';
+import { canBindToHost } from 'can-bind-to-host';
+import { glob } from 'glob';
+import { execSync, spawn } from 'node:child_process';
+import fs from 'node:fs';
+import { createRequire } from 'node:module';
+import { fileURLToPath } from 'node:url';
+import path, { join, resolve } from 'pathe';
import { getInterpretedFile } from 'storybook/internal/common';
import { readConfig } from 'storybook/internal/csf-tools';
import { telemetry } from 'storybook/internal/telemetry';
-import { glob } from 'glob';
+import tempy from 'tempy';
+import dedent from 'ts-dedent';
-import { JestOptions, getCliOptions } from './util/getCliOptions';
-import { getStorybookMetadata } from './util/getStorybookMetadata';
-import { getTestRunnerConfig } from './util/getTestRunnerConfig';
-import { transformPlaywrightJson } from './playwright/transformPlaywrightJson';
-import { TestRunnerConfig } from './playwright/hooks';
+import { TestRunnerConfig } from './playwright/hooks.js';
+import { transformPlaywrightJson } from './playwright/transformPlaywrightJson.js';
+import { JestOptions, getCliOptions } from './util/getCliOptions.js';
+import { getStorybookMetadata } from './util/getStorybookMetadata.js';
+import { getTestRunnerConfig } from './util/getTestRunnerConfig.js';
+
+// Get the current file's directory in ESM
+const __filename2 = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename2);
+const require = createRequire(import.meta.url);
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'test';
@@ -48,10 +54,11 @@ const cleanup = () => {
}
};
-function getNycBinPath() {
- const nycPath = path.join(require.resolve('nyc/package.json'));
- const nycBin = require(nycPath).bin.nyc;
- const nycBinFullPath = path.join(path.dirname(nycPath), nycBin);
+async function getNycBinPath() {
+ const nycPkgUrl = new URL('nyc/package.json', import.meta.url);
+ const { bin } = await import(nycPkgUrl.href, { assert: { type: 'json' } });
+ const nycBin = bin.nyc;
+ const nycBinFullPath = path.join(path.dirname(fileURLToPath(nycPkgUrl)), nycBin);
return nycBinFullPath;
}
@@ -80,11 +87,11 @@ async function reportCoverage() {
// --check-coverage if we want to break if coverage reaches certain threshold
// .nycrc will be respected for thresholds etc. https://www.npmjs.com/package/nyc#coverage-thresholds
if (process.env.JEST_SHARD !== 'true') {
- const nycBinFullPath = getNycBinPath();
+ const nycBinFullPath = await getNycBinPath();
execSync(
`node ${nycBinFullPath} report --reporter=text --reporter=lcov -t ${coverageFolder} --report-dir ${coverageFolder}`,
{
- stdio: 'inherit',
+ stdio: 'pipe',
cwd: process.cwd(),
}
);
@@ -124,12 +131,14 @@ function sanitizeURL(url: string) {
async function executeJestPlaywright(args: JestOptions) {
// Always prefer jest installed via the test runner. If it's hoisted, it will get it from root node_modules
- const jestPath = path.dirname(
- require.resolve('jest', {
- paths: [path.join(__dirname, '../@storybook/test-runner/node_modules')],
- })
+ const jestPath = path.join(
+ path.dirname(
+ require.resolve('jest/package.json', {
+ paths: [path.join(__dirname, '../@storybook/test-runner/node_modules')],
+ })
+ ),
+ 'bin/jest.js'
);
- const jest = require(jestPath);
const argv = args.slice(2);
// jest configs could either come in the root dir, or inside of the Storybook config dir
@@ -145,16 +154,48 @@ async function executeJestPlaywright(args: JestOptions) {
userDefinedJestConfig ||
path.resolve(__dirname, path.join('..', 'playwright', 'test-runner-jest.config.js'));
- argv.push('--config', jestConfigPath);
+ const command = `node --experimental-vm-modules "${jestPath}" ${argv.join(
+ ' '
+ )} --config "${jestConfigPath}" --color`;
+
+ const child = spawn(command, {
+ shell: true,
+ cwd: process.cwd(),
+ stdio: ['inherit', 'pipe'],
+ });
+ const shouldLog = (str: string) => {
+ return (
+ !str.includes('watchman') &&
+ !str.includes('DeprecationWarning') &&
+ !str.includes('ExperimentalWarning')
+ );
+ };
+ const exitCode = await new Promise((resolve) => {
+ // filter out messages like DeprecationWarning and ExperimentalWarning
+ child.stdout?.on('data', (data) => {
+ const str = data.toString();
+ if (shouldLog(str)) {
+ process.stdout.write(data);
+ }
+ });
+ child.stderr?.on('data', (data) => {
+ const str = data.toString();
+ if (shouldLog(str)) {
+ process.stderr.write(data);
+ }
+ });
+
+ child.on('exit', (exitCode) => resolve(exitCode || 0));
+ });
- await jest.run(argv);
+ process.exit(exitCode);
}
async function checkStorybook(url: string) {
try {
const headers = await getHttpHeaders(url);
const res = await fetch(url, { method: 'GET', headers });
- if (res.status !== 200) throw new Error(`Unxpected status: ${res.status}`);
+ if (res.status !== 200) throw new Error(`Unexpected status: ${res.status}`);
} catch (e) {
console.error(
dedent`\x1b[31m[test-storybook]\x1b[0m It seems that your Storybook instance is not running at: ${url}. Are you sure it's running?
@@ -212,7 +253,7 @@ async function getIndexTempDir(url: string) {
let tmpDir: string;
try {
const indexJson = await getIndexJson(url);
- const titleIdToTest = transformPlaywrightJson(indexJson);
+ const titleIdToTest = await transformPlaywrightJson(indexJson);
tmpDir = tempy.directory();
for (const [titleId, test] of Object.entries(titleIdToTest)) {
@@ -246,7 +287,9 @@ function ejectConfiguration() {
}
// copy contents of origin and replace ../dist with @storybook/test-runner
- const content = fs.readFileSync(origin, 'utf-8').replace(/..\/dist/g, '@storybook/test-runner');
+ const content = fs
+ .readFileSync(origin, 'utf-8')
+ .replace('../dist/index.js', '@storybook/test-runner');
fs.writeFileSync(destination, content);
log(`Configuration file successfully generated at ${destination}`);
}
@@ -282,7 +325,8 @@ const main = async () => {
process.env.STORYBOOK_CONFIG_DIR = runnerOptions.configDir;
- const testRunnerConfig = getTestRunnerConfig(runnerOptions.configDir) ?? ({} as TestRunnerConfig);
+ const testRunnerConfig =
+ (await getTestRunnerConfig(runnerOptions.configDir)) ?? ({} as TestRunnerConfig);
if (testRunnerConfig.preVisit && testRunnerConfig.preRender) {
throw new Error(
@@ -378,7 +422,7 @@ const main = async () => {
}
const { storiesPaths, lazyCompilation, disableTelemetry, enableCrashReports } =
- getStorybookMetadata();
+ await getStorybookMetadata();
if (!shouldRunIndexJson) {
process.env.STORYBOOK_STORIES_PATTERN = storiesPaths;
diff --git a/src/util/__snapshots__/getStorybookMain.test.ts.snap b/src/util/__snapshots__/getStorybookMain.test.ts.snap
index c06d0310..480469f0 100644
--- a/src/util/__snapshots__/getStorybookMain.test.ts.snap
+++ b/src/util/__snapshots__/getStorybookMain.test.ts.snap
@@ -1,15 +1,15 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[`getStorybookMain no stories should throw an error if no stories are defined 1`] = `
-"Could not find stories in main.js in ".storybook".
+exports[`getStorybookMain > no stories > should throw an error if no stories are defined 1`] = `
+[Error: Could not find stories in main.js in ".storybook".
If you are using a mono-repository, please run the test-runner only against your sub-package, which contains a .storybook folder with "stories" defined in main.js.
-You can change the config directory by using --config-dir "
+You can change the config directory by using --config-dir ]
`;
-exports[`getStorybookMain no stories should throw an error if stories list is empty 1`] = `
-"Could not find stories in main.js in ".storybook".
+exports[`getStorybookMain > no stories > should throw an error if stories list is empty 1`] = `
+[Error: Could not find stories in main.js in ".storybook".
If you are using a mono-repository, please run the test-runner only against your sub-package, which contains a .storybook folder with "stories" defined in main.js.
-You can change the config directory by using --config-dir "
+You can change the config directory by using --config-dir ]
`;
-exports[`getStorybookMain should throw an error if no configuration is found 1`] = `"Could not load main.js in .storybook. Is the ".storybook" config directory correct? You can change it by using --config-dir "`;
+exports[`getStorybookMain > should throw an error if no configuration is found 1`] = `[Error: Module not found]`;
diff --git a/src/util/getCliOptions.test.ts b/src/util/getCliOptions.test.ts
index 47e6eddf..b5f2a44a 100644
--- a/src/util/getCliOptions.test.ts
+++ b/src/util/getCliOptions.test.ts
@@ -1,5 +1,6 @@
import { getCliOptions } from './getCliOptions';
import * as cliHelper from './getParsedCliOptions';
+import { describe, it, expect, afterEach, vi } from 'vitest';
describe('getCliOptions', () => {
const originalArgv: string[] = process.argv;
@@ -10,15 +11,16 @@ describe('getCliOptions', () => {
it('returns custom options if passed', () => {
const customConfig = { configDir: 'custom', indexJson: true };
- jest
- .spyOn(cliHelper, 'getParsedCliOptions')
- .mockReturnValueOnce({ options: customConfig, extraArgs: [] });
+ vi.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValueOnce({
+ options: customConfig,
+ extraArgs: [],
+ });
const opts = getCliOptions();
expect(opts.runnerOptions).toMatchObject(customConfig);
});
it('returns default options if no options are passed', () => {
- jest.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue({ options: {}, extraArgs: [] });
+ vi.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue({ options: {}, extraArgs: [] });
const opts = getCliOptions();
const jestOptions = opts.jestOptions.length > 0 ? ['--coverage'] : [];
expect(opts).toEqual({
@@ -29,33 +31,36 @@ describe('getCliOptions', () => {
it('returns failOnConsole option if passed', () => {
const customConfig = { failOnConsole: true };
- jest
- .spyOn(cliHelper, 'getParsedCliOptions')
- .mockReturnValue({ options: customConfig, extraArgs: [] });
+ vi.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue({
+ options: customConfig,
+ extraArgs: [],
+ });
const opts = getCliOptions();
expect(opts.runnerOptions).toMatchObject(customConfig);
});
it('handles boolean options correctly', () => {
const customConfig = { coverage: true, junit: false };
- jest
- .spyOn(cliHelper, 'getParsedCliOptions')
- .mockReturnValue({ options: customConfig, extraArgs: [] });
+ vi.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue({
+ options: customConfig,
+ extraArgs: [],
+ });
const opts = getCliOptions();
expect(opts).toEqual({ jestOptions: [], runnerOptions: { coverage: true, junit: false } });
});
it('handles string options correctly', () => {
const customConfig = { url: 'http://localhost:3000' };
- jest
- .spyOn(cliHelper, 'getParsedCliOptions')
- .mockReturnValue({ options: customConfig, extraArgs: [] });
+ vi.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue({
+ options: customConfig,
+ extraArgs: [],
+ });
const opts = getCliOptions();
expect(opts).toEqual({ jestOptions: [], runnerOptions: { url: 'http://localhost:3000' } });
});
it('handles extra arguments correctly', () => {
- jest.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue({
+ vi.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue({
options: { version: true, cache: false, env: 'node' } as any,
extraArgs: ['--watch', '--coverage'],
});
@@ -75,7 +80,7 @@ describe('getCliOptions', () => {
// mock argv to avoid side effect from running tests e.g. jest --coverage,
// which would end up caught by getCliOptions
process.argv = [];
- jest.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValueOnce({ options: {}, extraArgs });
+ vi.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValueOnce({ options: {}, extraArgs });
const opts = getCliOptions();
expect(opts.jestOptions).toEqual(extraArgs);
});
diff --git a/src/util/getCliOptions.ts b/src/util/getCliOptions.ts
index af4e4a12..13fa5b0e 100644
--- a/src/util/getCliOptions.ts
+++ b/src/util/getCliOptions.ts
@@ -1,5 +1,5 @@
import { getParsedCliOptions } from './getParsedCliOptions';
-import type { BrowserType } from 'jest-playwright-preset';
+import type { BrowserType } from '../jest-playwright-preset/types';
export type JestOptions = string[];
diff --git a/src/util/getParsedCliOptions.test.ts b/src/util/getParsedCliOptions.test.ts
index 6f34bd6a..7a9183e5 100644
--- a/src/util/getParsedCliOptions.test.ts
+++ b/src/util/getParsedCliOptions.test.ts
@@ -1,9 +1,10 @@
import { program } from 'commander';
import { getParsedCliOptions } from './getParsedCliOptions';
+import { describe, it, expect, afterEach, vi } from 'vitest';
describe('getParsedCliOptions', () => {
afterEach(() => {
- jest.restoreAllMocks();
+ vi.restoreAllMocks();
});
it('should return the parsed CLI options', () => {
@@ -41,10 +42,10 @@ describe('getParsedCliOptions', () => {
it('should handle unknown options', () => {
const originalWarn = console.warn;
- console.warn = jest.fn();
+ console.warn = vi.fn();
const originalExit = process.exit;
- process.exit = jest.fn() as unknown as typeof process.exit;
+ process.exit = vi.fn() as unknown as typeof process.exit;
const argv = process.argv.slice();
process.argv.push('--unknown-option');
@@ -62,7 +63,7 @@ describe('getParsedCliOptions', () => {
});
it('handles unknown options correctly', () => {
- jest.spyOn(program, 'parse').mockImplementation(() => {
+ vi.spyOn(program, 'parse').mockImplementation(() => {
throw new Error('Unknown error');
});
diff --git a/src/util/getParsedCliOptions.ts b/src/util/getParsedCliOptions.ts
index eb15a9d7..9ee29b22 100644
--- a/src/util/getParsedCliOptions.ts
+++ b/src/util/getParsedCliOptions.ts
@@ -39,7 +39,7 @@ export const getParsedCliOptions = (): ParsedCliOptions => {
'--maxWorkers ',
'Specifies the maximum number of workers the worker-pool will spawn for running tests'
)
- .option('--testTimeout ', 'This option sets the default timeouts of test cases')
+ .option('--testTimeout ', 'This option sets the timeout of each test case')
.option('--no-cache', 'Disable the cache')
.option('--clearCache', 'Deletes the Jest cache directory and then exits without running tests')
.option('--verbose', 'Display individual test results with the test suite hierarchy')
diff --git a/src/util/getStorybookMain.test.ts b/src/util/getStorybookMain.test.ts
index 0ac9dc8f..b4e02566 100644
--- a/src/util/getStorybookMain.test.ts
+++ b/src/util/getStorybookMain.test.ts
@@ -1,32 +1,39 @@
import { getStorybookMain, resetStorybookMainCache, storybookMainConfig } from './getStorybookMain';
-import * as coreCommon from 'storybook/internal/common';
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { serverRequire } from 'storybook/internal/common';
-jest.mock('storybook/internal/common');
+vi.mock('storybook/internal/common', () => ({
+ serverRequire: vi.fn(),
+}));
describe('getStorybookMain', () => {
beforeEach(() => {
resetStorybookMainCache();
+ vi.clearAllMocks();
});
- it('should throw an error if no configuration is found', () => {
- expect(() => getStorybookMain('.storybook')).toThrowErrorMatchingSnapshot();
+ it('should throw an error if no configuration is found', async () => {
+ vi.mocked(serverRequire).mockRejectedValueOnce(new Error('Module not found'));
+ await expect(getStorybookMain('.storybook')).rejects.toThrowErrorMatchingSnapshot();
});
describe('no stories', () => {
- it('should throw an error if no stories are defined', () => {
- jest.spyOn(coreCommon, 'serverRequire').mockImplementation(() => ({}));
+ it('should throw an error if no stories are defined', async () => {
+ vi.mocked(serverRequire).mockResolvedValueOnce({});
- expect(() => getStorybookMain('.storybook')).toThrowErrorMatchingSnapshot();
+ await expect(getStorybookMain('.storybook')).rejects.toThrowErrorMatchingSnapshot();
});
- it('should throw an error if stories list is empty', () => {
- jest.spyOn(coreCommon, 'serverRequire').mockImplementation(() => ({ stories: [] }));
+ it('should throw an error if stories list is empty', async () => {
+ vi.mocked(serverRequire).mockResolvedValueOnce({
+ stories: [],
+ });
- expect(() => getStorybookMain('.storybook')).toThrowErrorMatchingSnapshot();
+ await expect(getStorybookMain('.storybook')).rejects.toThrowErrorMatchingSnapshot();
});
});
- it('should return mainjs', () => {
+ it('should return mainjs', async () => {
const mockedMain = {
stories: [
{
@@ -36,13 +43,13 @@ describe('getStorybookMain', () => {
],
};
- jest.spyOn(coreCommon, 'serverRequire').mockImplementation(() => mockedMain);
+ vi.mocked(serverRequire).mockResolvedValueOnce(mockedMain);
- const res = getStorybookMain('.storybook');
+ const res = await getStorybookMain('.storybook');
expect(res).toMatchObject(mockedMain);
});
- it('should return the configDir value if it exists', () => {
+ it('should return the configDir value if it exists', async () => {
const mockedMain = {
stories: [
{
@@ -51,9 +58,9 @@ describe('getStorybookMain', () => {
},
],
};
- storybookMainConfig.set('configDir', mockedMain);
+ storybookMainConfig.set('.storybook', mockedMain);
- const res = getStorybookMain('.storybook');
+ const res = await getStorybookMain('.storybook');
expect(res).toMatchObject(mockedMain);
});
});
diff --git a/src/util/getStorybookMain.ts b/src/util/getStorybookMain.ts
index 90d66c16..5db693a6 100644
--- a/src/util/getStorybookMain.ts
+++ b/src/util/getStorybookMain.ts
@@ -5,32 +5,39 @@ import dedent from 'ts-dedent';
export const storybookMainConfig = new Map();
-export const getStorybookMain = (configDir = '.storybook') => {
+export const getStorybookMain = async (configDir = '.storybook') => {
if (storybookMainConfig.has(configDir)) {
return storybookMainConfig.get(configDir) as StorybookConfig;
- } else {
- storybookMainConfig.set(configDir, serverRequire(join(resolve(configDir), 'main')));
}
- const mainConfig = storybookMainConfig.get(configDir);
+ try {
+ const mainConfig = await serverRequire(join(resolve(configDir), 'main'));
+ if (!mainConfig) {
+ throw new Error(
+ `Could not load main.js in ${configDir}. Is the "${configDir}" config directory correct? You can change it by using --config-dir `
+ );
+ }
- if (!mainConfig) {
- throw new Error(
- `Could not load main.js in ${configDir}. Is the "${configDir}" config directory correct? You can change it by using --config-dir `
- );
- }
+ if (!mainConfig.stories || mainConfig.stories.length === 0) {
+ throw new Error(
+ dedent`
+ Could not find stories in main.js in "${configDir}".
+ If you are using a mono-repository, please run the test-runner only against your sub-package, which contains a .storybook folder with "stories" defined in main.js.
+ You can change the config directory by using --config-dir
+ `
+ );
+ }
- if (!mainConfig.stories || mainConfig.stories.length === 0) {
+ storybookMainConfig.set(configDir, mainConfig);
+ return mainConfig;
+ } catch (error) {
+ if (error instanceof Error) {
+ throw error;
+ }
throw new Error(
- dedent`
- Could not find stories in main.js in "${configDir}".
- If you are using a mono-repository, please run the test-runner only against your sub-package, which contains a .storybook folder with "stories" defined in main.js.
- You can change the config directory by using --config-dir
- `
+ `Could not load main.js in ${configDir}. Is the "${configDir}" config directory correct? You can change it by using --config-dir `
);
}
-
- return mainConfig;
};
export function resetStorybookMainCache() {
diff --git a/src/util/getStorybookMetadata.test.ts b/src/util/getStorybookMetadata.test.ts
index 1a5c103a..3f13d6f0 100644
--- a/src/util/getStorybookMetadata.test.ts
+++ b/src/util/getStorybookMetadata.test.ts
@@ -2,11 +2,11 @@ import { StorybookConfig } from 'storybook/internal/types';
import * as storybookMain from './getStorybookMain';
import { getStorybookMetadata } from './getStorybookMetadata';
+import { describe, it, expect, afterAll, vi } from 'vitest';
-jest.mock('storybook/internal/common', () => ({
- ...jest.requireActual('storybook/internal/common'),
- getProjectRoot: jest.fn(() => '/foo/bar'),
- normalizeStories: jest.fn(() => [
+vi.mock('storybook/internal/common', () => ({
+ getProjectRoot: vi.fn(() => '/foo/bar'),
+ normalizeStories: vi.fn(() => [
{
titlePrefix: 'Example',
files: '**/*.stories.@(mdx|tsx|ts|jsx|js)',
@@ -22,18 +22,18 @@ describe('getStorybookMetadata', () => {
process.env.STORYBOOK_CONFIG_DIR = undefined;
});
- it('should return configDir coming from environment variable', () => {
+ it('should return configDir coming from environment variable', async () => {
const mockedMain: Pick = {
stories: [],
};
- jest.spyOn(storybookMain, 'getStorybookMain').mockReturnValueOnce(mockedMain);
+ vi.spyOn(storybookMain, 'getStorybookMain').mockResolvedValueOnce(mockedMain);
process.env.STORYBOOK_CONFIG_DIR = '.storybook';
- const { configDir } = getStorybookMetadata();
+ const { configDir } = await getStorybookMetadata();
expect(configDir).toEqual(process.env.STORYBOOK_CONFIG_DIR);
});
- it('should return storiesPath with default glob from CSF3 style config', () => {
+ it('should return storiesPath with default glob from CSF3 style config', async () => {
const mockedMain: Pick = {
stories: [
{
@@ -43,28 +43,28 @@ describe('getStorybookMetadata', () => {
],
};
- jest.spyOn(storybookMain, 'getStorybookMain').mockReturnValueOnce(mockedMain);
+ vi.spyOn(storybookMain, 'getStorybookMain').mockResolvedValueOnce(mockedMain);
process.env.STORYBOOK_CONFIG_DIR = '.storybook';
- const { storiesPaths } = getStorybookMetadata();
+ const { storiesPaths } = await getStorybookMetadata();
expect(storiesPaths).toMatchInlineSnapshot(
`"/foo/bar/stories/basic/**/*.stories.@(mdx|tsx|ts|jsx|js)"`
);
});
- it('should return storiesPath with glob defined in CSF2 style config', () => {
+ it('should return storiesPath with glob defined in CSF2 style config', async () => {
const mockedMain: Pick = {
stories: ['../**/stories/*.stories.@(js|ts)'],
};
- jest.spyOn(storybookMain, 'getStorybookMain').mockReturnValueOnce(mockedMain);
+ vi.spyOn(storybookMain, 'getStorybookMain').mockResolvedValueOnce(mockedMain);
process.env.STORYBOOK_CONFIG_DIR = '.storybook';
- const { storiesPaths } = getStorybookMetadata();
+ const { storiesPaths } = await getStorybookMetadata();
expect(storiesPaths).toMatchInlineSnapshot(
`"/foo/bar/stories/basic/**/*.stories.@(mdx|tsx|ts|jsx|js)"`
);
});
- it('should return storiesPath from mixed CSF2 and CSF3 style config', () => {
+ it('should return storiesPath from mixed CSF2 and CSF3 style config', async () => {
const mockedMain: Pick = {
stories: [
{
@@ -75,29 +75,29 @@ describe('getStorybookMetadata', () => {
],
};
- jest.spyOn(storybookMain, 'getStorybookMain').mockReturnValueOnce(mockedMain);
+ vi.spyOn(storybookMain, 'getStorybookMain').mockResolvedValueOnce(mockedMain);
process.env.STORYBOOK_CONFIG_DIR = '.storybook';
- const { storiesPaths } = getStorybookMetadata();
+ const { storiesPaths } = await getStorybookMetadata();
expect(storiesPaths).toMatchInlineSnapshot(
`"/foo/bar/stories/basic/**/*.stories.@(mdx|tsx|ts|jsx|js)"`
);
});
- it('should return lazyCompilation=false when unset', () => {
+ it('should return lazyCompilation=false when unset', async () => {
const mockedMain: Pick = { stories: [] };
- jest.spyOn(storybookMain, 'getStorybookMain').mockReturnValueOnce(mockedMain);
+ vi.spyOn(storybookMain, 'getStorybookMain').mockResolvedValueOnce(mockedMain);
process.env.STORYBOOK_CONFIG_DIR = '.storybook';
- expect(getStorybookMetadata().lazyCompilation).toBe(false);
+ expect((await getStorybookMetadata()).lazyCompilation).toBe(false);
});
- it('should return lazyCompilation=true when set', () => {
+ it('should return lazyCompilation=true when set', async () => {
const mockedMain: Pick = {
stories: [],
core: { builder: { name: 'webpack5', options: { lazyCompilation: true } } },
};
- jest.spyOn(storybookMain, 'getStorybookMain').mockReturnValueOnce(mockedMain);
+ vi.spyOn(storybookMain, 'getStorybookMain').mockResolvedValueOnce(mockedMain);
process.env.STORYBOOK_CONFIG_DIR = '.storybook';
- expect(getStorybookMetadata().lazyCompilation).toBe(true);
+ expect((await getStorybookMetadata()).lazyCompilation).toBe(true);
});
});
diff --git a/src/util/getStorybookMetadata.ts b/src/util/getStorybookMetadata.ts
index bf496bd8..c35d1ebf 100644
--- a/src/util/getStorybookMetadata.ts
+++ b/src/util/getStorybookMetadata.ts
@@ -4,11 +4,11 @@ import { StoriesEntry } from 'storybook/internal/types';
import { getStorybookMain } from './getStorybookMain';
-export const getStorybookMetadata = () => {
+export const getStorybookMetadata = async () => {
const workingDir = getProjectRoot();
const configDir = process.env.STORYBOOK_CONFIG_DIR ?? '.storybook';
- const main = getStorybookMain(configDir);
+ const main = await getStorybookMain(configDir);
const normalizedStoriesEntries = normalizeStories(main.stories as StoriesEntry[], {
configDir,
workingDir,
@@ -22,10 +22,8 @@ export const getStorybookMetadata = () => {
.map((dir) => join(workingDir, dir))
.join(';');
- // @ts-expect-error -- this is added in storybook/internal/common@6.5, which we don't depend on
const lazyCompilation = !!main.core?.builder?.options?.lazyCompilation;
- // @ts-expect-error -- need to update to latest sb version
const { disableTelemetry, enableCrashReports } = main.core || {};
return {
diff --git a/src/util/getTagOptions.ts b/src/util/getTagOptions.ts
index 0a72d942..2be39a02 100644
--- a/src/util/getTagOptions.ts
+++ b/src/util/getTagOptions.ts
@@ -11,8 +11,8 @@ type TagOptions = {
* 1. Test runner config 'tags' object
* 2. Environment variables (takes precedence)
*/
-export function getTagOptions() {
- const config = getTestRunnerConfig();
+export async function getTagOptions() {
+ const config = await getTestRunnerConfig();
let tagOptions = {
includeTags: config?.tags?.include || ['test'],
diff --git a/src/util/getTestRunnerConfig.test.ts b/src/util/getTestRunnerConfig.test.ts
index 0361d5eb..28bbf740 100644
--- a/src/util/getTestRunnerConfig.test.ts
+++ b/src/util/getTestRunnerConfig.test.ts
@@ -1,6 +1,8 @@
import { TestRunnerConfig } from '../playwright/hooks';
import { getTestRunnerConfig } from './getTestRunnerConfig';
import { join, resolve } from 'path';
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { serverRequire } from 'storybook/internal/common';
const testRunnerConfig: TestRunnerConfig = {
setup: () => {
@@ -26,45 +28,41 @@ const testRunnerConfig: TestRunnerConfig = {
},
};
-jest.mock('storybook/internal/common', () => ({
- serverRequire: jest.fn(),
+vi.mock('storybook/internal/common', () => ({
+ serverRequire: vi.fn(),
}));
describe('getTestRunnerConfig', () => {
beforeEach(() => {
- jest.clearAllMocks();
+ vi.clearAllMocks();
});
- it('should load the test runner config', () => {
+ it('should load the test runner config', async () => {
const configDir = '.storybook';
- (require('storybook/internal/common').serverRequire as jest.Mock).mockReturnValueOnce(
- testRunnerConfig
- );
+ vi.mocked(serverRequire).mockResolvedValueOnce(testRunnerConfig);
- const result = getTestRunnerConfig(configDir);
+ const result = await getTestRunnerConfig(configDir);
expect(result).toEqual(testRunnerConfig);
- expect(require('storybook/internal/common').serverRequire).toHaveBeenCalledWith(
+ expect(vi.mocked(serverRequire)).toHaveBeenCalledWith(
join(resolve('.storybook', 'test-runner'))
);
});
- it('should cache the test runner config', () => {
+ it('should cache the test runner config', async () => {
const configDir = '.storybook';
- (require('storybook/internal/common').serverRequire as jest.Mock).mockReturnValueOnce(
- testRunnerConfig
- );
+ vi.mocked(serverRequire).mockResolvedValueOnce(testRunnerConfig);
- const result1 = getTestRunnerConfig(configDir);
- const result2 = getTestRunnerConfig(configDir);
+ const result1 = await getTestRunnerConfig(configDir);
+ const result2 = await getTestRunnerConfig(configDir);
expect(result1).toEqual(testRunnerConfig);
expect(result2).toEqual(testRunnerConfig);
});
- it('should load the test runner config with default configDir', () => {
+ it('should load the test runner config with default configDir', async () => {
process.env.STORYBOOK_CONFIG_DIR = '.storybook';
- const result = getTestRunnerConfig();
+ const result = await getTestRunnerConfig();
expect(result).toEqual(testRunnerConfig);
});
diff --git a/src/util/getTestRunnerConfig.ts b/src/util/getTestRunnerConfig.ts
index 4b3307ad..43f0eb1c 100644
--- a/src/util/getTestRunnerConfig.ts
+++ b/src/util/getTestRunnerConfig.ts
@@ -5,15 +5,15 @@ import { TestRunnerConfig } from '../playwright/hooks';
let testRunnerConfig: TestRunnerConfig;
let loaded = false;
-export const getTestRunnerConfig = (
+export const getTestRunnerConfig = async (
configDir = process.env.STORYBOOK_CONFIG_DIR ?? '.storybook'
-): TestRunnerConfig | undefined => {
+): Promise => {
// testRunnerConfig can be undefined
if (loaded) {
return testRunnerConfig;
}
- testRunnerConfig = serverRequire(join(resolve(configDir), 'test-runner'));
+ testRunnerConfig = await serverRequire(join(resolve(configDir), 'test-runner'));
loaded = true;
return testRunnerConfig;
};
diff --git a/stories/atoms/__snapshots__/Button.stories.tsx.snap b/stories/atoms/__snapshots__/Button.stories.tsx.snap
index dab58ccb..0fd0fab4 100644
--- a/stories/atoms/__snapshots__/Button.stories.tsx.snap
+++ b/stories/atoms/__snapshots__/Button.stories.tsx.snap
@@ -1,4 +1,4 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
+// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`Atoms/Button Demo play-test 2`] = `