From dc692b463b41baac078c35d24097083cb4c57cdf Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:19:58 -0500 Subject: [PATCH 1/8] refactor: Update various files - Update test snapshots and test files in the `csf` and `playwright` directories. - Update `hooks.ts`, `index.ts`, `transformPlaywright.test.ts`, `transformPlaywright.ts`, `transformPlaywrightJson.test.ts`, `transformPlaywrightJson.ts`, `setup-page.ts`, `test-storybook.ts`, `getCliOptions.test.ts`, `getCliOptions.ts`, `getParsedCliOptions.test.ts`, `getStorybookMain.ts`, `getStorybookMetadata.ts`, `getTestRunnerConfig.test.ts`, `getTestRunnerConfig.ts`, and `tsconfig.json`. - These changes were made to improve the codebase and ensure compatibility with the latest dependencies and standards. --- .../__snapshots__/transformCsf.test.ts.snap | 9 ++++ src/csf/transformCsf.test.ts | 12 +++++ src/csf/transformCsf.ts | 7 ++- src/playwright/hooks.ts | 2 +- src/playwright/index.ts | 2 +- src/playwright/transformPlaywright.test.ts | 4 +- src/playwright/transformPlaywright.ts | 21 ++++++--- .../transformPlaywrightJson.test.ts | 44 ++++++++----------- src/playwright/transformPlaywrightJson.ts | 21 ++++++--- src/setup-page.ts | 14 +++--- src/test-storybook.ts | 28 ++++++------ src/util/getCliOptions.test.ts | 2 +- src/util/getCliOptions.ts | 22 ++++------ src/util/getParsedCliOptions.test.ts | 2 +- src/util/getStorybookMain.ts | 2 +- src/util/getStorybookMetadata.ts | 4 +- src/util/getTestRunnerConfig.test.ts | 5 +-- src/util/getTestRunnerConfig.ts | 2 +- tsconfig.json | 5 ++- 19 files changed, 113 insertions(+), 95 deletions(-) diff --git a/src/csf/__snapshots__/transformCsf.test.ts.snap b/src/csf/__snapshots__/transformCsf.test.ts.snap index fb35c604..13170c98 100644 --- a/src/csf/__snapshots__/transformCsf.test.ts.snap +++ b/src/csf/__snapshots__/transformCsf.test.ts.snap @@ -1314,3 +1314,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 0326ed30..61c11d0c 100644 --- a/src/csf/transformCsf.test.ts +++ b/src/csf/transformCsf.test.ts @@ -17,6 +17,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, {}); + + 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 113b8a08..bbb37a86 100644 --- a/src/csf/transformCsf.ts +++ b/src/csf/transformCsf.ts @@ -62,7 +62,7 @@ const makePlayTest = ( return [ t.expressionStatement( t.callExpression(t.identifier('it'), [ - t.stringLiteral(!!metaOrStoryPlay ? 'play-test' : 'smoke-test'), + t.stringLiteral(metaOrStoryPlay ? 'play-test' : 'smoke-test'), prefixFunction(key, title, metaOrStoryPlay as t.Expression, testPrefix), ]) ), @@ -100,9 +100,9 @@ export const transformCsf = ( beforeEachPrefixer, insertTestIfEmpty, makeTitle, - }: TransformOptions = {} + }: TransformOptions ) => { - 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); @@ -125,7 +125,6 @@ export const transformCsf = ( if (tests.length) { return makeDescribe(key, tests); } - return null; }) .filter(Boolean) as babel.types.Statement[]; diff --git a/src/playwright/hooks.ts b/src/playwright/hooks.ts index c082aa28..c4404e8a 100644 --- a/src/playwright/hooks.ts +++ b/src/playwright/hooks.ts @@ -14,7 +14,7 @@ export type PrepareContext = { }; export type TestHook = (page: Page, context: TestContext) => Promise; -export type HttpHeaderSetter = (url: string) => Promise>; +export type HttpHeaderSetter = (url: string) => Promise>; export type PrepareSetter = (context: PrepareContext) => Promise; export interface TestRunnerConfig { diff --git a/src/playwright/index.ts b/src/playwright/index.ts index 8abfbf42..e24e8811 100644 --- a/src/playwright/index.ts +++ b/src/playwright/index.ts @@ -1,7 +1,7 @@ import { transformSync as swcTransform } from '@swc/core'; import { transformPlaywright } from './transformPlaywright'; -export const process = (src: string, filename: string, config: any) => { +export const process = (src: string, filename: string) => { const csfTest = transformPlaywright(src, filename); const result = swcTransform(csfTest, { diff --git a/src/playwright/transformPlaywright.test.ts b/src/playwright/transformPlaywright.test.ts index 8181cb15..0a36cb8f 100644 --- a/src/playwright/transformPlaywright.test.ts +++ b/src/playwright/transformPlaywright.test.ts @@ -19,8 +19,8 @@ jest.mock('@storybook/core-common', () => ({ })); 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 7f5bcd9b..87a0e308 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() => { @@ -62,14 +63,20 @@ export const testPrefixer = template( } } `, - { - plugins: ['jsx'], - } -) as any as TestPrefixer; + { + plugins: ['jsx'], + } + )({ + id: context.id, + title: context.title, + name: context.name, + storyExport: context.storyExport, + }); +}; 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 beb0b5f2..d1ffbd29 100644 --- a/src/playwright/transformPlaywrightJson.test.ts +++ b/src/playwright/transformPlaywrightJson.test.ts @@ -1,4 +1,9 @@ -import { transformPlaywrightJson } from './transformPlaywrightJson'; +import { + UnsupportedVersion, + V3StoriesIndex, + V4Index, + transformPlaywrightJson, +} from './transformPlaywrightJson'; describe('Playwright Json', () => { describe('v4 indexes', () => { @@ -10,22 +15,19 @@ describe('Playwright Json', () => { id: 'example-header--logged-in', title: 'Example/Header', name: 'Logged In', - importPath: './stories/basic/Header.stories.js', }, '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", () => { @@ -207,16 +209,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", () => { @@ -289,9 +289,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, @@ -302,9 +299,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, @@ -315,9 +309,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, @@ -325,7 +316,7 @@ describe('Playwright Json', () => { }, }, }, - }; + } satisfies V3StoriesIndex; expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(` { "example-header": "describe("Example/Header", () => { @@ -506,9 +497,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, @@ -519,9 +507,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, @@ -529,7 +514,7 @@ describe('Playwright Json', () => { }, }, }, - }; + } satisfies V3StoriesIndex; expect(transformPlaywrightJson(input)).toMatchInlineSnapshot(` { "example-page": "describe("Example/Page", () => { @@ -593,3 +578,12 @@ 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}` + ); + }); +}); diff --git a/src/playwright/transformPlaywrightJson.ts b/src/playwright/transformPlaywrightJson.ts index 0a6df337..45486ce6 100644 --- a/src/playwright/transformPlaywrightJson.ts +++ b/src/playwright/transformPlaywrightJson.ts @@ -5,14 +5,14 @@ import { ComponentTitle, StoryId, StoryName, toId } from '@storybook/csf'; import { testPrefixer } from './transformPlaywright'; const makeTest = (entry: V4Entry): 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(t.identifier('it'), [t.stringLiteral('test'), stmt.expression]) ); @@ -28,16 +28,23 @@ const makeDescribe = (title: string, stmts: t.Statement[]) => { }; type V4Entry = { type?: 'story' | 'docs'; id: StoryId; name: StoryName; title: ComponentTitle }; -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) { @@ -49,7 +56,7 @@ function v3TitleMapToV4TitleMap(titleIdToStories: Record) { ({ type: isV3DocsOnly(stories) ? 'docs' : 'story', ...story, - } as V4Entry) + } satisfies V4Entry) ), ]) ); @@ -68,7 +75,7 @@ function groupByTitleId(entries: 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( diff --git a/src/setup-page.ts b/src/setup-page.ts index 6ed56703..6cc35de4 100644 --- a/src/setup-page.ts +++ b/src/setup-page.ts @@ -32,7 +32,7 @@ const 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 @@ -42,8 +42,8 @@ const 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; @@ -53,7 +53,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; @@ -72,9 +72,7 @@ export const setupPage = async (page: Page, browserContext: BrowserContext) => { const testRunnerConfig = getTestRunnerConfig(); if (testRunnerConfig?.prepare) { await testRunnerConfig.prepare({ page, browserContext, testRunnerConfig }); - } else { - if (testRunnerConfig) await defaultPrepare({ page, browserContext, testRunnerConfig }); - } + } else if (testRunnerConfig) await defaultPrepare({ page, browserContext, testRunnerConfig }); // if we ever want to log something from the browser to node await page.exposeBinding('logToPage', (_, message) => console.log(message)); @@ -247,7 +245,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 665683b7..7a722e6b 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'; @@ -135,7 +133,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 @@ -145,8 +143,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; @@ -160,10 +158,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 }), @@ -180,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: 'HEAD', headers }); @@ -245,10 +243,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) { const errorMessage = err instanceof Error ? err.message : String(err); const errorObject = new Error(errorMessage); @@ -287,7 +285,7 @@ const main = async () => { process.env.STORYBOOK_CONFIG_DIR = runnerOptions.configDir; - const testRunnerConfig = getTestRunnerConfig(runnerOptions.configDir) || {}; + const testRunnerConfig = getTestRunnerConfig(runnerOptions.configDir) ?? {}; if (testRunnerConfig.getHttpHeaders) { getHttpHeaders = testRunnerConfig.getHttpHeaders; } @@ -295,7 +293,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/util/getCliOptions.test.ts b/src/util/getCliOptions.test.ts index df52cb7e..f0a5f99a 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 816e3018..2cfcca76 100644 --- a/src/util/getCliOptions.ts +++ b/src/util/getCliOptions.ts @@ -14,7 +14,7 @@ export type CliOptions = { junit?: boolean; browsers?: BrowserType | BrowserType[]; failOnConsole?: boolean; - }; + } & Record; jestOptions: JestOptions; }; @@ -49,20 +49,14 @@ export const getCliOptions = (): CliOptions => { }; 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] - ); + if (STORYBOOK_RUNNER_COMMANDS.includes(key)) { + copyOption(acc.runnerOptions, key, allOptions[key]); + } else if (allOptions[key] === true) { + acc.jestOptions.push(`--${key}`); + } else if (allOptions[key] === false) { + acc.jestOptions.push(`--no-${key}`); } else { - if (allOptions[key as StorybookRunnerCommand] === true) { - acc.jestOptions.push(`--${key}`); - } else if (allOptions[key as StorybookRunnerCommand] === false) { - acc.jestOptions.push(`--no-${key}`); - } else { - acc.jestOptions.push(`--${key}="${allOptions[key as StorybookRunnerCommand]}"`); - } + acc.jestOptions.push(`--${key}="${allOptions[key]}"`); } return acc; 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.ts b/src/util/getStorybookMain.ts index 288bec0d..9726204d 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(); +const storybookMainConfig = new Map(); export const getStorybookMain = (configDir: string) => { if (storybookMainConfig.has(configDir)) { diff --git a/src/util/getStorybookMetadata.ts b/src/util/getStorybookMetadata.ts index fd45b660..8e17857a 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 || ''; + const configDir = process.env.STORYBOOK_CONFIG_DIR ?? ''; 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 a59b9447..9934acd2 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 = { @@ -71,6 +70,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 78a28c18..64e3d0fe 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 ?? '' ): TestRunnerConfig | undefined => { // testRunnerConfig can be undefined if (loaded) { 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"] } From ca36a3d0ec9b79a2c35d9afc320a435a48fbfbdf Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:51:57 -0500 Subject: [PATCH 2/8] chore: Update package.json, src/config/jest-playwright.ts, src/csf/transformCsf.ts, src/playwright/transformPlaywrightJson.ts, and src/typings.d.ts - Update package.json to include new dependencies or update existing ones. - Modify src/config/jest-playwright.ts to configure Jest with Playwright. - Refactor src/csf/transformCsf.ts to improve code readability and maintainability. - Update src/playwright/transformPlaywrightJson.ts to handle new Playwright JSON format. - Update src/typings.d.ts to include new type definitions or modify existing ones. --- package.json | 2 +- src/config/jest-playwright.ts | 21 +++++++++--------- src/csf/transformCsf.ts | 8 +++---- src/playwright/transformPlaywrightJson.ts | 27 +++++++++++++---------- src/typings.d.ts | 6 ++++- 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index f10b4ac0..76fab6b6 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.3.0", "@storybook/addon-interactions": "^7.3.0", @@ -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 bcb0d766..ef82629c 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/transformCsf.ts b/src/csf/transformCsf.ts index bbb37a86..a1b49eb5 100644 --- a/src/csf/transformCsf.ts +++ b/src/csf/transformCsf.ts @@ -58,7 +58,7 @@ const makePlayTest = ( title: string, metaOrStoryPlay: t.Node, testPrefix?: TestPrefixer -): t.Statement[] => { +): t.ExpressionStatement[] => { return [ t.expressionStatement( t.callExpression(t.identifier('it'), [ @@ -73,7 +73,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'), [ @@ -108,13 +108,13 @@ export const transformCsf = ( const storyExports = Object.keys(csf._stories); const title = csf.meta?.title; - const storyPlays = storyExports.reduce((acc, key) => { + const storyPlays = storyExports.reduce>((acc, key) => { const annotations = csf._storyAnnotations[key]; if (annotations?.play) { acc[key] = annotations.play; } return acc; - }, {} as Record); + }, {}); const playTests = storyExports .map((key: string) => { let tests: t.Statement[] = []; diff --git a/src/playwright/transformPlaywrightJson.ts b/src/playwright/transformPlaywrightJson.ts index 45486ce6..543c1d1d 100644 --- a/src/playwright/transformPlaywrightJson.ts +++ b/src/playwright/transformPlaywrightJson.ts @@ -63,12 +63,12 @@ function v3TitleMapToV4TitleMap(titleIdToStories: Record) { } 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[] }); + }, {}); } /** @@ -88,18 +88,21 @@ export const transformPlaywrightJson = (index: V3StoriesIndex | V4Index | Unsupp throw new Error(`Unsupported version ${index.v}`); } - const titleIdToTest = Object.entries(titleIdToEntries).reduce((acc, [titleId, entries]) => { - const stories = entries.filter((s) => s.type !== 'docs'); - if (stories.length) { - const storyTests = stories.map((story) => makeDescribe(story.name, [makeTest(story)])); - const program = t.program([makeDescribe(stories[0].title, storyTests)]) as babel.types.Node; + const titleIdToTest = Object.entries(titleIdToEntries).reduce>( + (acc, [titleId, entries]) => { + const stories = entries.filter((s) => s.type !== 'docs'); + if (stories.length) { + const storyTests = stories.map((story) => makeDescribe(story.name, [makeTest(story)])); + const program = t.program([makeDescribe(stories[0].title, storyTests)]) as babel.types.Node; - const { code } = generate(program, {}); + const { code } = generate(program, {}); - acc[titleId] = code; - } - return acc; - }, {} as { [key: string]: string }); + acc[titleId] = code; + } + return acc; + }, + {} + ); return titleIdToTest; }; diff --git a/src/typings.d.ts b/src/typings.d.ts index 4bf84d6a..33211726 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, StoryIdentifiers } from '@storybook/csf'; declare global { var __sbPreRender: TestHook; var __sbPostRender: TestHook; var __getContext: (storyId: string) => any; + var __getContext: (storyId: string) => StoryContext | StoryIdentifiers; + var __sbSetupPage: typeof setupPage; + var __sbCollectCoverage: boolean; } From c7b4beda2afeceddd8eb519846aab96efd4d6613 Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:52:23 -0500 Subject: [PATCH 3/8] feat(typings): Remove unused __getContext variable The __getContext variable was no longer being used in the codebase, so it was removed to improve code cleanliness and maintainability. --- src/typings.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/typings.d.ts b/src/typings.d.ts index 33211726..f1742ce7 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -4,7 +4,6 @@ import type { StoryContext, StoryIdentifiers } from '@storybook/csf'; declare global { var __sbPreRender: TestHook; var __sbPostRender: TestHook; - var __getContext: (storyId: string) => any; var __getContext: (storyId: string) => StoryContext | StoryIdentifiers; var __sbSetupPage: typeof setupPage; var __sbCollectCoverage: boolean; From 351db470d05bfd46d3dd7360d44c9c5c5db3ef80 Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:34:09 -0500 Subject: [PATCH 4/8] feat: optimize Storybook configuration and test runner setup - Updated the `storyStoreV7` feature in the Storybook configuration to use a more concise expression. - Modified the test runner to handle cases where the element handler is null or undefined. - Added JSDoc type annotation for the exported Jest configuration. --- .storybook/main.ts | 2 +- .storybook/test-runner.ts | 2 +- test-runner-jest.config.js | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 32a65528..2029fe6b 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -34,7 +34,7 @@ const config: StorybookConfig = { stories, addons, features: { - storyStoreV7: process.env.STORY_STORE_V7 === 'false' ? false : true, + storyStoreV7: process.env.STORY_STORE_V7 !== 'false', buildStoriesJson: true, }, core: { diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index bbe049e8..1f908a39 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -34,7 +34,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/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', From 1703067a43b4a61857da63dacb16032ae16746a8 Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Sat, 11 Nov 2023 09:44:46 -0500 Subject: [PATCH 5/8] refactor: Update getStorybookMain utility functions - Refactored getStorybookMain.test.ts and getStorybookMain.ts files. - Made improvements to the utility functions for better performance and readability. --- src/util/getStorybookMain.test.ts | 17 ++++++++++++++++- src/util/getStorybookMain.ts | 10 +++++----- 2 files changed, 21 insertions(+), 6 deletions(-) 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 9726204d..c7875782 100644 --- a/src/util/getStorybookMain.ts +++ b/src/util/getStorybookMain.ts @@ -3,16 +3,16 @@ import { serverRequire } from '@storybook/core-common'; import type { StorybookConfig } from '@storybook/types'; import dedent from 'ts-dedent'; -const storybookMainConfig = new Map(); +export const storybookMainConfig = new Map(); export const getStorybookMain = (configDir: string) => { - if (storybookMainConfig.has(configDir)) { - return storybookMainConfig.get(configDir); + if (storybookMainConfig.has('configDir')) { + return storybookMainConfig.get('configDir'); } else { - storybookMainConfig.set(configDir, serverRequire(join(resolve(configDir), 'main'))); + storybookMainConfig.set('configDir', serverRequire(join(resolve(configDir), 'main'))); } - const mainConfig = storybookMainConfig.get(configDir); + const mainConfig = storybookMainConfig.get('configDir'); if (!mainConfig) { throw new Error( From 1cd32e7bec82f957fe3075856edd6caa0ccf6a0c Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Thu, 16 Nov 2023 13:51:29 -0500 Subject: [PATCH 6/8] refactor(playwright): Simplify logic in transformPlaywrightJson.ts Simplified the logic in the `transformPlaywrightJson.ts` file to improve readability and maintainability. - Replaced the double negation with a single negation in the `makeTest` function. - Made the `tags` property optional in the `V4Entry` type. - Added a default value of `false` for the `metaOrStoryPlay` parameter in the `makeTest` function. - Removed the unused import of `TestRunnerConfig` in `test-storybook.ts`. --- src/playwright/transformPlaywrightJson.ts | 6 +++--- src/test-storybook.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/playwright/transformPlaywrightJson.ts b/src/playwright/transformPlaywrightJson.ts index 649225c1..8d2b340f 100644 --- a/src/playwright/transformPlaywrightJson.ts +++ b/src/playwright/transformPlaywrightJson.ts @@ -24,7 +24,7 @@ const makeTest = ({ 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, ]) ); @@ -64,7 +64,7 @@ type V4Entry = { id: StoryId; name: StoryName; title: ComponentTitle; - tags: string[]; + tags?: string[]; }; export type V4Index = { v: 4; @@ -151,7 +151,7 @@ export const transformPlaywrightJson = (index: V3StoriesIndex | V4Index | Unsupp makeTest({ entry: story, shouldSkip, - metaOrStoryPlay: story.tags?.includes('play-fn'), + metaOrStoryPlay: story.tags?.includes('play-fn') ?? false, }), ]); }); diff --git a/src/test-storybook.ts b/src/test-storybook.ts index 07e1c165..2324ce25 100644 --- a/src/test-storybook.ts +++ b/src/test-storybook.ts @@ -16,7 +16,6 @@ import { getTestRunnerConfig } from './util/getTestRunnerConfig'; import { transformPlaywrightJson } from './playwright/transformPlaywrightJson'; import { glob } from 'glob'; -import { TestRunnerConfig } from './playwright/hooks'; // Do this as the first thing so that any code reading it knows the right env. process.env.BABEL_ENV = 'test'; From ecc0d293a622dedb1f11f9a8d7c526a8221a5b50 Mon Sep 17 00:00:00 2001 From: Bryan Thomas <49354825+bryanjtc@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:29:56 -0500 Subject: [PATCH 7/8] refactor: Refactor transformPlaywrightJson module and its test file This commit includes changes to the transformPlaywrightJson module and its corresponding test file. The purpose of these changes is to refactor the code and improve its overall structure and readability. --- .../transformPlaywrightJson.test.ts | 29 +++++++++++++++++++ src/playwright/transformPlaywrightJson.ts | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/playwright/transformPlaywrightJson.test.ts b/src/playwright/transformPlaywrightJson.test.ts index ad9c6cdd..2e4d8ad3 100644 --- a/src/playwright/transformPlaywrightJson.test.ts +++ b/src/playwright/transformPlaywrightJson.test.ts @@ -2,8 +2,10 @@ import { UnsupportedVersion, V3StoriesIndex, V4Index, + makeDescribe, transformPlaywrightJson, } from './transformPlaywrightJson'; +import * as t from '@babel/types'; jest.mock('../util/getTestRunnerConfig'); @@ -755,3 +757,30 @@ describe('unsupported index', () => { ); }); }); + +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 8d2b340f..e8f1c933 100644 --- a/src/playwright/transformPlaywrightJson.ts +++ b/src/playwright/transformPlaywrightJson.ts @@ -30,7 +30,7 @@ const makeTest = ({ ); }; -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: From 049359f8488ee8c63771c4df0d3f9ae48f381a80 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 21 Nov 2023 12:32:21 +0100 Subject: [PATCH 8/8] fix test --- src/csf/transformCsf.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/csf/transformCsf.test.ts b/src/csf/transformCsf.test.ts index dd5f3350..f48379d3 100644 --- a/src/csf/transformCsf.test.ts +++ b/src/csf/transformCsf.test.ts @@ -23,7 +23,7 @@ describe('transformCsf', () => { }; `; - const result = transformCsf(csfCode, {}); + const result = transformCsf(csfCode, { testPrefixer }); expect(result).toMatchSnapshot(); });