diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index bc7f0e9d..460f4c98 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -39,7 +39,7 @@ const config: TestRunnerConfig = { }); const elementHandler = (await page.$('#root')) || (await page.$('#storybook-root')); - const innerHTML = await elementHandler.innerHTML(); + const innerHTML = await elementHandler?.innerHTML(); // HTML snapshot tests expect(innerHTML).toMatchSnapshot(); }, diff --git a/package.json b/package.json index 53adadd3..be79ef22 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "@babel/preset-env": "^7.19.4", "@babel/preset-react": "^7.18.6", "@babel/preset-typescript": "^7.18.6", - "@jest/types": "^29.6.3", "@storybook/addon-coverage": "^0.0.9", "@storybook/addon-essentials": "^7.5.3", "@storybook/addon-interactions": "^7.5.3", @@ -96,6 +95,7 @@ "@babel/generator": "^7.22.5", "@babel/template": "^7.22.5", "@babel/types": "^7.22.5", + "@jest/types": "^29.6.3", "@storybook/core-common": "^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0", "@storybook/csf": "^0.1.1", "@storybook/csf-tools": "^7.0.0-beta.0 || ^7.0.0-rc.0 || ^7.0.0", diff --git a/src/config/jest-playwright.ts b/src/config/jest-playwright.ts index 144bc4ee..e7a35a25 100644 --- a/src/config/jest-playwright.ts +++ b/src/config/jest-playwright.ts @@ -1,7 +1,8 @@ import path from 'path'; import { getProjectRoot } from '@storybook/core-common'; +import type { Config } from '@jest/types'; -const TEST_RUNNER_PATH = process.env.STORYBOOK_TEST_RUNNER_PATH || '@storybook/test-runner'; +const TEST_RUNNER_PATH = process.env.STORYBOOK_TEST_RUNNER_PATH ?? '@storybook/test-runner'; /** * IMPORTANT NOTE: @@ -15,7 +16,7 @@ const TEST_RUNNER_PATH = process.env.STORYBOOK_TEST_RUNNER_PATH || '@storybook/t * This function does the same thing as `preset: 'jest-playwright-preset` but makes sure that the * necessary moving parts are all required within the correct path. * */ -const getJestPlaywrightConfig = () => { +const getJestPlaywrightConfig = (): Config.InitialOptions => { const presetBasePath = path.dirname( require.resolve('jest-playwright-preset', { paths: [path.join(__dirname, '../node_modules')], @@ -28,18 +29,18 @@ const getJestPlaywrightConfig = () => { ); return { runner: path.join(presetBasePath, 'runner.js'), - globalSetup: require.resolve(TEST_RUNNER_PATH + '/playwright/global-setup.js'), - globalTeardown: require.resolve(TEST_RUNNER_PATH + '/playwright/global-teardown.js'), - testEnvironment: require.resolve(TEST_RUNNER_PATH + '/playwright/custom-environment.js'), + globalSetup: require.resolve(`${TEST_RUNNER_PATH}/playwright/global-setup.js`), + globalTeardown: require.resolve(`${TEST_RUNNER_PATH}/playwright/global-teardown.js`), + testEnvironment: require.resolve(`${TEST_RUNNER_PATH}/playwright/custom-environment.js`), setupFilesAfterEnv: [ - require.resolve(TEST_RUNNER_PATH + '/playwright/jest-setup.js'), + require.resolve(`${TEST_RUNNER_PATH}/playwright/jest-setup.js`), expectPlaywrightPath, path.join(presetBasePath, 'lib', 'extends.js'), ], }; }; -export const getJestConfig = () => { +export const getJestConfig = (): Config.InitialOptions => { const { TEST_ROOT, TEST_MATCH, @@ -69,16 +70,16 @@ export const getJestConfig = () => { const reporters = STORYBOOK_JUNIT ? ['default', jestJunitPath] : ['default']; - const testMatch = (STORYBOOK_STORIES_PATTERN && STORYBOOK_STORIES_PATTERN.split(';')) || []; + const testMatch = STORYBOOK_STORIES_PATTERN?.split(';') ?? []; - let config = { + const config: Config.InitialOptions = { rootDir: getProjectRoot(), roots: TEST_ROOT ? [TEST_ROOT] : undefined, reporters, testMatch, transform: { '^.+\\.(story|stories)\\.[jt]sx?$': require.resolve( - TEST_RUNNER_PATH + '/playwright/transform' + `${TEST_RUNNER_PATH}/playwright/transform` ), '^.+\\.[jt]sx?$': swcJestPath, }, diff --git a/src/csf/__snapshots__/transformCsf.test.ts.snap b/src/csf/__snapshots__/transformCsf.test.ts.snap index 5e4d7b32..e83f4531 100644 --- a/src/csf/__snapshots__/transformCsf.test.ts.snap +++ b/src/csf/__snapshots__/transformCsf.test.ts.snap @@ -229,3 +229,12 @@ if (!require.main) { }); }" `; + +exports[`transformCsf returns empty result if there are no stories 1`] = ` +" + export default { + title: 'Button', + }; + +" +`; diff --git a/src/csf/transformCsf.test.ts b/src/csf/transformCsf.test.ts index 15c600b7..f48379d3 100644 --- a/src/csf/transformCsf.test.ts +++ b/src/csf/transformCsf.test.ts @@ -16,6 +16,18 @@ describe('transformCsf', () => { expect(result).toEqual(expectedCode); }); + it('returns empty result if there are no stories', () => { + const csfCode = ` + export default { + title: 'Button', + }; + `; + + const result = transformCsf(csfCode, { testPrefixer }); + + expect(result).toMatchSnapshot(); + }); + it('calls the testPrefixer function for each test', () => { const csfCode = ` export default { diff --git a/src/csf/transformCsf.ts b/src/csf/transformCsf.ts index f7ce7761..18391594 100644 --- a/src/csf/transformCsf.ts +++ b/src/csf/transformCsf.ts @@ -54,11 +54,11 @@ const makePlayTest = ({ metaOrStoryPlay?: boolean; testPrefix: TestPrefixer; shouldSkip?: boolean; -}): t.Statement[] => { +}): t.ExpressionStatement[] => { return [ t.expressionStatement( t.callExpression(shouldSkip ? t.identifier('it.skip') : t.identifier('it'), [ - t.stringLiteral(!!metaOrStoryPlay ? 'play-test' : 'smoke-test'), + t.stringLiteral(metaOrStoryPlay ? 'play-test' : 'smoke-test'), prefixFunction(key, title, testPrefix), ]) ), @@ -69,7 +69,7 @@ const makeDescribe = ( key: string, tests: t.Statement[], beforeEachBlock?: t.ExpressionStatement -): t.Statement | null => { +): t.ExpressionStatement => { const blockStatements = beforeEachBlock ? [beforeEachBlock, ...tests] : tests; return t.expressionStatement( t.callExpression(t.identifier('describe'), [ @@ -100,22 +100,25 @@ export const transformCsf = ( ) => { const { includeTags, excludeTags, skipTags } = getTagOptions(); - const csf = loadCsf(code, { makeTitle: makeTitle || ((userTitle: string) => userTitle) }); + const csf = loadCsf(code, { makeTitle: makeTitle ?? ((userTitle: string) => userTitle) }); csf.parse(); const storyExports = Object.keys(csf._stories); const title = csf.meta?.title; - const storyAnnotations = storyExports.reduce((acc, key) => { - const annotations = csf._storyAnnotations[key]; - acc[key] = {}; - if (annotations?.play) { - acc[key].play = annotations.play; - } + const storyAnnotations = storyExports.reduce>( + (acc, key) => { + const annotations = csf._storyAnnotations[key]; + acc[key] = {}; + if (annotations?.play) { + acc[key].play = annotations.play; + } - acc[key].tags = csf._stories[key].tags || csf.meta?.tags || []; - return acc; - }, {} as Record); + acc[key].tags = csf._stories[key].tags || csf.meta?.tags || []; + return acc; + }, + {} + ); const allTests = storyExports .filter((key) => { @@ -148,7 +151,6 @@ export const transformCsf = ( if (tests.length) { return makeDescribe(key, tests); } - return null; }) .filter(Boolean) as babel.types.Statement[]; diff --git a/src/playwright/transformPlaywright.test.ts b/src/playwright/transformPlaywright.test.ts index 5f0ffd29..02999719 100644 --- a/src/playwright/transformPlaywright.test.ts +++ b/src/playwright/transformPlaywright.test.ts @@ -21,8 +21,8 @@ jest.mock('@storybook/core-common', () => ({ jest.mock('../util/getTestRunnerConfig'); expect.addSnapshotSerializer({ - print: (val: any) => val.trim(), - test: (val: any) => true, + print: (val: unknown) => (typeof val === 'string' ? val.trim() : String(val)), + test: () => true, }); describe('Playwright', () => { diff --git a/src/playwright/transformPlaywright.ts b/src/playwright/transformPlaywright.ts index 326b37b4..5aed7df5 100644 --- a/src/playwright/transformPlaywright.ts +++ b/src/playwright/transformPlaywright.ts @@ -13,8 +13,9 @@ const coverageErrorMessage = dedent` More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage `; -export const testPrefixer = template( - ` +export const testPrefixer: TestPrefixer = (context) => { + return template( + ` console.log({ id: %%id%%, title: %%title%%, name: %%name%%, storyExport: %%storyExport%% }); async () => { const testFn = async() => { @@ -66,14 +67,15 @@ export const testPrefixer = template( } } `, - { - plugins: ['jsx'], - } -) as unknown as TestPrefixer; + { + plugins: ['jsx'], + } + )({ ...context }); +}; const makeTitleFactory = (filename: string) => { const { workingDir, normalizedStoriesEntries } = getStorybookMetadata(); - const filePath = './' + relative(workingDir, filename); + const filePath = `./${relative(workingDir, filename)}`; return (userTitle: string) => userOrAutoTitle(filePath, normalizedStoriesEntries, userTitle) as string; diff --git a/src/playwright/transformPlaywrightJson.test.ts b/src/playwright/transformPlaywrightJson.test.ts index e25b30f7..72659c43 100644 --- a/src/playwright/transformPlaywrightJson.test.ts +++ b/src/playwright/transformPlaywrightJson.test.ts @@ -1,4 +1,11 @@ -import { transformPlaywrightJson } from './transformPlaywrightJson'; +import { + UnsupportedVersion, + V3StoriesIndex, + V4Index, + makeDescribe, + transformPlaywrightJson, +} from './transformPlaywrightJson'; +import * as t from '@babel/types'; jest.mock('../util/getTestRunnerConfig'); @@ -18,23 +25,20 @@ describe('Playwright Json', () => { id: 'example-header--logged-in', title: 'Example/Header', name: 'Logged In', - importPath: './stories/basic/Header.stories.js', tags: ['play-fn'], }, 'example-header--logged-out': { id: 'example-header--logged-out', title: 'Example/Header', name: 'Logged Out', - importPath: './stories/basic/Header.stories.js', }, 'example-page--logged-in': { id: 'example-page--logged-in', title: 'Example/Page', name: 'Logged In', - importPath: './stories/basic/Page.stories.js', }, }, - }; + } satisfies V4Index; expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(` { "example-header": "describe("Example/Header", () => { @@ -355,16 +359,14 @@ describe('Playwright Json', () => { id: 'example-introduction--page', title: 'Example/Introduction', name: 'Page', - importPath: './stories/basic/Introduction.stories.mdx', }, 'example-page--logged-in': { id: 'example-page--logged-in', title: 'Example/Page', name: 'Logged In', - importPath: './stories/basic/Page.stories.js', }, }, - }; + } satisfies V4Index; expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(` { "example-page": "describe("Example/Page", () => { @@ -433,9 +435,6 @@ describe('Playwright Json', () => { id: 'example-header--logged-in', title: 'Example/Header', name: 'Logged In', - importPath: './stories/basic/Header.stories.js', - kind: 'Example/Header', - story: 'Logged In', parameters: { __id: 'example-header--logged-in', docsOnly: false, @@ -446,9 +445,6 @@ describe('Playwright Json', () => { id: 'example-header--logged-out', title: 'Example/Header', name: 'Logged Out', - importPath: './stories/basic/Header.stories.js', - kind: 'Example/Header', - story: 'Logged Out', parameters: { __id: 'example-header--logged-out', docsOnly: false, @@ -459,9 +455,6 @@ describe('Playwright Json', () => { id: 'example-page--logged-in', title: 'Example/Page', name: 'Logged In', - importPath: './stories/basic/Page.stories.js', - kind: 'Example/Page', - story: 'Logged In', parameters: { __id: 'example-page--logged-in', docsOnly: false, @@ -469,7 +462,7 @@ describe('Playwright Json', () => { }, }, }, - }; + } satisfies V3StoriesIndex; expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(` { "example-header": "describe("Example/Header", () => { @@ -638,9 +631,6 @@ describe('Playwright Json', () => { id: 'example-introduction--page', title: 'Example/Introduction', name: 'Page', - importPath: './stories/basic/Introduction.stories.mdx', - kind: 'Example/Introduction', - story: 'Page', parameters: { __id: 'example-introduction--page', docsOnly: true, @@ -651,9 +641,6 @@ describe('Playwright Json', () => { id: 'example-page--logged-in', title: 'Example/Page', name: 'Logged In', - importPath: './stories/basic/Page.stories.js', - kind: 'Example/Page', - story: 'Logged In', parameters: { __id: 'example-page--logged-in', docsOnly: false, @@ -661,7 +648,7 @@ describe('Playwright Json', () => { }, }, }, - }; + } satisfies V3StoriesIndex; expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(` { "example-page": "describe("Example/Page", () => { @@ -721,3 +708,39 @@ describe('Playwright Json', () => { }); }); }); + +describe('unsupported index', () => { + it('throws an error for unsupported versions', () => { + const unsupportedVersion = { v: 1 } satisfies UnsupportedVersion; + expect(() => transformPlaywrightJson(unsupportedVersion)).toThrowError( + `Unsupported version ${unsupportedVersion.v}` + ); + }); +}); + +describe('makeDescribe', () => { + it('should generate a skipped describe block with a no-op test when stmts is empty', () => { + const title = 'Test Title'; + const stmts: t.Statement[] = []; // Empty array + + const result = makeDescribe(title, stmts); + + // Create the expected AST manually for a skipped describe block with a no-op test + const noOpIt = t.expressionStatement( + t.callExpression(t.identifier('it'), [ + t.stringLiteral('no-op'), + t.arrowFunctionExpression([], t.blockStatement([])), + ]) + ); + + const expectedAST = t.expressionStatement( + t.callExpression(t.memberExpression(t.identifier('describe'), t.identifier('skip')), [ + t.stringLiteral(title), + t.arrowFunctionExpression([], t.blockStatement([noOpIt])), + ]) + ); + + // Compare the generated AST with the expected AST + expect(result).toEqual(expectedAST); + }); +}); diff --git a/src/playwright/transformPlaywrightJson.ts b/src/playwright/transformPlaywrightJson.ts index b17e81fb..e8f1c933 100644 --- a/src/playwright/transformPlaywrightJson.ts +++ b/src/playwright/transformPlaywrightJson.ts @@ -14,24 +14,23 @@ 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), // FIXME storyExport: t.identifier(entry.id), }); - - const stmt = result[1] as t.ExpressionStatement; + const stmt = (result as Array)[1]; return t.expressionStatement( t.callExpression(shouldSkip ? t.identifier('it.skip') : t.identifier('it'), [ - t.stringLiteral(!!metaOrStoryPlay ? 'play-test' : 'smoke-test'), + t.stringLiteral(metaOrStoryPlay ? 'play-test' : 'smoke-test'), stmt.expression, ]) ); }; -const makeDescribe = (title: string, stmts: t.Statement[]) => { +export const makeDescribe = (title: string, stmts: t.Statement[]) => { // When there are no tests at all, we skip. The reason is that the file already went through Jest's transformation, // so we have to skip the describe to achieve a "excluded test" experience. // The code below recreates the following source: @@ -65,18 +64,25 @@ type V4Entry = { id: StoryId; name: StoryName; title: ComponentTitle; - tags: string[]; + tags?: string[]; }; -type V4Index = { +export type V4Index = { v: 4; entries: Record; }; -type V3Story = Omit & { parameters?: Record }; -type V3StoriesIndex = { +type StoryParameters = { + __id: StoryId; + docsOnly?: boolean; + fileName?: string; +}; + +type V3Story = Omit & { parameters?: StoryParameters }; +export type V3StoriesIndex = { v: 3; stories: Record; }; +export type UnsupportedVersion = { v: number }; const isV3DocsOnly = (stories: V3Story[]) => stories.length === 1 && stories[0].name === 'Page'; function v3TitleMapToV4TitleMap(titleIdToStories: Record) { @@ -88,26 +94,26 @@ function v3TitleMapToV4TitleMap(titleIdToStories: Record) { ({ type: isV3DocsOnly(stories) ? 'docs' : 'story', ...story, - } as V4Entry) + } satisfies V4Entry) ), ]) ); } function groupByTitleId(entries: T[]) { - return entries.reduce((acc, entry) => { + return entries.reduce>((acc, entry) => { const titleId = toId(entry.title); acc[titleId] = acc[titleId] || []; acc[titleId].push(entry); return acc; - }, {} as { [key: string]: T[] }); + }, {}); } /** * Generate one test file per component so that Jest can * run them in parallel. */ -export const transformPlaywrightJson = (index: Record) => { +export const transformPlaywrightJson = (index: V3StoriesIndex | V4Index | UnsupportedVersion) => { let titleIdToEntries: Record; if (index.v === 3) { const titleIdToStories = groupByTitleId( @@ -123,39 +129,42 @@ export const transformPlaywrightJson = (index: Record) => { const { includeTags, excludeTags, skipTags } = getTagOptions(); - const titleIdToTest = Object.entries(titleIdToEntries).reduce((acc, [titleId, entries]) => { - const stories = entries.filter((s) => s.type !== 'docs'); - if (stories.length) { - const storyTests = stories - .filter((story) => { - // If includeTags is passed, check if the story has any of them - else include by default - const isIncluded = - includeTags.length === 0 || includeTags.some((tag) => story.tags?.includes(tag)); - - // If excludeTags is passed, check if the story does not have any of them - const isNotExcluded = excludeTags.every((tag) => !story.tags?.includes(tag)); - - return isIncluded && isNotExcluded; - }) - .map((story) => { - const shouldSkip = skipTags.some((tag) => story.tags?.includes(tag)); - - return makeDescribe(story.name, [ - makeTest({ - entry: story, - shouldSkip, - metaOrStoryPlay: story.tags?.includes('play-fn'), - }), - ]); - }); - const program = t.program([makeDescribe(stories[0].title, storyTests)]) as babel.types.Node; - - const { code } = generate(program, {}); - - acc[titleId] = code; - } - return acc; - }, {} as { [key: string]: string }); + const titleIdToTest = Object.entries(titleIdToEntries).reduce>( + (acc, [titleId, entries]) => { + const stories = entries.filter((s) => s.type !== 'docs'); + if (stories.length) { + const storyTests = stories + .filter((story) => { + // If includeTags is passed, check if the story has any of them - else include by default + const isIncluded = + includeTags.length === 0 || includeTags.some((tag) => story.tags?.includes(tag)); + + // If excludeTags is passed, check if the story does not have any of them + const isNotExcluded = excludeTags.every((tag) => !story.tags?.includes(tag)); + + return isIncluded && isNotExcluded; + }) + .map((story) => { + const shouldSkip = skipTags.some((tag) => story.tags?.includes(tag)); + + return makeDescribe(story.name, [ + makeTest({ + entry: story, + shouldSkip, + metaOrStoryPlay: story.tags?.includes('play-fn') ?? false, + }), + ]); + }); + const program = t.program([makeDescribe(stories[0].title, storyTests)]) as babel.types.Node; + + const { code } = generate(program, {}); + + acc[titleId] = code; + } + return acc; + }, + {} + ); return titleIdToTest; }; diff --git a/src/setup-page.ts b/src/setup-page.ts index 8bbe4fa5..239febcd 100644 --- a/src/setup-page.ts +++ b/src/setup-page.ts @@ -32,7 +32,7 @@ export const setupPage = async (page: Page, browserContext: BrowserContext) => { const targetURL = process.env.TARGET_URL; const failOnConsole = process.env.TEST_CHECK_CONSOLE; - const viewMode = process.env.VIEW_MODE || 'story'; + const viewMode = process.env.VIEW_MODE ?? 'story'; const renderedEvent = viewMode === 'docs' ? 'docsRendered' : 'storyRendered'; const { packageJson } = (await readPackageUp()) as NormalizedReadResult; const { version: testRunnerVersion } = packageJson; @@ -226,7 +226,7 @@ export const setupPage = async (page: Page, browserContext: BrowserContext) => { constructor(storyId, errorMessage, logs = []) { super(errorMessage); this.name = 'StorybookTestRunnerError'; - const storyUrl = \`${referenceURL || targetURL}?path=/story/\${storyId}\`; + const storyUrl = \`${referenceURL ?? targetURL}?path=/story/\${storyId}\`; const finalStoryUrl = \`\${storyUrl}&addonPanel=storybook/interactions/panel\`; const separator = '\\n\\n--------------------------------------------------'; const extraLogs = logs.length > 0 ? separator + "\\n\\nBrowser logs:\\n\\n"+ logs.join('\\n\\n') : ''; diff --git a/src/test-storybook.ts b/src/test-storybook.ts index 5c4bcae5..c229d7ea 100644 --- a/src/test-storybook.ts +++ b/src/test-storybook.ts @@ -1,5 +1,4 @@ #!/usr/bin/env node -'use strict'; import fs from 'fs'; import { execSync } from 'child_process'; @@ -9,10 +8,9 @@ import dedent from 'ts-dedent'; import path from 'path'; import tempy from 'tempy'; import semver from 'semver'; -import { detect as detectPackageManager, PM } from 'detect-package-manager'; +import { detect as detectPackageManager } from 'detect-package-manager'; -import { JestOptions } from './util/getCliOptions'; -import { getCliOptions } from './util/getCliOptions'; +import { JestOptions, getCliOptions } from './util/getCliOptions'; import { getStorybookMetadata } from './util/getStorybookMetadata'; import { getTestRunnerConfig } from './util/getTestRunnerConfig'; import { transformPlaywrightJson } from './playwright/transformPlaywrightJson'; @@ -133,7 +131,7 @@ function sanitizeURL(url: string) { let finalURL = url; // prepend URL protocol if not there if (finalURL.indexOf('http://') === -1 && finalURL.indexOf('https://') === -1) { - finalURL = 'http://' + finalURL; + finalURL = `http://${finalURL}`; } // remove iframe.html if present @@ -143,8 +141,8 @@ function sanitizeURL(url: string) { finalURL = finalURL.replace(/index.html\s*$/, ''); // add forward slash at the end if not there - if (finalURL.slice(-1) !== '/') { - finalURL = finalURL + '/'; + if (!finalURL.endsWith('/')) { + finalURL = `${finalURL}/`; } return finalURL; @@ -158,10 +156,10 @@ async function executeJestPlaywright(args: JestOptions) { }) ); const jest = require(jestPath); - let argv = args.slice(2); + const argv = args.slice(2); // jest configs could either come in the root dir, or inside of the Storybook config dir - const configDir = process.env.STORYBOOK_CONFIG_DIR || ''; + const configDir = process.env.STORYBOOK_CONFIG_DIR ?? ''; const [userDefinedJestConfig] = ( await Promise.all([ glob(path.join(configDir, 'test-runner-jest*'), { windowsPathsNoEscape: true }), @@ -243,10 +241,10 @@ async function getIndexTempDir(url: string) { const titleIdToTest = transformPlaywrightJson(indexJson); tmpDir = tempy.directory(); - Object.entries(titleIdToTest).forEach(([titleId, test]) => { + for (const [titleId, test] of Object.entries(titleIdToTest)) { const tmpFile = path.join(tmpDir, `${titleId}.test.js`); - fs.writeFileSync(tmpFile, test as string); - }); + fs.writeFileSync(tmpFile, test); + } } catch (err) { if (err instanceof Error) { error(err); @@ -298,7 +296,7 @@ const main = async () => { process.env.STORYBOOK_CONFIG_DIR = runnerOptions.configDir; - const testRunnerConfig = getTestRunnerConfig(runnerOptions.configDir) || ({} as TestRunnerConfig); + const testRunnerConfig = getTestRunnerConfig(runnerOptions.configDir) ?? ({} as TestRunnerConfig); if (testRunnerConfig.preVisit && testRunnerConfig.preRender) { throw new Error( @@ -331,7 +329,7 @@ const main = async () => { // set this flag to skip reporting coverage in watch mode const isWatchMode = jestOptions.includes('--watch') || jestOptions.includes('--watchAll'); - const rawTargetURL = process.env.TARGET_URL || runnerOptions.url || 'http://127.0.0.1:6006'; + const rawTargetURL = process.env.TARGET_URL ?? runnerOptions.url ?? 'http://127.0.0.1:6006'; await checkStorybook(rawTargetURL); const targetURL = sanitizeURL(rawTargetURL); diff --git a/src/typings.d.ts b/src/typings.d.ts index a756e2cc..f4bdf425 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -1,7 +1,11 @@ import { TestHook } from './playwright/hooks'; +import { type setupPage } from './setup-page'; +import type { StoryContext } from '@storybook/csf'; declare global { var __sbPreVisit: TestHook; var __sbPostVisit: TestHook; - var __getContext: (storyId: string) => any; + var __getContext: (storyId: string) => StoryContext; + var __sbSetupPage: typeof setupPage; + var __sbCollectCoverage: boolean; } diff --git a/src/util/getCliOptions.test.ts b/src/util/getCliOptions.test.ts index d5956a1c..47e6eddf 100644 --- a/src/util/getCliOptions.test.ts +++ b/src/util/getCliOptions.test.ts @@ -2,7 +2,7 @@ import { getCliOptions } from './getCliOptions'; import * as cliHelper from './getParsedCliOptions'; describe('getCliOptions', () => { - let originalArgv: string[] = process.argv; + const originalArgv: string[] = process.argv; afterEach(() => { process.argv = originalArgv; diff --git a/src/util/getCliOptions.ts b/src/util/getCliOptions.ts index 6da687e5..99b27197 100644 --- a/src/util/getCliOptions.ts +++ b/src/util/getCliOptions.ts @@ -17,7 +17,7 @@ export type CliOptions = { includeTags?: string; excludeTags?: string; skipTags?: string; - }; + } & Record; jestOptions: JestOptions; }; diff --git a/src/util/getParsedCliOptions.test.ts b/src/util/getParsedCliOptions.test.ts index 3ee51258..6f34bd6a 100644 --- a/src/util/getParsedCliOptions.test.ts +++ b/src/util/getParsedCliOptions.test.ts @@ -44,7 +44,7 @@ describe('getParsedCliOptions', () => { console.warn = jest.fn(); const originalExit = process.exit; - process.exit = jest.fn() as any; + process.exit = jest.fn() as unknown as typeof process.exit; const argv = process.argv.slice(); process.argv.push('--unknown-option'); diff --git a/src/util/getStorybookMain.test.ts b/src/util/getStorybookMain.test.ts index 92f9f408..2ccb2783 100644 --- a/src/util/getStorybookMain.test.ts +++ b/src/util/getStorybookMain.test.ts @@ -1,4 +1,4 @@ -import { getStorybookMain, resetStorybookMainCache } from './getStorybookMain'; +import { getStorybookMain, resetStorybookMainCache, storybookMainConfig } from './getStorybookMain'; import * as coreCommon from '@storybook/core-common'; jest.mock('@storybook/core-common'); @@ -41,4 +41,19 @@ describe('getStorybookMain', () => { const res = getStorybookMain('.storybook'); expect(res).toMatchObject(mockedMain); }); + + it('should return the configDir value if it exists', () => { + const mockedMain = { + stories: [ + { + directory: '../stories/basic', + titlePrefix: 'Example', + }, + ], + }; + storybookMainConfig.set('configDir', mockedMain); + + const res = getStorybookMain('.storybook'); + expect(res).toMatchObject(mockedMain); + }); }); diff --git a/src/util/getStorybookMain.ts b/src/util/getStorybookMain.ts index eb4ea145..76ee33e5 100644 --- a/src/util/getStorybookMain.ts +++ b/src/util/getStorybookMain.ts @@ -3,7 +3,7 @@ import { serverRequire } from '@storybook/core-common'; import type { StorybookConfig } from '@storybook/types'; import dedent from 'ts-dedent'; -let storybookMainConfig = new Map(); +export const storybookMainConfig = new Map(); export const getStorybookMain = (configDir = '.storybook') => { if (storybookMainConfig.has(configDir)) { diff --git a/src/util/getStorybookMetadata.ts b/src/util/getStorybookMetadata.ts index a7e4de9c..68e2a7c6 100644 --- a/src/util/getStorybookMetadata.ts +++ b/src/util/getStorybookMetadata.ts @@ -5,7 +5,7 @@ import { StoriesEntry } from '@storybook/types'; export const getStorybookMetadata = () => { const workingDir = getProjectRoot(); - const configDir = process.env.STORYBOOK_CONFIG_DIR || '.storybook'; + const configDir = process.env.STORYBOOK_CONFIG_DIR ?? '.storybook'; const main = getStorybookMain(configDir); const normalizedStoriesEntries = normalizeStories(main.stories as StoriesEntry[], { @@ -17,7 +17,7 @@ export const getStorybookMetadata = () => { })); const storiesPaths = normalizedStoriesEntries - .map((entry) => entry.directory + '/' + entry.files) + .map((entry) => `${entry.directory}/${entry.files}`) .map((dir) => join(workingDir, dir)) .join(';'); diff --git a/src/util/getTestRunnerConfig.test.ts b/src/util/getTestRunnerConfig.test.ts index 91670849..64a7829a 100644 --- a/src/util/getTestRunnerConfig.test.ts +++ b/src/util/getTestRunnerConfig.test.ts @@ -69,6 +69,6 @@ describe('getTestRunnerConfig', () => { }); afterEach(() => { - delete process.env.STORYBOOK_CONFIG_DIR; + process.env.STORYBOOK_CONFIG_DIR = undefined; }); }); diff --git a/src/util/getTestRunnerConfig.ts b/src/util/getTestRunnerConfig.ts index 237f9543..0a5a86c1 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 || '.storybook' + configDir = process.env.STORYBOOK_CONFIG_DIR ?? '.storybook' ): TestRunnerConfig | undefined => { // testRunnerConfig can be undefined if (loaded) { diff --git a/test-runner-jest.config.js b/test-runner-jest.config.js index 11f9f27f..1a4b886b 100644 --- a/test-runner-jest.config.js +++ b/test-runner-jest.config.js @@ -8,6 +8,9 @@ const { getJestConfig } = require('./dist'); const testRunnerConfig = getJestConfig(); +/** + * @type {import('@jest/types').Config.InitialOptions} + */ module.exports = { ...testRunnerConfig, cacheDirectory: 'node_modules/.cache/storybook/test-runner', diff --git a/tsconfig.json b/tsconfig.json index ddc1f0da..ba12020e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ "moduleResolution": "node", "strict": true, "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, }, - "include": ["src/**/*.ts"], - "exclude": ["src/**/*.test.ts"] + "include": ["src/**/*.ts"] }