From cdcc12fea503949ee97949869c193293fa9197bd Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:05:22 -0500 Subject: [PATCH 01/16] Enable strict mode --- .github/workflows/typecheck.yml | 19 +++++++++++++++ src/config/jest-playwright.ts | 2 +- src/csf/transformCsf.ts | 31 +++++++++++++++++------- src/playwright/transformPlaywright.ts | 3 ++- src/setup-page.ts | 6 ++--- src/test-storybook.ts | 7 ++++-- src/util/getCliOptions.ts | 16 ++++++++----- src/util/getParsedCliOptions.ts | 34 +++++++++++++++------------ src/util/getStorybookMetadata.ts | 5 ++-- src/util/getTestRunnerConfig.ts | 2 +- tsconfig.json | 4 +++- 11 files changed, 88 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/typecheck.yml diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 00000000..ee76f474 --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,19 @@ +on: [push, pull_request] + +jobs: + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js 18.x + uses: actions/setup-node@v1 + with: + node-version: '18.x' + + - name: Install dependencies + uses: bahmutov/npm-install@v1 + + - name: Type check + run: yarn tsc + diff --git a/src/config/jest-playwright.ts b/src/config/jest-playwright.ts index fc86105e..bcb0d766 100644 --- a/src/config/jest-playwright.ts +++ b/src/config/jest-playwright.ts @@ -85,7 +85,7 @@ export const getJestConfig = () => { snapshotSerializers: [jestSerializerHtmlPath], testEnvironmentOptions: { 'jest-playwright': { - browsers: TEST_BROWSERS.split(',') + browsers: TEST_BROWSERS?.split(',') .map((p) => p.trim().toLowerCase()) .filter(Boolean), collectCoverage: STORYBOOK_COLLECT_COVERAGE === 'true', diff --git a/src/csf/transformCsf.ts b/src/csf/transformCsf.ts index fd7821d3..9c865fe8 100644 --- a/src/csf/transformCsf.ts +++ b/src/csf/transformCsf.ts @@ -37,9 +37,20 @@ const prefixFunction = ( id: t.stringLiteral(toId(title, name)), }; - const result = makeArray(testPrefixer(context)); - const stmt = result[1] as t.ExpressionStatement; - return stmt.expression; + let result = input; + if (testPrefixer) { + const prefixResult = makeArray(testPrefixer(context)); + const stmt = prefixResult[1] as t.ExpressionStatement; + if (stmt) { + result = stmt.expression; + } + } + + if (!result) { + result = t.nullLiteral(); + } + + return result; }; const makePlayTest = ( @@ -91,11 +102,11 @@ export const transformCsf = ( makeTitle, }: TransformOptions = {} ) => { - const csf = loadCsf(code, { makeTitle }); + const csf = loadCsf(code, { makeTitle: makeTitle || ((userTitle: string) => userTitle) }); csf.parse(); const storyExports = Object.keys(csf._stories); - const title = csf.meta.title; + const title = csf.meta?.title; const storyPlays = storyExports.reduce((acc, key) => { const annotations = csf._storyAnnotations[key]; @@ -107,14 +118,16 @@ export const transformCsf = ( const playTests = storyExports .map((key: string) => { let tests: t.Statement[] = []; - tests = [...tests, ...makePlayTest(key, title, storyPlays[key], testPrefixer)]; + if (title) { + tests = [...tests, ...makePlayTest(key, title, storyPlays[key], testPrefixer)]; + } if (tests.length) { return makeDescribe(key, tests); } return null; }) - .filter(Boolean); + .filter(Boolean) as babel.types.Statement[]; const allTests = playTests; @@ -123,7 +136,7 @@ export const transformCsf = ( if (!clearBody) result = `${result}${code}\n`; if (allTests.length) { const describe = makeDescribe( - csf.meta.title, + csf.meta?.title as string, allTests, beforeEachPrefixer ? makeBeforeEach(beforeEachPrefixer) : undefined ) as babel.types.Node; @@ -135,7 +148,7 @@ export const transformCsf = ( } `; } else if (insertTestIfEmpty) { - result = `describe('${csf.meta.title}', () => { it('no-op', () => {}) });`; + result = `describe('${csf.meta?.title}', () => { it('no-op', () => {}) });`; } return result; }; diff --git a/src/playwright/transformPlaywright.ts b/src/playwright/transformPlaywright.ts index 0e43cd84..7f5bcd9b 100644 --- a/src/playwright/transformPlaywright.ts +++ b/src/playwright/transformPlaywright.ts @@ -71,7 +71,8 @@ const makeTitleFactory = (filename: string) => { const { workingDir, normalizedStoriesEntries } = getStorybookMetadata(); const filePath = './' + relative(workingDir, filename); - return (userTitle: string) => userOrAutoTitle(filePath, normalizedStoriesEntries, userTitle); + return (userTitle: string) => + userOrAutoTitle(filePath, normalizedStoriesEntries, userTitle) as string; }; export const transformPlaywright = (src: string, filename: string) => { diff --git a/src/setup-page.ts b/src/setup-page.ts index b64b78ad..6ed56703 100644 --- a/src/setup-page.ts +++ b/src/setup-page.ts @@ -1,5 +1,5 @@ import type { Page, BrowserContext } from 'playwright'; -import readPackageUp from 'read-pkg-up'; +import readPackageUp, { NormalizedReadResult } from 'read-pkg-up'; import { PrepareContext } from './playwright/hooks'; import { getTestRunnerConfig } from './util'; @@ -55,7 +55,7 @@ export const setupPage = async (page: Page, browserContext: BrowserContext) => { const viewMode = process.env.VIEW_MODE || 'story'; const renderedEvent = viewMode === 'docs' ? 'docsRendered' : 'storyRendered'; - const { packageJson } = await readPackageUp(); + const { packageJson } = (await readPackageUp()) as NormalizedReadResult; const { version: testRunnerVersion } = packageJson; const referenceURL = process.env.REFERENCE_URL && sanitizeURL(process.env.REFERENCE_URL); @@ -73,7 +73,7 @@ export const setupPage = async (page: Page, browserContext: BrowserContext) => { if (testRunnerConfig?.prepare) { await testRunnerConfig.prepare({ page, browserContext, testRunnerConfig }); } else { - await defaultPrepare({ page, browserContext, testRunnerConfig }); + if (testRunnerConfig) await defaultPrepare({ page, browserContext, testRunnerConfig }); } // if we ever want to log something from the browser to node diff --git a/src/test-storybook.ts b/src/test-storybook.ts index ae0cb608..665683b7 100644 --- a/src/test-storybook.ts +++ b/src/test-storybook.ts @@ -35,7 +35,7 @@ process.on('unhandledRejection', (err) => { }); const log = (message: string) => console.log(`[test-storybook] ${message}`); -const error = (err: { message: any; stack: any }) => { +const error = (err: Error) => { if (err instanceof Error) { console.error(`\x1b[31m[test-storybook]\x1b[0m ${err.message} \n\n${err.stack}`); } else { @@ -250,7 +250,10 @@ async function getIndexTempDir(url: string) { fs.writeFileSync(tmpFile, test as string); }); } catch (err) { - error(err); + const errorMessage = err instanceof Error ? err.message : String(err); + const errorObject = new Error(errorMessage); + errorObject.stack = err instanceof Error ? err.stack : undefined; + error(errorObject); process.exit(1); } return tmpDir; diff --git a/src/util/getCliOptions.ts b/src/util/getCliOptions.ts index 2db332e1..816e3018 100644 --- a/src/util/getCliOptions.ts +++ b/src/util/getCliOptions.ts @@ -48,16 +48,20 @@ export const getCliOptions = (): CliOptions => { jestOptions: process.argv.splice(0, 2), }; - const finalOptions = Object.keys(allOptions).reduce((acc, key: StorybookRunnerCommand) => { - if (STORYBOOK_RUNNER_COMMANDS.includes(key)) { - copyOption(acc.runnerOptions, key, allOptions[key]); + const finalOptions = Object.keys(allOptions).reduce((acc: CliOptions, key: string) => { + if (STORYBOOK_RUNNER_COMMANDS.includes(key as StorybookRunnerCommand)) { + copyOption( + acc.runnerOptions, + key as StorybookRunnerCommand, + allOptions[key as StorybookRunnerCommand] + ); } else { - if (allOptions[key] === true) { + if (allOptions[key as StorybookRunnerCommand] === true) { acc.jestOptions.push(`--${key}`); - } else if (allOptions[key] === false) { + } else if (allOptions[key as StorybookRunnerCommand] === false) { acc.jestOptions.push(`--no-${key}`); } else { - acc.jestOptions.push(`--${key}`, allOptions[key] as string); + acc.jestOptions.push(`--${key}="${allOptions[key as StorybookRunnerCommand]}"`); } } diff --git a/src/util/getParsedCliOptions.ts b/src/util/getParsedCliOptions.ts index d4df6774..df487599 100644 --- a/src/util/getParsedCliOptions.ts +++ b/src/util/getParsedCliOptions.ts @@ -1,5 +1,5 @@ import type { CliOptions } from './getCliOptions'; -import { program } from 'commander'; +import { CommanderError, program } from 'commander'; type ParsedCliOptions = { options: CliOptions['runnerOptions']; @@ -82,23 +82,27 @@ export const getParsedCliOptions = (): ParsedCliOptions => { try { program.parse(); - } catch (err) { - switch (err.code) { - case 'commander.unknownOption': { - program.outputHelp(); - console.warn( - `\nIf you'd like this option to be supported, please open an issue at https://github.com/storybookjs/test-runner/issues/new\n` - ); - process.exit(1); - } + } catch (err: unknown) { + if (err instanceof CommanderError) { + switch (err.code) { + case 'commander.unknownOption': { + program.outputHelp(); + console.warn( + `\nIf you'd like this option to be supported, please open an issue at https://github.com/storybookjs/test-runner/issues/new\n` + ); + process.exit(1); + } - case 'commander.helpDisplayed': { - process.exit(0); - } + case 'commander.helpDisplayed': { + process.exit(0); + } - default: { - throw err; + default: { + throw err; + } } + } else { + throw err; } } diff --git a/src/util/getStorybookMetadata.ts b/src/util/getStorybookMetadata.ts index 7ac02571..fd45b660 100644 --- a/src/util/getStorybookMetadata.ts +++ b/src/util/getStorybookMetadata.ts @@ -1,13 +1,14 @@ import { join } from 'path'; import { normalizeStories, getProjectRoot } from '@storybook/core-common'; import { getStorybookMain } from './getStorybookMain'; +import { StoriesEntry } from '@storybook/types'; export const getStorybookMetadata = () => { const workingDir = getProjectRoot(); - const configDir = process.env.STORYBOOK_CONFIG_DIR; + const configDir = process.env.STORYBOOK_CONFIG_DIR || ''; const main = getStorybookMain(configDir); - const normalizedStoriesEntries = normalizeStories(main.stories, { + const normalizedStoriesEntries = normalizeStories(main?.stories as StoriesEntry[], { configDir, workingDir, }).map((specifier) => ({ diff --git a/src/util/getTestRunnerConfig.ts b/src/util/getTestRunnerConfig.ts index fc4b33c9..78a28c18 100644 --- a/src/util/getTestRunnerConfig.ts +++ b/src/util/getTestRunnerConfig.ts @@ -6,7 +6,7 @@ let testRunnerConfig: TestRunnerConfig; let loaded = false; export const getTestRunnerConfig = ( - configDir: string = process.env.STORYBOOK_CONFIG_DIR + configDir = process.env.STORYBOOK_CONFIG_DIR || '' ): TestRunnerConfig | undefined => { // testRunnerConfig can be undefined if (loaded) { diff --git a/tsconfig.json b/tsconfig.json index 9637798a..ddc1f0da 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,9 @@ "skipLibCheck": true, "target": "es2020", "types": ["jest", "node"], - "moduleResolution": "node" + "moduleResolution": "node", + "strict": true, + "noEmit": true, }, "include": ["src/**/*.ts"], "exclude": ["src/**/*.test.ts"] From 5cbce315285719e577355d8c0041fb25cd577d4e Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:10:05 -0500 Subject: [PATCH 02/16] Name ci job --- .github/workflows/typecheck.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index ee76f474..51e5b163 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -1,3 +1,5 @@ +name: Typecheck + on: [push, pull_request] jobs: From 3d386a747219a9a6992d4af8d80112a97a7513c4 Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:18:07 -0500 Subject: [PATCH 03/16] Add new tests --- src/util/getCliOptions.test.ts | 37 ++++++++++++++ src/util/getParsedCliOptions.test.ts | 73 ++++++++++++++++++++++++++ src/util/getTestRunnerConfig.test.ts | 76 ++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 src/util/getParsedCliOptions.test.ts create mode 100644 src/util/getTestRunnerConfig.test.ts diff --git a/src/util/getCliOptions.test.ts b/src/util/getCliOptions.test.ts index e9e663a0..befd193b 100644 --- a/src/util/getCliOptions.test.ts +++ b/src/util/getCliOptions.test.ts @@ -17,6 +17,16 @@ describe('getCliOptions', () => { expect(opts.runnerOptions).toMatchObject(customConfig); }); + it('returns default options if no options are passed', () => { + jest.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue({ options: {}, extraArgs: [] }); + const opts = getCliOptions(); + const jestOptions = opts.jestOptions.length > 0 ? ['--coverage'] : []; + expect(opts).toEqual({ + runnerOptions: {}, + jestOptions, + }); + }); + it('returns failOnConsole option if passed', () => { const customConfig = { failOnConsole: true }; jest @@ -26,6 +36,33 @@ describe('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: [] }); + 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: [] }); + const opts = getCliOptions(); + expect(opts).toEqual({ jestOptions: [], runnerOptions: { url: 'http://localhost:3000' } }); + }); + + it('handles extra arguments correctly', () => { + jest.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue({ + options: { version: true, cache: false, coverageDirectory: './test' }, + extraArgs: ['--watch', '--coverage'], + }); + const opts = getCliOptions(); + expect(opts.jestOptions).toEqual(['--version', '--no-cache', '--watch', '--coverage']); + }); + it('returns extra args if passed', () => { const extraArgs = ['TestName', 'AnotherTestName']; // mock argv to avoid side effect from running tests e.g. jest --coverage, diff --git a/src/util/getParsedCliOptions.test.ts b/src/util/getParsedCliOptions.test.ts new file mode 100644 index 00000000..3ee51258 --- /dev/null +++ b/src/util/getParsedCliOptions.test.ts @@ -0,0 +1,73 @@ +import { program } from 'commander'; +import { getParsedCliOptions } from './getParsedCliOptions'; + +describe('getParsedCliOptions', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should return the parsed CLI options', () => { + const parsedCliOptions = getParsedCliOptions(); + const coverageEnabled = parsedCliOptions.options.coverage ?? false; + if (coverageEnabled) { + expect(parsedCliOptions).toEqual({ + options: { + indexJson: undefined, + configDir: '.storybook', + coverageDirectory: 'coverage/storybook', + watch: false, + browsers: ['chromium'], + url: 'http://127.0.0.1:6006', + cache: true, + coverage: true, + }, + extraArgs: [], + }); + } else { + expect(parsedCliOptions).toEqual({ + options: { + indexJson: undefined, + configDir: '.storybook', + coverageDirectory: 'coverage/storybook', + watch: false, + browsers: ['chromium'], + url: 'http://127.0.0.1:6006', + cache: true, + }, + extraArgs: [], + }); + } + }); + + it('should handle unknown options', () => { + const originalWarn = console.warn; + console.warn = jest.fn(); + + const originalExit = process.exit; + process.exit = jest.fn() as any; + + const argv = process.argv.slice(); + process.argv.push('--unknown-option'); + + expect(() => { + getParsedCliOptions(); + }).toThrow(); + + expect(console.warn).toHaveBeenCalled(); + expect(process.exit).toHaveBeenCalledWith(1); + + process.argv = argv; + console.warn = originalWarn; + process.exit = originalExit; + }); + + it('handles unknown options correctly', () => { + jest.spyOn(program, 'parse').mockImplementation(() => { + throw new Error('Unknown error'); + }); + + expect(() => { + getParsedCliOptions(); + }).toThrow(Error); + }); +}); diff --git a/src/util/getTestRunnerConfig.test.ts b/src/util/getTestRunnerConfig.test.ts new file mode 100644 index 00000000..a59b9447 --- /dev/null +++ b/src/util/getTestRunnerConfig.test.ts @@ -0,0 +1,76 @@ +import { serverRequire } from '@storybook/core-common'; +import { TestRunnerConfig } from '../playwright/hooks'; +import { getTestRunnerConfig, loaded } from './getTestRunnerConfig'; +import { join, resolve } from 'path'; + +const testRunnerConfig: TestRunnerConfig = { + setup: () => { + console.log('Running setup'); + }, + preRender: async (page) => { + console.log('Running preRender'); + await page.goto('https://example.com'); + }, + postRender: async (page) => { + console.log('Running postRender'); + const title = await page.title(); + console.log(`Page title: ${title}`); + }, + getHttpHeaders: async (url: string) => { + console.log(`Getting http headers for ${url}`); + const headers = { Authorization: 'Bearer token' }; + return headers; + }, + prepare: async ({ page }) => { + console.log('Preparing browser'); + await page.setViewportSize({ width: 1920, height: 1080 }); + }, +}; + +jest.mock('@storybook/core-common', () => ({ + serverRequire: jest.fn(), +})); + +describe('getTestRunnerConfig', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should load the test runner config', () => { + const configDir = '.storybook'; + (require('@storybook/core-common').serverRequire as jest.Mock).mockReturnValueOnce( + testRunnerConfig + ); + + const result = getTestRunnerConfig(configDir); + console.log(result); + + expect(result).toEqual(testRunnerConfig); + expect(require('@storybook/core-common').serverRequire).toHaveBeenCalledWith( + join(resolve('.storybook', 'test-runner')) + ); + }); + + it('should cache the test runner config', () => { + const configDir = '.storybook'; + (require('@storybook/core-common').serverRequire as jest.Mock).mockReturnValueOnce( + testRunnerConfig + ); + + const result1 = getTestRunnerConfig(configDir); + const result2 = getTestRunnerConfig(configDir); + + expect(result1).toEqual(testRunnerConfig); + expect(result2).toEqual(testRunnerConfig); + }); + + it('should load the test runner config with default configDir', () => { + process.env.STORYBOOK_CONFIG_DIR = '.storybook'; + const result = getTestRunnerConfig(); + expect(result).toEqual(testRunnerConfig); + }); + + afterEach(() => { + delete process.env.STORYBOOK_CONFIG_DIR; + }); +}); From cc15dc7ab9dae9d1cead11a014c37a6d00ef1c6d Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:35:47 -0500 Subject: [PATCH 04/16] Add new test --- .../__snapshots__/transformCsf.test.ts.snap | 1316 +++++++++++++++++ src/csf/transformCsf.test.ts | 133 ++ src/csf/transformCsf.ts | 2 +- 3 files changed, 1450 insertions(+), 1 deletion(-) create mode 100644 src/csf/__snapshots__/transformCsf.test.ts.snap create mode 100644 src/csf/transformCsf.test.ts diff --git a/src/csf/__snapshots__/transformCsf.test.ts.snap b/src/csf/__snapshots__/transformCsf.test.ts.snap new file mode 100644 index 00000000..fb35c604 --- /dev/null +++ b/src/csf/__snapshots__/transformCsf.test.ts.snap @@ -0,0 +1,1316 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`prefixFunction returns null literal if testPrefixer returns undefined 1`] = ` +{ + "async": true, + "body": { + "body": [ + { + "declarations": [ + { + "id": { + "loc": undefined, + "name": "testFn", + "type": "Identifier", + }, + "init": { + "async": true, + "body": { + "body": [ + { + "declarations": [ + { + "id": { + "loc": undefined, + "name": "context", + "type": "Identifier", + }, + "init": { + "loc": undefined, + "properties": [ + { + "computed": false, + "key": { + "loc": undefined, + "name": "id", + "type": "Identifier", + }, + "loc": undefined, + "shorthand": false, + "type": "ObjectProperty", + "value": { + "type": "StringLiteral", + "value": "title--key", + }, + }, + { + "computed": false, + "key": { + "loc": undefined, + "name": "title", + "type": "Identifier", + }, + "loc": undefined, + "shorthand": false, + "type": "ObjectProperty", + "value": { + "type": "StringLiteral", + "value": "title", + }, + }, + { + "computed": false, + "key": { + "loc": undefined, + "name": "name", + "type": "Identifier", + }, + "loc": undefined, + "shorthand": false, + "type": "ObjectProperty", + "value": { + "type": "StringLiteral", + "value": "Key", + }, + }, + ], + "type": "ObjectExpression", + }, + "loc": undefined, + "type": "VariableDeclarator", + }, + ], + "kind": "const", + "loc": undefined, + "type": "VariableDeclaration", + }, + { + "expression": { + "arguments": [ + { + "extra": { + "raw": "'pageerror'", + "rawValue": "pageerror", + }, + "loc": undefined, + "type": "StringLiteral", + "value": "pageerror", + }, + { + "async": false, + "body": { + "body": [ + { + "expression": { + "arguments": [ + { + "async": false, + "body": { + "arguments": [ + { + "loc": undefined, + "name": "id", + "type": "Identifier", + }, + { + "loc": undefined, + "name": "err", + "type": "Identifier", + }, + ], + "callee": { + "loc": undefined, + "name": "__throwError", + "type": "Identifier", + }, + "loc": undefined, + "type": "CallExpression", + }, + "generator": false, + "loc": undefined, + "params": [ + { + "loc": undefined, + "properties": [ + { + "computed": false, + "extra": { + "shorthand": true, + }, + "key": { + "loc": undefined, + "name": "id", + "type": "Identifier", + }, + "loc": undefined, + "shorthand": true, + "type": "ObjectProperty", + "value": { + "extra": {}, + "loc": undefined, + "name": "id", + "type": "Identifier", + }, + }, + { + "computed": false, + "extra": { + "shorthand": true, + }, + "key": { + "loc": undefined, + "name": "err", + "type": "Identifier", + }, + "loc": undefined, + "shorthand": true, + "type": "ObjectProperty", + "value": { + "extra": {}, + "loc": undefined, + "name": "err", + "type": "Identifier", + }, + }, + ], + "type": "ObjectPattern", + }, + ], + "type": "ArrowFunctionExpression", + }, + { + "loc": undefined, + "properties": [ + { + "computed": false, + "key": { + "loc": undefined, + "name": "id", + "type": "Identifier", + }, + "loc": undefined, + "shorthand": false, + "type": "ObjectProperty", + "value": { + "type": "StringLiteral", + "value": "title--key", + }, + }, + { + "computed": false, + "key": { + "loc": undefined, + "name": "err", + "type": "Identifier", + }, + "loc": undefined, + "shorthand": false, + "type": "ObjectProperty", + "value": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "err", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "message", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + }, + ], + "type": "ObjectExpression", + }, + ], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "page", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "evaluate", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "ExpressionStatement", + }, + ], + "directives": [], + "loc": undefined, + "type": "BlockStatement", + }, + "generator": false, + "loc": undefined, + "params": [ + { + "loc": undefined, + "name": "err", + "type": "Identifier", + }, + ], + "type": "ArrowFunctionExpression", + }, + ], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "page", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "on", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "ExpressionStatement", + }, + { + "alternate": null, + "consequent": { + "body": [ + { + "expression": { + "argument": { + "arguments": [ + { + "loc": undefined, + "name": "page", + "type": "Identifier", + }, + { + "loc": undefined, + "name": "context", + "type": "Identifier", + }, + ], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "globalThis", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "__sbPreRender", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "AwaitExpression", + }, + "loc": undefined, + "type": "ExpressionStatement", + }, + ], + "directives": [], + "loc": undefined, + "type": "BlockStatement", + }, + "loc": undefined, + "test": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "globalThis", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "__sbPreRender", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "type": "IfStatement", + }, + { + "declarations": [ + { + "id": { + "loc": undefined, + "name": "result", + "type": "Identifier", + }, + "init": { + "argument": { + "arguments": [ + { + "async": false, + "body": { + "arguments": [ + { + "loc": undefined, + "name": "id", + "type": "Identifier", + }, + { + "loc": undefined, + "name": "hasPlayFn", + "type": "Identifier", + }, + ], + "callee": { + "loc": undefined, + "name": "__test", + "type": "Identifier", + }, + "loc": undefined, + "type": "CallExpression", + }, + "generator": false, + "loc": undefined, + "params": [ + { + "loc": undefined, + "properties": [ + { + "computed": false, + "extra": { + "shorthand": true, + }, + "key": { + "loc": undefined, + "name": "id", + "type": "Identifier", + }, + "loc": undefined, + "shorthand": true, + "type": "ObjectProperty", + "value": { + "extra": {}, + "loc": undefined, + "name": "id", + "type": "Identifier", + }, + }, + { + "computed": false, + "extra": { + "shorthand": true, + }, + "key": { + "loc": undefined, + "name": "hasPlayFn", + "type": "Identifier", + }, + "loc": undefined, + "shorthand": true, + "type": "ObjectProperty", + "value": { + "extra": {}, + "loc": undefined, + "name": "hasPlayFn", + "type": "Identifier", + }, + }, + ], + "type": "ObjectPattern", + }, + ], + "type": "ArrowFunctionExpression", + }, + { + "extra": { + "trailingComma": 618, + }, + "loc": undefined, + "properties": [ + { + "computed": false, + "key": { + "loc": undefined, + "name": "id", + "type": "Identifier", + }, + "loc": undefined, + "shorthand": false, + "type": "ObjectProperty", + "value": { + "type": "StringLiteral", + "value": "title--key", + }, + }, + ], + "type": "ObjectExpression", + }, + ], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "page", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "evaluate", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "AwaitExpression", + }, + "loc": undefined, + "type": "VariableDeclarator", + }, + ], + "kind": "const", + "loc": undefined, + "type": "VariableDeclaration", + }, + { + "alternate": null, + "consequent": { + "body": [ + { + "expression": { + "argument": { + "arguments": [ + { + "loc": undefined, + "name": "page", + "type": "Identifier", + }, + { + "loc": undefined, + "name": "context", + "type": "Identifier", + }, + ], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "globalThis", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "__sbPostRender", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "AwaitExpression", + }, + "loc": undefined, + "type": "ExpressionStatement", + }, + ], + "directives": [], + "loc": undefined, + "type": "BlockStatement", + }, + "loc": undefined, + "test": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "globalThis", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "__sbPostRender", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "type": "IfStatement", + }, + { + "alternate": null, + "consequent": { + "body": [ + { + "declarations": [ + { + "id": { + "loc": undefined, + "name": "isCoverageSetupCorrectly", + "type": "Identifier", + }, + "init": { + "argument": { + "arguments": [ + { + "async": false, + "body": { + "left": { + "extra": { + "raw": "'__coverage__'", + "rawValue": "__coverage__", + }, + "loc": undefined, + "type": "StringLiteral", + "value": "__coverage__", + }, + "loc": undefined, + "operator": "in", + "right": { + "loc": undefined, + "name": "window", + "type": "Identifier", + }, + "type": "BinaryExpression", + }, + "generator": false, + "loc": undefined, + "params": [], + "type": "ArrowFunctionExpression", + }, + ], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "page", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "evaluate", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "AwaitExpression", + }, + "loc": undefined, + "type": "VariableDeclarator", + }, + ], + "kind": "const", + "loc": undefined, + "type": "VariableDeclaration", + }, + { + "alternate": null, + "consequent": { + "body": [ + { + "argument": { + "arguments": [ + { + "expressions": [], + "loc": undefined, + "quasis": [ + { + "loc": undefined, + "tail": true, + "type": "TemplateElement", + "value": { + "cooked": "[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", + "raw": "[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", + }, + }, + ], + "type": "TemplateLiteral", + }, + ], + "callee": { + "loc": undefined, + "name": "Error", + "type": "Identifier", + }, + "loc": undefined, + "type": "NewExpression", + }, + "loc": undefined, + "type": "ThrowStatement", + }, + ], + "directives": [], + "loc": undefined, + "type": "BlockStatement", + }, + "loc": undefined, + "test": { + "argument": { + "loc": undefined, + "name": "isCoverageSetupCorrectly", + "type": "Identifier", + }, + "loc": undefined, + "operator": "!", + "prefix": true, + "type": "UnaryExpression", + }, + "type": "IfStatement", + }, + { + "expression": { + "argument": { + "arguments": [ + { + "loc": undefined, + "name": "page", + "type": "Identifier", + }, + ], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "jestPlaywright", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "saveCoverage", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "AwaitExpression", + }, + "loc": undefined, + "type": "ExpressionStatement", + }, + ], + "directives": [], + "loc": undefined, + "type": "BlockStatement", + }, + "loc": undefined, + "test": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "globalThis", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "__sbCollectCoverage", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "type": "IfStatement", + }, + { + "argument": { + "loc": undefined, + "name": "result", + "type": "Identifier", + }, + "loc": undefined, + "type": "ReturnStatement", + }, + ], + "directives": [], + "loc": undefined, + "type": "BlockStatement", + }, + "generator": false, + "loc": undefined, + "params": [], + "type": "ArrowFunctionExpression", + }, + "loc": undefined, + "type": "VariableDeclarator", + }, + ], + "kind": "const", + "loc": undefined, + "type": "VariableDeclaration", + }, + { + "block": { + "body": [ + { + "expression": { + "argument": { + "arguments": [], + "callee": { + "loc": undefined, + "name": "testFn", + "type": "Identifier", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "AwaitExpression", + }, + "loc": undefined, + "type": "ExpressionStatement", + }, + ], + "directives": [], + "loc": undefined, + "type": "BlockStatement", + }, + "finalizer": null, + "handler": { + "body": { + "body": [ + { + "alternate": { + "body": [ + { + "argument": { + "loc": undefined, + "name": "err", + "type": "Identifier", + }, + "loc": undefined, + "type": "ThrowStatement", + }, + ], + "directives": [], + "loc": undefined, + "type": "BlockStatement", + }, + "consequent": { + "body": [ + { + "expression": { + "arguments": [ + { + "expressions": [ + { + "type": "StringLiteral", + "value": "title", + }, + { + "type": "StringLiteral", + "value": "Key", + }, + ], + "loc": undefined, + "quasis": [ + { + "loc": undefined, + "tail": false, + "type": "TemplateElement", + "value": { + "cooked": "An error occurred in the following story, most likely because of a navigation: "", + "raw": "An error occurred in the following story, most likely because of a navigation: "", + }, + }, + { + "loc": undefined, + "tail": false, + "type": "TemplateElement", + "value": { + "cooked": "/", + "raw": "/", + }, + }, + { + "loc": undefined, + "tail": true, + "type": "TemplateElement", + "value": { + "cooked": "". Retrying...", + "raw": "". Retrying...", + }, + }, + ], + "type": "TemplateLiteral", + }, + ], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "console", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "log", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "ExpressionStatement", + }, + { + "expression": { + "argument": { + "arguments": [], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "jestPlaywright", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "resetPage", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "AwaitExpression", + }, + "loc": undefined, + "type": "ExpressionStatement", + }, + { + "expression": { + "argument": { + "arguments": [ + { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "globalThis", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "page", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "globalThis", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "context", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + ], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "globalThis", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "__sbSetupPage", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "AwaitExpression", + }, + "loc": undefined, + "type": "ExpressionStatement", + }, + { + "expression": { + "argument": { + "arguments": [], + "callee": { + "loc": undefined, + "name": "testFn", + "type": "Identifier", + }, + "loc": undefined, + "type": "CallExpression", + }, + "loc": undefined, + "type": "AwaitExpression", + }, + "loc": undefined, + "type": "ExpressionStatement", + }, + ], + "directives": [], + "loc": undefined, + "type": "BlockStatement", + }, + "loc": undefined, + "test": { + "arguments": [ + { + "extra": { + "raw": "'Execution context was destroyed'", + "rawValue": "Execution context was destroyed", + }, + "loc": undefined, + "type": "StringLiteral", + "value": "Execution context was destroyed", + }, + ], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "arguments": [], + "callee": { + "computed": false, + "loc": undefined, + "object": { + "loc": undefined, + "name": "err", + "type": "Identifier", + }, + "property": { + "loc": undefined, + "name": "toString", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "property": { + "loc": undefined, + "name": "includes", + "type": "Identifier", + }, + "type": "MemberExpression", + }, + "loc": undefined, + "type": "CallExpression", + }, + "type": "IfStatement", + }, + ], + "directives": [], + "loc": undefined, + "type": "BlockStatement", + }, + "loc": undefined, + "param": { + "loc": undefined, + "name": "err", + "type": "Identifier", + }, + "type": "CatchClause", + }, + "loc": undefined, + "type": "TryStatement", + }, + ], + "directives": [], + "loc": undefined, + "type": "BlockStatement", + }, + "generator": false, + "loc": undefined, + "params": [], + "type": "ArrowFunctionExpression", +} +`; + +exports[`transformCsf calls the beforeEachPrefixer function once 1`] = ` +" + export default { + title: 'Button', + parameters: { + play: { + steps: [ + { id: 'step1', action: 'click', target: 'button' }, + { id: 'step2', action: 'click', target: 'button' }, + ], + }, + }, + }; + 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" + }; + page.on('pageerror', err => { + page.evaluate(({ + id, + err + }) => __throwError(id, err), { + id: "button--primary", + err: err.message + }); + }); + if (globalThis.__sbPreRender) { + await globalThis.__sbPreRender(page, context); + } + const result = await page.evaluate(({ + id, + hasPlayFn + }) => __test(id, hasPlayFn), { + id: "button--primary" + }); + if (globalThis.__sbPostRender) { + await globalThis.__sbPostRender(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`] = ` +" + export default { + title: 'Button', + parameters: { + play: { + steps: [ + { id: 'step1', action: 'click', target: 'button' }, + { id: 'step2', action: 'click', target: 'button' }, + ], + }, + }, + }; + 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" + }; + page.on('pageerror', err => { + page.evaluate(({ + id, + err + }) => __throwError(id, err), { + id: "button--primary", + err: err.message + }); + }); + if (globalThis.__sbPreRender) { + await globalThis.__sbPreRender(page, context); + } + const result = await page.evaluate(({ + id, + hasPlayFn + }) => __test(id, hasPlayFn), { + id: "button--primary" + }); + if (globalThis.__sbPostRender) { + await globalThis.__sbPostRender(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`] = ` +" +if (!require.main) { + describe("Button", () => { + describe("Primary", () => { + it("smoke-test", async () => { + const testFn = async () => { + const context = { + id: "button--primary", + title: "Button", + name: "Primary" + }; + page.on('pageerror', err => { + page.evaluate(({ + id, + err + }) => __throwError(id, err), { + id: "button--primary", + err: err.message + }); + }); + if (globalThis.__sbPreRender) { + await globalThis.__sbPreRender(page, context); + } + const result = await page.evaluate(({ + id, + hasPlayFn + }) => __test(id, hasPlayFn), { + id: "button--primary" + }); + if (globalThis.__sbPostRender) { + await globalThis.__sbPostRender(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`] = ` +" + export default { + title: 'Button', + parameters: { + play: { + steps: [ + { id: 'step1', action: 'click', target: 'button' }, + { id: 'step2', action: 'click', target: 'button' }, + ], + }, + }, + }; + export const Primary = () => ''; + + +if (!require.main) { + describe("Button", () => { + beforeEach(beforeEach(() => { + console.log("beforeEach called"); + })); + describe("Primary", () => { + it("smoke-test", null); + }); + }); +}" +`; diff --git a/src/csf/transformCsf.test.ts b/src/csf/transformCsf.test.ts new file mode 100644 index 00000000..0326ed30 --- /dev/null +++ b/src/csf/transformCsf.test.ts @@ -0,0 +1,133 @@ +import { prefixFunction, transformCsf } from './transformCsf'; +import { testPrefixer } from '../playwright/transformPlaywright'; +import template from '@babel/template'; +import * as t from '@babel/types'; + +describe('transformCsf', () => { + it('inserts a no-op test if there are no stories', () => { + const csfCode = ` + export default { + title: 'Button', + }; + `; + const expectedCode = `describe('Button', () => { it('no-op', () => {}) });`; + + const result = transformCsf(csfCode, { insertTestIfEmpty: true }); + + expect(result).toEqual(expectedCode); + }); + + it('calls the testPrefixer function for each test', () => { + const csfCode = ` + export default { + title: 'Button', + parameters: { + play: { + steps: [ + { id: 'step1', action: 'click', target: 'button' }, + { id: 'step2', action: 'click', target: 'button' }, + ], + }, + }, + }; + export const Primary = () => ''; + `; + + const result = transformCsf(csfCode, { testPrefixer }); + + expect(result).toMatchSnapshot(); + }); + + it('calls the beforeEachPrefixer function once', () => { + const csfCode = ` + export default { + title: 'Button', + parameters: { + play: { + steps: [ + { id: 'step1', action: 'click', target: 'button' }, + { id: 'step2', action: 'click', target: 'button' }, + ], + }, + }, + }; + export const Primary = () => ''; + `; + const result = transformCsf(csfCode, { testPrefixer, beforeEachPrefixer: undefined }); + + expect(result).toMatchSnapshot(); + }); + + it('clears the body if clearBody option is true', () => { + const csfCode = ` + export default { + title: 'Button', + parameters: { + play: { + steps: [ + { id: 'step1', action: 'click', target: 'button' }, + { id: 'step2', action: 'click', target: 'button' }, + ], + }, + }, + }; + export const Primary = () => ''; + `; + + const result = transformCsf(csfCode, { testPrefixer, clearBody: true }); + + expect(result).toMatchSnapshot(); + }); + + it('executes beforeEach code before each test', () => { + const code = ` + export default { + title: 'Button', + parameters: { + play: { + steps: [ + { id: 'step1', action: 'click', target: 'button' }, + { id: 'step2', action: 'click', target: 'button' }, + ], + }, + }, + }; + export const Primary = () => ''; + `; + const beforeEachPrefixer = () => { + const logStatement = template.expression`console.log("beforeEach called")`; + const beforeEachBlock = template.statement`beforeEach(() => { ${logStatement()} })`; + return beforeEachBlock(); + }; + const result = transformCsf(code, { beforeEachPrefixer }); + + expect(result).toMatchSnapshot(); + }); +}); + +describe('prefixFunction', () => { + it('returns input expression if testPrefixer is not provided', () => { + const key = 'key'; + const title = 'title'; + const input = t.identifier('input'); + const result = prefixFunction(key, title, input); + expect(result).toEqual(input); + }); + + it('returns null literal if testPrefixer returns undefined', () => { + const key = 'key'; + const title = 'title'; + const input = t.identifier('input'); + const result = prefixFunction(key, title, input, testPrefixer); + expect(result).toMatchSnapshot(); + }); + + it('returns expression from testPrefixer if it returns a valid expression', () => { + const key = 'key'; + const title = 'title'; + const input = t.identifier('input'); + const testPrefixer = () => t.expressionStatement(t.identifier('prefix')); + const result = prefixFunction(key, title, input, testPrefixer); + expect(result).toEqual(t.identifier('input')); + }); +}); diff --git a/src/csf/transformCsf.ts b/src/csf/transformCsf.ts index 9c865fe8..113b8a08 100644 --- a/src/csf/transformCsf.ts +++ b/src/csf/transformCsf.ts @@ -23,7 +23,7 @@ interface TransformOptions { makeTitle?: (userTitle: string) => string; } -const prefixFunction = ( +export const prefixFunction = ( key: string, title: string, input: t.Expression, From 44bae0df3d1aed4f702f3c6e4e31d11d04ae4da3 Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:58:22 -0500 Subject: [PATCH 05/16] Modify extra arguments test to improve coverage --- src/util/getCliOptions.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/util/getCliOptions.test.ts b/src/util/getCliOptions.test.ts index befd193b..df52cb7e 100644 --- a/src/util/getCliOptions.test.ts +++ b/src/util/getCliOptions.test.ts @@ -56,11 +56,17 @@ describe('getCliOptions', () => { it('handles extra arguments correctly', () => { jest.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue({ - options: { version: true, cache: false, coverageDirectory: './test' }, + options: { version: true, cache: false, env: 'node' }, extraArgs: ['--watch', '--coverage'], }); const opts = getCliOptions(); - expect(opts.jestOptions).toEqual(['--version', '--no-cache', '--watch', '--coverage']); + expect(opts.jestOptions).toEqual([ + '--version', + '--no-cache', + '--env="node"', + '--watch', + '--coverage', + ]); }); it('returns extra args if passed', () => { From d8f698cacf00d858539228ea200788891d9d69d9 Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:56:29 -0500 Subject: [PATCH 06/16] feat: Add Jest Playwright configuration and test file - Added Jest configuration file (jest.config.js) to project root. - Created a new test file (jest-playwright.test.ts) in the src/config directory. - This commit introduces the necessary setup for running tests using Jest Playwright. - The Jest configuration file allows customization of Jest's behavior. - The new test file will be used for writing tests using the Jest Playwright library. - This addition enhances the project's testing capabilities and enables UI testing with Playwright. --- jest.config.js | 8 ++ src/config/jest-playwright.test.ts | 114 +++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 src/config/jest-playwright.test.ts diff --git a/jest.config.js b/jest.config.js index 851b11be..fec48d9a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,11 @@ module.exports = { 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/custom-environment': + '/playwright/custom-environment', + '@storybook/test-runner/playwright/jest-setup': '/playwright/jest-setup', + '@storybook/test-runner/playwright/transform': '/playwright/transform', + }, }; diff --git a/src/config/jest-playwright.test.ts b/src/config/jest-playwright.test.ts new file mode 100644 index 00000000..f221000b --- /dev/null +++ b/src/config/jest-playwright.test.ts @@ -0,0 +1,114 @@ +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, + }, + }, + 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'], + }); + }); +}); From 8b3c664565ed402782c2e913a2fa7100344d7983 Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Fri, 10 Nov 2023 19:20:08 -0500 Subject: [PATCH 07/16] test: add hooks test file This commit adds a new test file for testing the hooks functionality in Playwright. --- src/playwright/hooks.test.ts | 117 +++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/playwright/hooks.test.ts diff --git a/src/playwright/hooks.test.ts b/src/playwright/hooks.test.ts new file mode 100644 index 00000000..8f183e54 --- /dev/null +++ b/src/playwright/hooks.test.ts @@ -0,0 +1,117 @@ +import { Page } from 'playwright-core'; +import { + getStoryContext, + setPreRender, + setPostRender, + TestRunnerConfig, + waitForPageReady, +} from './hooks'; + +type MockPage = Page & { evaluate: jest.Mock }; + +describe('test-runner', () => { + describe('setPreRender', () => { + it('sets the preRender function', () => { + const preRender = jest.fn(); + setPreRender(preRender); + expect(globalThis.__sbPreRender).toBe(preRender); + }); + }); + + describe('setPostRender', () => { + it('sets the postRender function', () => { + const postRender = jest.fn(); + setPostRender(postRender); + expect(globalThis.__sbPostRender).toBe(postRender); + }); + }); + + describe('getStoryContext', () => { + const page = { + evaluate: jest.fn(), + } as MockPage; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls page.evaluate with the correct arguments', async () => { + const context = { id: 'id', title: 'title', name: 'name' }; + await getStoryContext(page, context); + expect(page.evaluate).toHaveBeenCalledWith(expect.any(Function), { storyId: context.id }); + }); + + it('returns the result of page.evaluate', async () => { + const context = { id: 'id', title: 'title', name: 'name' }; + const storyContext = { kind: 'kind', name: 'name' }; + page.evaluate.mockResolvedValueOnce(storyContext); + const result = await getStoryContext(page, context); + expect(result).toBe(storyContext); + }); + + it('calls globalThis.__getContext with the correct storyId', async () => { + const context = { id: 'id', title: 'title', name: 'name' }; + const storyContext = { kind: 'kind', name: 'name' }; + + // Mock globalThis.__getContext + globalThis.__getContext = jest.fn(); + + page.evaluate.mockImplementation(async (func) => { + // Call the function passed to page.evaluate + func({ storyId: context.id }); + return storyContext; + }); + + await getStoryContext(page, context); + + // Check that globalThis.__getContext was called with the correct storyId + expect(globalThis.__getContext).toHaveBeenCalledWith(context.id); + }); + }); + + describe('TestRunnerConfig', () => { + it('has the correct properties', () => { + const config: TestRunnerConfig = {}; + expect(config).toMatchObject({}); + }); + }); + + describe('waitForPageReady', () => { + let page: Page; + + beforeEach(() => { + page = { + waitForLoadState: jest.fn(), + evaluate: jest.fn(), + } as unknown as Page; + }); + + it('waits for the page to be ready', async () => { + await waitForPageReady(page); + expect(page.waitForLoadState).toHaveBeenCalledWith('domcontentloaded'); + expect(page.waitForLoadState).toHaveBeenCalledWith('load'); + expect(page.waitForLoadState).toHaveBeenCalledWith('networkidle'); + expect(page.evaluate).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('calls page.evaluate with () => document.fonts.ready', async () => { + const page = { + waitForLoadState: jest.fn(), + evaluate: jest.fn(), + } as unknown as MockPage; + + // Mock document.fonts.ready + globalThis.document = { + fonts: { + ready: 'ready', + }, + } as unknown as Document; + await waitForPageReady(page); + + expect(page.evaluate).toHaveBeenCalledWith(expect.any(Function)); + const evaluateFn = page.evaluate.mock.calls[0][0]; + const mockDocument = { fonts: { ready: 'ready' } }; + expect(evaluateFn(mockDocument)).toBe('ready'); + }); + }); +}); From a34d9e8d827fcb35ee4d5a2ee646a660cd812dee Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Thu, 16 Nov 2023 12:58:13 -0500 Subject: [PATCH 08/16] Fix tests --- src/csf/transformCsf.test.ts | 2 +- src/csf/transformCsf.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/csf/transformCsf.test.ts b/src/csf/transformCsf.test.ts index 0326ed30..ab0e88dd 100644 --- a/src/csf/transformCsf.test.ts +++ b/src/csf/transformCsf.test.ts @@ -10,7 +10,7 @@ describe('transformCsf', () => { title: 'Button', }; `; - const expectedCode = `describe('Button', () => { it('no-op', () => {}) });`; + const expectedCode = `describe.skip('Button', () => { it('no-op', () => {}) });`; const result = transformCsf(csfCode, { insertTestIfEmpty: true }); diff --git a/src/csf/transformCsf.ts b/src/csf/transformCsf.ts index 5ea715c2..062ec936 100644 --- a/src/csf/transformCsf.ts +++ b/src/csf/transformCsf.ts @@ -115,7 +115,7 @@ export const transformCsf = ( ) => { const { includeTags, excludeTags, skipTags } = getTagOptions(); - const csf = loadCsf(code, { makeTitle }); + const csf = loadCsf(code, { makeTitle: makeTitle || ((userTitle: string) => userTitle) }); csf.parse(); const storyExports = Object.keys(csf._stories); From 1e3e56405abd77dac1ff24051b1b6476ddb8dc3c Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:10:01 -0500 Subject: [PATCH 09/16] Fix types --- src/csf/transformCsf.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/csf/transformCsf.ts b/src/csf/transformCsf.ts index 062ec936..be5f3503 100644 --- a/src/csf/transformCsf.ts +++ b/src/csf/transformCsf.ts @@ -128,7 +128,7 @@ export const transformCsf = ( acc[key].play = annotations.play; } - acc[key].tags = csf._stories[key].tags || csf.meta.tags || []; + acc[key].tags = csf._stories[key].tags || csf.meta?.tags || []; return acc; }, {} as Record); @@ -137,27 +137,32 @@ export const transformCsf = ( // If includeTags is passed, check if the story has any of them - else include by default const isIncluded = includeTags.length === 0 || - includeTags.some((tag) => storyAnnotations[key].tags.includes(tag)); + includeTags.some((tag) => storyAnnotations[key].tags?.includes(tag)); // If excludeTags is passed, check if the story does not have any of them - const isNotExcluded = excludeTags.every((tag) => !storyAnnotations[key].tags.includes(tag)); + const isNotExcluded = excludeTags.every((tag) => !storyAnnotations[key].tags?.includes(tag)); return isIncluded && isNotExcluded; }) .map((key: string) => { let tests: t.Statement[] = []; - const shouldSkip = skipTags.some((tag) => storyAnnotations[key].tags.includes(tag)); + const shouldSkip = skipTags.some((tag) => storyAnnotations[key].tags?.includes(tag)); + const playFunctions = storyAnnotations[key]?.play; if (title) { tests = [ ...tests, - ...makePlayTest({ - key, - title, - metaOrStoryPlay: storyAnnotations[key].play, - testPrefix: testPrefixer, - shouldSkip, - }), - ]; + ...(playFunctions !== undefined + ? [ + makePlayTest({ + key, + title, + metaOrStoryPlay: playFunctions, + testPrefix: testPrefixer, + shouldSkip, + }), + ] + : []), + ].flat(); } if (tests.length) { From de631e42dfbadda93768e77ed9143b3d84cc32de Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:22:44 -0500 Subject: [PATCH 10/16] refactor(transformCsf): Simplify makePlayTest function and remove unnecessary conditional The makePlayTest function in the transformCsf file has been simplified to remove an unnecessary conditional statement. The metaOrStoryPlay parameter is now optional, indicated by the use of the "?" symbol. This change was made to improve code readability and reduce complexity. --- src/csf/transformCsf.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/csf/transformCsf.ts b/src/csf/transformCsf.ts index be5f3503..7a941702 100644 --- a/src/csf/transformCsf.ts +++ b/src/csf/transformCsf.ts @@ -66,7 +66,7 @@ const makePlayTest = ({ }: { key: string; title: string; - metaOrStoryPlay: t.Node; + metaOrStoryPlay?: t.Node; testPrefix?: TestPrefixer; shouldSkip?: boolean; }): t.Statement[] => { @@ -147,22 +147,17 @@ export const transformCsf = ( .map((key: string) => { let tests: t.Statement[] = []; const shouldSkip = skipTags.some((tag) => storyAnnotations[key].tags?.includes(tag)); - const playFunctions = storyAnnotations[key]?.play; if (title) { tests = [ ...tests, - ...(playFunctions !== undefined - ? [ - makePlayTest({ - key, - title, - metaOrStoryPlay: playFunctions, - testPrefix: testPrefixer, - shouldSkip, - }), - ] - : []), - ].flat(); + ...makePlayTest({ + key, + title, + metaOrStoryPlay: storyAnnotations[key]?.play, + testPrefix: testPrefixer, + shouldSkip, + }), + ]; } if (tests.length) { From 7fa52e5d167e9634baec17a0a1fe3dec57f630fd Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:23:03 -0500 Subject: [PATCH 11/16] feat(playwright): add optional tags property to TestRunnerConfig The TestRunnerConfig interface in the hooks.ts file now includes an optional tags property. This property allows users to include, exclude, or skip specific tags when running tests. The tags are defined as annotations in the story or meta. This change provides more flexibility and control over test execution based on specific tags. --- src/playwright/hooks.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/playwright/hooks.ts b/src/playwright/hooks.ts index 05be178a..dcc094ab 100644 --- a/src/playwright/hooks.ts +++ b/src/playwright/hooks.ts @@ -34,10 +34,10 @@ export interface TestRunnerConfig { /** * Tags to include, exclude, or skip. These tags are defined as annotations in your story or meta. */ - tags: { - include: string[]; - exclude: string[]; - skip: string[]; + tags?: { + include?: string[]; + exclude?: string[]; + skip?: string[]; }; } From f80f8704b8115169283bd7e4e1553106c864f50b Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:34:59 -0500 Subject: [PATCH 12/16] build: Update tsup.config.ts - Updated the entry configuration in tsup.config.ts to include all JavaScript and TypeScript files in the src directory, excluding declaration files and test files. - This change was made to ensure that only the necessary source files are included in the build process, improving build performance and reducing bundle size. --- tsup.config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tsup.config.ts b/tsup.config.ts index 007276bb..0e0ae64d 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -3,7 +3,11 @@ import { defineConfig } from 'tsup'; export default defineConfig([ { clean: true, - entry: ['./src/**/!(*.{d,test}).{js,jsx,ts,tsx}'], + entry: [ + './src/**/*.{js,jsx,ts,tsx}', + '!./src/**/*.d.{js,jsx,ts,tsx}', + '!./src/**/*test.{js,jsx,ts,tsx}', + ], format: ['cjs', 'esm'], splitting: false, dts: true, From ef775c1c27bcc62931859f61cb0a9067c5c3ed84 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 21 Nov 2023 11:23:20 +0100 Subject: [PATCH 13/16] fix conflicts and add various fixes --- src/config/jest-playwright.test.ts | 1 + .../__snapshots__/transformCsf.test.ts.snap | 1141 +---------------- src/csf/transformCsf.test.ts | 36 +- src/csf/transformCsf.ts | 39 +- src/playwright/hooks.test.ts | 24 +- src/playwright/transformPlaywright.ts | 2 +- src/playwright/transformPlaywrightJson.ts | 2 +- src/setup-page.ts | 4 +- src/test-storybook.ts | 2 +- .../getStorybookMain.test.ts.snap | 6 +- src/util/getCliOptions.test.ts | 5 +- src/util/getCliOptions.ts | 19 +- src/util/getStorybookMain.ts | 8 +- src/util/getStorybookMetadata.ts | 6 +- src/util/getTestRunnerConfig.test.ts | 4 +- src/util/getTestRunnerConfig.ts | 2 +- 16 files changed, 88 insertions(+), 1213 deletions(-) diff --git a/src/config/jest-playwright.test.ts b/src/config/jest-playwright.test.ts index f221000b..b382a6da 100644 --- a/src/config/jest-playwright.test.ts +++ b/src/config/jest-playwright.test.ts @@ -20,6 +20,7 @@ describe('getJestConfig', () => { 'jest-playwright': { browsers: undefined, collectCoverage: false, + exitOnPageError: false, }, }, watchPlugins: [ diff --git a/src/csf/__snapshots__/transformCsf.test.ts.snap b/src/csf/__snapshots__/transformCsf.test.ts.snap index fb35c604..5e4d7b32 100644 --- a/src/csf/__snapshots__/transformCsf.test.ts.snap +++ b/src/csf/__snapshots__/transformCsf.test.ts.snap @@ -1,1078 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`prefixFunction returns null literal if testPrefixer returns undefined 1`] = ` -{ - "async": true, - "body": { - "body": [ - { - "declarations": [ - { - "id": { - "loc": undefined, - "name": "testFn", - "type": "Identifier", - }, - "init": { - "async": true, - "body": { - "body": [ - { - "declarations": [ - { - "id": { - "loc": undefined, - "name": "context", - "type": "Identifier", - }, - "init": { - "loc": undefined, - "properties": [ - { - "computed": false, - "key": { - "loc": undefined, - "name": "id", - "type": "Identifier", - }, - "loc": undefined, - "shorthand": false, - "type": "ObjectProperty", - "value": { - "type": "StringLiteral", - "value": "title--key", - }, - }, - { - "computed": false, - "key": { - "loc": undefined, - "name": "title", - "type": "Identifier", - }, - "loc": undefined, - "shorthand": false, - "type": "ObjectProperty", - "value": { - "type": "StringLiteral", - "value": "title", - }, - }, - { - "computed": false, - "key": { - "loc": undefined, - "name": "name", - "type": "Identifier", - }, - "loc": undefined, - "shorthand": false, - "type": "ObjectProperty", - "value": { - "type": "StringLiteral", - "value": "Key", - }, - }, - ], - "type": "ObjectExpression", - }, - "loc": undefined, - "type": "VariableDeclarator", - }, - ], - "kind": "const", - "loc": undefined, - "type": "VariableDeclaration", - }, - { - "expression": { - "arguments": [ - { - "extra": { - "raw": "'pageerror'", - "rawValue": "pageerror", - }, - "loc": undefined, - "type": "StringLiteral", - "value": "pageerror", - }, - { - "async": false, - "body": { - "body": [ - { - "expression": { - "arguments": [ - { - "async": false, - "body": { - "arguments": [ - { - "loc": undefined, - "name": "id", - "type": "Identifier", - }, - { - "loc": undefined, - "name": "err", - "type": "Identifier", - }, - ], - "callee": { - "loc": undefined, - "name": "__throwError", - "type": "Identifier", - }, - "loc": undefined, - "type": "CallExpression", - }, - "generator": false, - "loc": undefined, - "params": [ - { - "loc": undefined, - "properties": [ - { - "computed": false, - "extra": { - "shorthand": true, - }, - "key": { - "loc": undefined, - "name": "id", - "type": "Identifier", - }, - "loc": undefined, - "shorthand": true, - "type": "ObjectProperty", - "value": { - "extra": {}, - "loc": undefined, - "name": "id", - "type": "Identifier", - }, - }, - { - "computed": false, - "extra": { - "shorthand": true, - }, - "key": { - "loc": undefined, - "name": "err", - "type": "Identifier", - }, - "loc": undefined, - "shorthand": true, - "type": "ObjectProperty", - "value": { - "extra": {}, - "loc": undefined, - "name": "err", - "type": "Identifier", - }, - }, - ], - "type": "ObjectPattern", - }, - ], - "type": "ArrowFunctionExpression", - }, - { - "loc": undefined, - "properties": [ - { - "computed": false, - "key": { - "loc": undefined, - "name": "id", - "type": "Identifier", - }, - "loc": undefined, - "shorthand": false, - "type": "ObjectProperty", - "value": { - "type": "StringLiteral", - "value": "title--key", - }, - }, - { - "computed": false, - "key": { - "loc": undefined, - "name": "err", - "type": "Identifier", - }, - "loc": undefined, - "shorthand": false, - "type": "ObjectProperty", - "value": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "err", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "message", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - }, - ], - "type": "ObjectExpression", - }, - ], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "page", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "evaluate", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "ExpressionStatement", - }, - ], - "directives": [], - "loc": undefined, - "type": "BlockStatement", - }, - "generator": false, - "loc": undefined, - "params": [ - { - "loc": undefined, - "name": "err", - "type": "Identifier", - }, - ], - "type": "ArrowFunctionExpression", - }, - ], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "page", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "on", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "ExpressionStatement", - }, - { - "alternate": null, - "consequent": { - "body": [ - { - "expression": { - "argument": { - "arguments": [ - { - "loc": undefined, - "name": "page", - "type": "Identifier", - }, - { - "loc": undefined, - "name": "context", - "type": "Identifier", - }, - ], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "globalThis", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "__sbPreRender", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "AwaitExpression", - }, - "loc": undefined, - "type": "ExpressionStatement", - }, - ], - "directives": [], - "loc": undefined, - "type": "BlockStatement", - }, - "loc": undefined, - "test": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "globalThis", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "__sbPreRender", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "type": "IfStatement", - }, - { - "declarations": [ - { - "id": { - "loc": undefined, - "name": "result", - "type": "Identifier", - }, - "init": { - "argument": { - "arguments": [ - { - "async": false, - "body": { - "arguments": [ - { - "loc": undefined, - "name": "id", - "type": "Identifier", - }, - { - "loc": undefined, - "name": "hasPlayFn", - "type": "Identifier", - }, - ], - "callee": { - "loc": undefined, - "name": "__test", - "type": "Identifier", - }, - "loc": undefined, - "type": "CallExpression", - }, - "generator": false, - "loc": undefined, - "params": [ - { - "loc": undefined, - "properties": [ - { - "computed": false, - "extra": { - "shorthand": true, - }, - "key": { - "loc": undefined, - "name": "id", - "type": "Identifier", - }, - "loc": undefined, - "shorthand": true, - "type": "ObjectProperty", - "value": { - "extra": {}, - "loc": undefined, - "name": "id", - "type": "Identifier", - }, - }, - { - "computed": false, - "extra": { - "shorthand": true, - }, - "key": { - "loc": undefined, - "name": "hasPlayFn", - "type": "Identifier", - }, - "loc": undefined, - "shorthand": true, - "type": "ObjectProperty", - "value": { - "extra": {}, - "loc": undefined, - "name": "hasPlayFn", - "type": "Identifier", - }, - }, - ], - "type": "ObjectPattern", - }, - ], - "type": "ArrowFunctionExpression", - }, - { - "extra": { - "trailingComma": 618, - }, - "loc": undefined, - "properties": [ - { - "computed": false, - "key": { - "loc": undefined, - "name": "id", - "type": "Identifier", - }, - "loc": undefined, - "shorthand": false, - "type": "ObjectProperty", - "value": { - "type": "StringLiteral", - "value": "title--key", - }, - }, - ], - "type": "ObjectExpression", - }, - ], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "page", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "evaluate", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "AwaitExpression", - }, - "loc": undefined, - "type": "VariableDeclarator", - }, - ], - "kind": "const", - "loc": undefined, - "type": "VariableDeclaration", - }, - { - "alternate": null, - "consequent": { - "body": [ - { - "expression": { - "argument": { - "arguments": [ - { - "loc": undefined, - "name": "page", - "type": "Identifier", - }, - { - "loc": undefined, - "name": "context", - "type": "Identifier", - }, - ], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "globalThis", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "__sbPostRender", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "AwaitExpression", - }, - "loc": undefined, - "type": "ExpressionStatement", - }, - ], - "directives": [], - "loc": undefined, - "type": "BlockStatement", - }, - "loc": undefined, - "test": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "globalThis", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "__sbPostRender", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "type": "IfStatement", - }, - { - "alternate": null, - "consequent": { - "body": [ - { - "declarations": [ - { - "id": { - "loc": undefined, - "name": "isCoverageSetupCorrectly", - "type": "Identifier", - }, - "init": { - "argument": { - "arguments": [ - { - "async": false, - "body": { - "left": { - "extra": { - "raw": "'__coverage__'", - "rawValue": "__coverage__", - }, - "loc": undefined, - "type": "StringLiteral", - "value": "__coverage__", - }, - "loc": undefined, - "operator": "in", - "right": { - "loc": undefined, - "name": "window", - "type": "Identifier", - }, - "type": "BinaryExpression", - }, - "generator": false, - "loc": undefined, - "params": [], - "type": "ArrowFunctionExpression", - }, - ], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "page", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "evaluate", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "AwaitExpression", - }, - "loc": undefined, - "type": "VariableDeclarator", - }, - ], - "kind": "const", - "loc": undefined, - "type": "VariableDeclaration", - }, - { - "alternate": null, - "consequent": { - "body": [ - { - "argument": { - "arguments": [ - { - "expressions": [], - "loc": undefined, - "quasis": [ - { - "loc": undefined, - "tail": true, - "type": "TemplateElement", - "value": { - "cooked": "[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", - "raw": "[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", - }, - }, - ], - "type": "TemplateLiteral", - }, - ], - "callee": { - "loc": undefined, - "name": "Error", - "type": "Identifier", - }, - "loc": undefined, - "type": "NewExpression", - }, - "loc": undefined, - "type": "ThrowStatement", - }, - ], - "directives": [], - "loc": undefined, - "type": "BlockStatement", - }, - "loc": undefined, - "test": { - "argument": { - "loc": undefined, - "name": "isCoverageSetupCorrectly", - "type": "Identifier", - }, - "loc": undefined, - "operator": "!", - "prefix": true, - "type": "UnaryExpression", - }, - "type": "IfStatement", - }, - { - "expression": { - "argument": { - "arguments": [ - { - "loc": undefined, - "name": "page", - "type": "Identifier", - }, - ], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "jestPlaywright", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "saveCoverage", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "AwaitExpression", - }, - "loc": undefined, - "type": "ExpressionStatement", - }, - ], - "directives": [], - "loc": undefined, - "type": "BlockStatement", - }, - "loc": undefined, - "test": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "globalThis", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "__sbCollectCoverage", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "type": "IfStatement", - }, - { - "argument": { - "loc": undefined, - "name": "result", - "type": "Identifier", - }, - "loc": undefined, - "type": "ReturnStatement", - }, - ], - "directives": [], - "loc": undefined, - "type": "BlockStatement", - }, - "generator": false, - "loc": undefined, - "params": [], - "type": "ArrowFunctionExpression", - }, - "loc": undefined, - "type": "VariableDeclarator", - }, - ], - "kind": "const", - "loc": undefined, - "type": "VariableDeclaration", - }, - { - "block": { - "body": [ - { - "expression": { - "argument": { - "arguments": [], - "callee": { - "loc": undefined, - "name": "testFn", - "type": "Identifier", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "AwaitExpression", - }, - "loc": undefined, - "type": "ExpressionStatement", - }, - ], - "directives": [], - "loc": undefined, - "type": "BlockStatement", - }, - "finalizer": null, - "handler": { - "body": { - "body": [ - { - "alternate": { - "body": [ - { - "argument": { - "loc": undefined, - "name": "err", - "type": "Identifier", - }, - "loc": undefined, - "type": "ThrowStatement", - }, - ], - "directives": [], - "loc": undefined, - "type": "BlockStatement", - }, - "consequent": { - "body": [ - { - "expression": { - "arguments": [ - { - "expressions": [ - { - "type": "StringLiteral", - "value": "title", - }, - { - "type": "StringLiteral", - "value": "Key", - }, - ], - "loc": undefined, - "quasis": [ - { - "loc": undefined, - "tail": false, - "type": "TemplateElement", - "value": { - "cooked": "An error occurred in the following story, most likely because of a navigation: "", - "raw": "An error occurred in the following story, most likely because of a navigation: "", - }, - }, - { - "loc": undefined, - "tail": false, - "type": "TemplateElement", - "value": { - "cooked": "/", - "raw": "/", - }, - }, - { - "loc": undefined, - "tail": true, - "type": "TemplateElement", - "value": { - "cooked": "". Retrying...", - "raw": "". Retrying...", - }, - }, - ], - "type": "TemplateLiteral", - }, - ], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "console", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "log", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "ExpressionStatement", - }, - { - "expression": { - "argument": { - "arguments": [], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "jestPlaywright", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "resetPage", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "AwaitExpression", - }, - "loc": undefined, - "type": "ExpressionStatement", - }, - { - "expression": { - "argument": { - "arguments": [ - { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "globalThis", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "page", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "globalThis", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "context", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - ], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "globalThis", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "__sbSetupPage", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "AwaitExpression", - }, - "loc": undefined, - "type": "ExpressionStatement", - }, - { - "expression": { - "argument": { - "arguments": [], - "callee": { - "loc": undefined, - "name": "testFn", - "type": "Identifier", - }, - "loc": undefined, - "type": "CallExpression", - }, - "loc": undefined, - "type": "AwaitExpression", - }, - "loc": undefined, - "type": "ExpressionStatement", - }, - ], - "directives": [], - "loc": undefined, - "type": "BlockStatement", - }, - "loc": undefined, - "test": { - "arguments": [ - { - "extra": { - "raw": "'Execution context was destroyed'", - "rawValue": "Execution context was destroyed", - }, - "loc": undefined, - "type": "StringLiteral", - "value": "Execution context was destroyed", - }, - ], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "arguments": [], - "callee": { - "computed": false, - "loc": undefined, - "object": { - "loc": undefined, - "name": "err", - "type": "Identifier", - }, - "property": { - "loc": undefined, - "name": "toString", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "property": { - "loc": undefined, - "name": "includes", - "type": "Identifier", - }, - "type": "MemberExpression", - }, - "loc": undefined, - "type": "CallExpression", - }, - "type": "IfStatement", - }, - ], - "directives": [], - "loc": undefined, - "type": "BlockStatement", - }, - "loc": undefined, - "param": { - "loc": undefined, - "name": "err", - "type": "Identifier", - }, - "type": "CatchClause", - }, - "loc": undefined, - "type": "TryStatement", - }, - ], - "directives": [], - "loc": undefined, - "type": "BlockStatement", - }, - "generator": false, - "loc": undefined, - "params": [], - "type": "ArrowFunctionExpression", -} -`; - exports[`transformCsf calls the beforeEachPrefixer function once 1`] = ` " export default { @@ -1099,17 +26,12 @@ if (!require.main) { title: "Button", name: "Primary" }; - page.on('pageerror', err => { - page.evaluate(({ - id, - err - }) => __throwError(id, err), { - id: "button--primary", - err: err.message - }); - }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + const onPageError = err => { + globalThis.__sbThrowUncaughtPageError(err, context); + }; + page.on('pageerror', onPageError); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -1117,8 +39,8 @@ if (!require.main) { }) => __test(id, hasPlayFn), { id: "button--primary" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -1129,6 +51,7 @@ if (!require.main) { } await jestPlaywright.saveCoverage(page); } + page.off('pageerror', onPageError); return result; }; try { @@ -1175,17 +98,12 @@ if (!require.main) { title: "Button", name: "Primary" }; - page.on('pageerror', err => { - page.evaluate(({ - id, - err - }) => __throwError(id, err), { - id: "button--primary", - err: err.message - }); - }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + const onPageError = err => { + globalThis.__sbThrowUncaughtPageError(err, context); + }; + page.on('pageerror', onPageError); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -1193,8 +111,8 @@ if (!require.main) { }) => __test(id, hasPlayFn), { id: "button--primary" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -1205,6 +123,7 @@ if (!require.main) { } await jestPlaywright.saveCoverage(page); } + page.off('pageerror', onPageError); return result; }; try { @@ -1237,17 +156,12 @@ if (!require.main) { title: "Button", name: "Primary" }; - page.on('pageerror', err => { - page.evaluate(({ - id, - err - }) => __throwError(id, err), { - id: "button--primary", - err: err.message - }); - }); - if (globalThis.__sbPreRender) { - await globalThis.__sbPreRender(page, context); + const onPageError = err => { + globalThis.__sbThrowUncaughtPageError(err, context); + }; + page.on('pageerror', onPageError); + if (globalThis.__sbPreVisit) { + await globalThis.__sbPreVisit(page, context); } const result = await page.evaluate(({ id, @@ -1255,8 +169,8 @@ if (!require.main) { }) => __test(id, hasPlayFn), { id: "button--primary" }); - if (globalThis.__sbPostRender) { - await globalThis.__sbPostRender(page, context); + if (globalThis.__sbPostVisit) { + await globalThis.__sbPostVisit(page, context); } if (globalThis.__sbCollectCoverage) { const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window); @@ -1267,6 +181,7 @@ if (!require.main) { } await jestPlaywright.saveCoverage(page); } + page.off('pageerror', onPageError); return result; }; try { @@ -1309,7 +224,7 @@ if (!require.main) { console.log("beforeEach called"); })); describe("Primary", () => { - it("smoke-test", null); + it("smoke-test", async () => {}); }); }); }" diff --git a/src/csf/transformCsf.test.ts b/src/csf/transformCsf.test.ts index ab0e88dd..15c600b7 100644 --- a/src/csf/transformCsf.test.ts +++ b/src/csf/transformCsf.test.ts @@ -1,7 +1,6 @@ -import { prefixFunction, transformCsf } from './transformCsf'; +import { TestPrefixer, TransformOptions, transformCsf } from './transformCsf'; import { testPrefixer } from '../playwright/transformPlaywright'; import template from '@babel/template'; -import * as t from '@babel/types'; describe('transformCsf', () => { it('inserts a no-op test if there are no stories', () => { @@ -12,7 +11,7 @@ describe('transformCsf', () => { `; const expectedCode = `describe.skip('Button', () => { it('no-op', () => {}) });`; - const result = transformCsf(csfCode, { insertTestIfEmpty: true }); + const result = transformCsf(csfCode, { insertTestIfEmpty: true } as TransformOptions); expect(result).toEqual(expectedCode); }); @@ -99,35 +98,12 @@ describe('transformCsf', () => { const beforeEachBlock = template.statement`beforeEach(() => { ${logStatement()} })`; return beforeEachBlock(); }; - const result = transformCsf(code, { beforeEachPrefixer }); + const testPrefixer = template(` + console.log({ id: %%id%%, title: %%title%%, name: %%name%%, storyExport: %%storyExport%% }); + async () => {}`) as unknown as TestPrefixer; - expect(result).toMatchSnapshot(); - }); -}); + const result = transformCsf(code, { beforeEachPrefixer, testPrefixer } as TransformOptions); -describe('prefixFunction', () => { - it('returns input expression if testPrefixer is not provided', () => { - const key = 'key'; - const title = 'title'; - const input = t.identifier('input'); - const result = prefixFunction(key, title, input); - expect(result).toEqual(input); - }); - - it('returns null literal if testPrefixer returns undefined', () => { - const key = 'key'; - const title = 'title'; - const input = t.identifier('input'); - const result = prefixFunction(key, title, input, testPrefixer); expect(result).toMatchSnapshot(); }); - - it('returns expression from testPrefixer if it returns a valid expression', () => { - const key = 'key'; - const title = 'title'; - const input = t.identifier('input'); - const testPrefixer = () => t.expressionStatement(t.identifier('prefix')); - const result = prefixFunction(key, title, input, testPrefixer); - expect(result).toEqual(t.identifier('input')); - }); }); diff --git a/src/csf/transformCsf.ts b/src/csf/transformCsf.ts index 7a941702..f7ce7761 100644 --- a/src/csf/transformCsf.ts +++ b/src/csf/transformCsf.ts @@ -4,6 +4,7 @@ import * as t from '@babel/types'; import generate from '@babel/generator'; import { toId, storyNameFromExport } from '@storybook/csf'; import dedent from 'ts-dedent'; + import { getTagOptions } from '../util/getTagOptions'; export interface TestContext { @@ -16,10 +17,10 @@ type TemplateResult = t.Statement | t.Statement[]; type FilePrefixer = () => TemplateResult; export type TestPrefixer = (context: TestContext) => TemplateResult; -interface TransformOptions { +export interface TransformOptions { clearBody?: boolean; beforeEachPrefixer?: FilePrefixer; - testPrefixer?: TestPrefixer; + testPrefixer: TestPrefixer; insertTestIfEmpty?: boolean; makeTitle?: (userTitle: string) => string; includeTags?: string[]; @@ -27,12 +28,7 @@ interface TransformOptions { skipTags?: string[]; } -export const prefixFunction = ( - key: string, - title: string, - input: t.Expression, - testPrefixer?: TestPrefixer -) => { +export const prefixFunction = (key: string, title: string, testPrefixer: TestPrefixer) => { const name = storyNameFromExport(key); const context: TestContext = { storyExport: t.identifier(key), @@ -41,20 +37,9 @@ export const prefixFunction = ( id: t.stringLiteral(toId(title, name)), }; - let result = input; - if (testPrefixer) { - const prefixResult = makeArray(testPrefixer(context)); - const stmt = prefixResult[1] as t.ExpressionStatement; - if (stmt) { - result = stmt.expression; - } - } - - if (!result) { - result = t.nullLiteral(); - } - - return result; + const result = makeArray(testPrefixer(context)); + const stmt = result[1] as t.ExpressionStatement; + return stmt.expression; }; const makePlayTest = ({ @@ -66,15 +51,15 @@ const makePlayTest = ({ }: { key: string; title: string; - metaOrStoryPlay?: t.Node; - testPrefix?: TestPrefixer; + metaOrStoryPlay?: boolean; + testPrefix: TestPrefixer; shouldSkip?: boolean; }): t.Statement[] => { return [ t.expressionStatement( t.callExpression(shouldSkip ? t.identifier('it.skip') : t.identifier('it'), [ t.stringLiteral(!!metaOrStoryPlay ? 'play-test' : 'smoke-test'), - prefixFunction(key, title, metaOrStoryPlay as t.Expression, testPrefix), + prefixFunction(key, title, testPrefix), ]) ), ]; @@ -111,7 +96,7 @@ export const transformCsf = ( beforeEachPrefixer, insertTestIfEmpty, makeTitle, - }: TransformOptions = {} + }: TransformOptions ) => { const { includeTags, excludeTags, skipTags } = getTagOptions(); @@ -153,7 +138,7 @@ export const transformCsf = ( ...makePlayTest({ key, title, - metaOrStoryPlay: storyAnnotations[key]?.play, + metaOrStoryPlay: !!storyAnnotations[key]?.play, testPrefix: testPrefixer, shouldSkip, }), diff --git a/src/playwright/hooks.test.ts b/src/playwright/hooks.test.ts index 8f183e54..673f445e 100644 --- a/src/playwright/hooks.test.ts +++ b/src/playwright/hooks.test.ts @@ -1,8 +1,8 @@ import { Page } from 'playwright-core'; import { getStoryContext, - setPreRender, - setPostRender, + setPreVisit, + setPostVisit, TestRunnerConfig, waitForPageReady, } from './hooks'; @@ -10,19 +10,19 @@ import { type MockPage = Page & { evaluate: jest.Mock }; describe('test-runner', () => { - describe('setPreRender', () => { - it('sets the preRender function', () => { - const preRender = jest.fn(); - setPreRender(preRender); - expect(globalThis.__sbPreRender).toBe(preRender); + describe('setPreVisit', () => { + it('sets the preVisit function', () => { + const preVisit = jest.fn(); + setPreVisit(preVisit); + expect(globalThis.__sbPreVisit).toBe(preVisit); }); }); - describe('setPostRender', () => { - it('sets the postRender function', () => { - const postRender = jest.fn(); - setPostRender(postRender); - expect(globalThis.__sbPostRender).toBe(postRender); + describe('setPostVisit', () => { + it('sets the postVisit function', () => { + const postVisit = jest.fn(); + setPostVisit(postVisit); + expect(globalThis.__sbPostVisit).toBe(postVisit); }); }); diff --git a/src/playwright/transformPlaywright.ts b/src/playwright/transformPlaywright.ts index af5ffafd..326b37b4 100644 --- a/src/playwright/transformPlaywright.ts +++ b/src/playwright/transformPlaywright.ts @@ -69,7 +69,7 @@ export const testPrefixer = template( { plugins: ['jsx'], } -) as any as TestPrefixer; +) as unknown as TestPrefixer; const makeTitleFactory = (filename: string) => { const { workingDir, normalizedStoriesEntries } = getStorybookMetadata(); diff --git a/src/playwright/transformPlaywrightJson.ts b/src/playwright/transformPlaywrightJson.ts index b17e81fb..b5b1303c 100644 --- a/src/playwright/transformPlaywrightJson.ts +++ b/src/playwright/transformPlaywrightJson.ts @@ -14,7 +14,7 @@ const makeTest = ({ shouldSkip: boolean; metaOrStoryPlay: boolean; }): t.Statement => { - const result: any = testPrefixer({ + const result = testPrefixer({ name: t.stringLiteral(entry.name), title: t.stringLiteral(entry.title), id: t.stringLiteral(entry.id), diff --git a/src/setup-page.ts b/src/setup-page.ts index 608a5a58..8bbe4fa5 100644 --- a/src/setup-page.ts +++ b/src/setup-page.ts @@ -48,11 +48,11 @@ export const setupPage = async (page: Page, browserContext: BrowserContext) => { ); } - const testRunnerConfig = getTestRunnerConfig(); + const testRunnerConfig = getTestRunnerConfig() || {}; if (testRunnerConfig?.prepare) { await testRunnerConfig.prepare({ page, browserContext, testRunnerConfig }); } else { - if (testRunnerConfig) await defaultPrepare({ page, browserContext, testRunnerConfig }); + await defaultPrepare({ page, browserContext, testRunnerConfig }); } // if we ever want to log something from the browser to node diff --git a/src/test-storybook.ts b/src/test-storybook.ts index 69583681..bd557118 100644 --- a/src/test-storybook.ts +++ b/src/test-storybook.ts @@ -178,7 +178,7 @@ async function executeJestPlaywright(args: JestOptions) { await jest.run(argv); } -async function checkStorybook(url: any) { +async function checkStorybook(url: string) { try { const headers = await getHttpHeaders(url); const res = await fetch(url, { method: 'GET', headers }); diff --git a/src/util/__snapshots__/getStorybookMain.test.ts.snap b/src/util/__snapshots__/getStorybookMain.test.ts.snap index 52aadfce..c06d0310 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 exports[`getStorybookMain no stories should throw an error if no stories are defined 1`] = ` -"Could not find stories in main.js in .storybook. +"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 " `; exports[`getStorybookMain no stories should throw an error if stories list is empty 1`] = ` -"Could not find stories in main.js in .storybook. +"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 " `; -exports[`getStorybookMain should throw an error if no configuration is found 1`] = `"Could not load main.js in .storybook. Is the config directory correct? You can change it 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 "`; diff --git a/src/util/getCliOptions.test.ts b/src/util/getCliOptions.test.ts index df52cb7e..d5956a1c 100644 --- a/src/util/getCliOptions.test.ts +++ b/src/util/getCliOptions.test.ts @@ -56,14 +56,15 @@ describe('getCliOptions', () => { it('handles extra arguments correctly', () => { jest.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue({ - options: { version: true, cache: false, env: 'node' }, + options: { version: true, cache: false, env: 'node' } as any, extraArgs: ['--watch', '--coverage'], }); const opts = getCliOptions(); expect(opts.jestOptions).toEqual([ '--version', '--no-cache', - '--env="node"', + '--env', + 'node', '--watch', '--coverage', ]); diff --git a/src/util/getCliOptions.ts b/src/util/getCliOptions.ts index d106e3fe..6da687e5 100644 --- a/src/util/getCliOptions.ts +++ b/src/util/getCliOptions.ts @@ -54,20 +54,19 @@ export const getCliOptions = (): CliOptions => { jestOptions: process.argv.splice(0, 2), }; - const finalOptions = Object.keys(allOptions).reduce((acc: CliOptions, key: string) => { - if (STORYBOOK_RUNNER_COMMANDS.includes(key as StorybookRunnerCommand)) { - copyOption( - acc.runnerOptions, - key as StorybookRunnerCommand, - allOptions[key as StorybookRunnerCommand] - ); + const finalOptions = Object.keys(allOptions).reduce((acc: CliOptions, _key: string) => { + let key = _key as StorybookRunnerCommand; + let optionValue = allOptions[key]; + + if (STORYBOOK_RUNNER_COMMANDS.includes(key)) { + copyOption(acc.runnerOptions, key, optionValue); } else { - if (allOptions[key as StorybookRunnerCommand] === true) { + if (optionValue === true) { acc.jestOptions.push(`--${key}`); - } else if (allOptions[key as StorybookRunnerCommand] === false) { + } else if (optionValue === false) { acc.jestOptions.push(`--no-${key}`); } else { - acc.jestOptions.push(`--${key}="${allOptions[key as StorybookRunnerCommand]}"`); + acc.jestOptions.push(`--${key}`, `${optionValue}`); } } diff --git a/src/util/getStorybookMain.ts b/src/util/getStorybookMain.ts index 288bec0d..eb4ea145 100644 --- a/src/util/getStorybookMain.ts +++ b/src/util/getStorybookMain.ts @@ -5,9 +5,9 @@ import dedent from 'ts-dedent'; let storybookMainConfig = new Map(); -export const getStorybookMain = (configDir: string) => { +export const getStorybookMain = (configDir = '.storybook') => { if (storybookMainConfig.has(configDir)) { - return storybookMainConfig.get(configDir); + return storybookMainConfig.get(configDir) as StorybookConfig; } else { storybookMainConfig.set(configDir, serverRequire(join(resolve(configDir), 'main'))); } @@ -16,14 +16,14 @@ export const getStorybookMain = (configDir: string) => { if (!mainConfig) { throw new Error( - `Could not load main.js in ${configDir}. Is the config directory correct? You can change it 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 ` ); } if (!mainConfig.stories || mainConfig.stories.length === 0) { throw new Error( dedent` - Could not find stories in main.js in ${configDir}. + 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 ` diff --git a/src/util/getStorybookMetadata.ts b/src/util/getStorybookMetadata.ts index fd45b660..a7e4de9c 100644 --- a/src/util/getStorybookMetadata.ts +++ b/src/util/getStorybookMetadata.ts @@ -5,10 +5,10 @@ import { StoriesEntry } from '@storybook/types'; export const getStorybookMetadata = () => { const workingDir = getProjectRoot(); - const configDir = process.env.STORYBOOK_CONFIG_DIR || ''; + const configDir = process.env.STORYBOOK_CONFIG_DIR || '.storybook'; const main = getStorybookMain(configDir); - const normalizedStoriesEntries = normalizeStories(main?.stories as StoriesEntry[], { + const normalizedStoriesEntries = normalizeStories(main.stories as StoriesEntry[], { configDir, workingDir, }).map((specifier) => ({ @@ -22,7 +22,7 @@ export const getStorybookMetadata = () => { .join(';'); // @ts-ignore -- this is added in @storybook/core-common@6.5, which we don't depend on - const lazyCompilation = !!main?.core?.builder?.options?.lazyCompilation; + const lazyCompilation = !!main.core?.builder?.options?.lazyCompilation; return { configDir, diff --git a/src/util/getTestRunnerConfig.test.ts b/src/util/getTestRunnerConfig.test.ts index a59b9447..91670849 100644 --- a/src/util/getTestRunnerConfig.test.ts +++ b/src/util/getTestRunnerConfig.test.ts @@ -1,6 +1,5 @@ -import { serverRequire } from '@storybook/core-common'; import { TestRunnerConfig } from '../playwright/hooks'; -import { getTestRunnerConfig, loaded } from './getTestRunnerConfig'; +import { getTestRunnerConfig } from './getTestRunnerConfig'; import { join, resolve } from 'path'; const testRunnerConfig: TestRunnerConfig = { @@ -43,7 +42,6 @@ describe('getTestRunnerConfig', () => { ); const result = getTestRunnerConfig(configDir); - console.log(result); expect(result).toEqual(testRunnerConfig); expect(require('@storybook/core-common').serverRequire).toHaveBeenCalledWith( diff --git a/src/util/getTestRunnerConfig.ts b/src/util/getTestRunnerConfig.ts index 78a28c18..237f9543 100644 --- a/src/util/getTestRunnerConfig.ts +++ b/src/util/getTestRunnerConfig.ts @@ -6,7 +6,7 @@ let testRunnerConfig: TestRunnerConfig; let loaded = false; export const getTestRunnerConfig = ( - configDir = process.env.STORYBOOK_CONFIG_DIR || '' + configDir = process.env.STORYBOOK_CONFIG_DIR || '.storybook' ): TestRunnerConfig | undefined => { // testRunnerConfig can be undefined if (loaded) { From 96954deb3d61dec4665ba454aff1a39c63c79a16 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 21 Nov 2023 11:38:41 +0100 Subject: [PATCH 14/16] fix --- src/playwright/transformPlaywrightJson.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/playwright/transformPlaywrightJson.ts b/src/playwright/transformPlaywrightJson.ts index b5b1303c..b17e81fb 100644 --- a/src/playwright/transformPlaywrightJson.ts +++ b/src/playwright/transformPlaywrightJson.ts @@ -14,7 +14,7 @@ const makeTest = ({ shouldSkip: boolean; metaOrStoryPlay: boolean; }): t.Statement => { - const result = testPrefixer({ + const result: any = testPrefixer({ name: t.stringLiteral(entry.name), title: t.stringLiteral(entry.title), id: t.stringLiteral(entry.id), From 94f833f56fb497d95c421dddedd986a7460bf898 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 21 Nov 2023 11:54:06 +0100 Subject: [PATCH 15/16] fix tests gh action --- .github/workflows/tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 381dde0d..0e96bbe2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,8 +19,9 @@ jobs: - name: Install dependencies uses: bahmutov/npm-install@v1 - - name: Run jest tests + - name: Run unit tests run: | + yarn build yarn test --coverage - name: Install Playwright Browsers From 1843beeb80be5b966acca8ab9d7b46f0beb5f3aa Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 21 Nov 2023 11:58:30 +0100 Subject: [PATCH 16/16] fix --- src/test-storybook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test-storybook.ts b/src/test-storybook.ts index bd557118..5c4bcae5 100644 --- a/src/test-storybook.ts +++ b/src/test-storybook.ts @@ -239,7 +239,7 @@ async function getIndexJson(url: string) { async function getIndexTempDir(url: string) { let tmpDir: string; try { - const indexJson = await getIndexJson('url'); + const indexJson = await getIndexJson(url); const titleIdToTest = transformPlaywrightJson(indexJson); tmpDir = tempy.directory();