From 876264b844f6a0e3dda14970729ff8ac5ef5b96c Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 27 Aug 2024 14:53:23 +0200 Subject: [PATCH 01/26] move hook usage to the top. --- code/core/src/manager/components/sidebar/StatusContext.tsx | 3 ++- code/core/src/manager/components/sidebar/Tree.tsx | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/code/core/src/manager/components/sidebar/StatusContext.tsx b/code/core/src/manager/components/sidebar/StatusContext.tsx index 11c66d6f02b8..c3db2ed9bc30 100644 --- a/code/core/src/manager/components/sidebar/StatusContext.tsx +++ b/code/core/src/manager/components/sidebar/StatusContext.tsx @@ -3,6 +3,7 @@ import { createContext, useContext } from 'react'; import type { API_StatusObject, API_StatusState, API_StatusValue, StoryId } from '@storybook/types'; import type { ComponentEntry, GroupEntry, StoriesHash } from '../../../manager-api'; +import type { Item } from '../../container/Sidebar'; import { getDescendantIds } from '../../utils/tree'; export const StatusContext = createContext<{ @@ -11,7 +12,7 @@ export const StatusContext = createContext<{ groupStatus?: Record; }>({}); -export const useStatusSummary = (item: GroupEntry | ComponentEntry) => { +export const useStatusSummary = (item: Item) => { const { data, status, groupStatus } = useContext(StatusContext); const summary: { counts: Record; diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx index 04aaffb974c5..4f542d3140a7 100644 --- a/code/core/src/manager/components/sidebar/Tree.tsx +++ b/code/core/src/manager/components/sidebar/Tree.tsx @@ -153,6 +153,7 @@ const Node = React.memo(function Node({ }) { const { isDesktop, isMobile, setMobileMenuOpen } = useLayout(); const theme = useTheme(); + const { counts, statuses } = useStatusSummary(item); if (!isDisplayed) { return null; @@ -283,8 +284,6 @@ const Node = React.memo(function Node({ } if (item.type === 'component' || item.type === 'group') { - const { counts, statuses } = useStatusSummary(item); - const itemStatus = groupStatus?.[item.id]; const color = itemStatus ? statusMapping[itemStatus][1] : null; const BranchNode = item.type === 'component' ? ComponentNode : GroupNode; From 3072e1582a3675e88197cd795469f4a4e357d5c7 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 27 Aug 2024 15:00:07 +0200 Subject: [PATCH 02/26] remove unused imports --- code/core/src/manager/components/sidebar/StatusContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/core/src/manager/components/sidebar/StatusContext.tsx b/code/core/src/manager/components/sidebar/StatusContext.tsx index c3db2ed9bc30..d143a3db9be5 100644 --- a/code/core/src/manager/components/sidebar/StatusContext.tsx +++ b/code/core/src/manager/components/sidebar/StatusContext.tsx @@ -2,7 +2,7 @@ import { createContext, useContext } from 'react'; import type { API_StatusObject, API_StatusState, API_StatusValue, StoryId } from '@storybook/types'; -import type { ComponentEntry, GroupEntry, StoriesHash } from '../../../manager-api'; +import type { StoriesHash } from '../../../manager-api'; import type { Item } from '../../container/Sidebar'; import { getDescendantIds } from '../../utils/tree'; From eb881e560b43d0eaf2c7d856b1663201b97b32b7 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Mon, 2 Sep 2024 10:38:19 +0200 Subject: [PATCH 03/26] wip --- code/addons/vitest/src/postinstall.ts | 30 +++++++++++++-------------- code/lib/cli-storybook/src/add.ts | 11 +++++----- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/code/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts index 3ffcd813628e..b48686a27186 100644 --- a/code/addons/vitest/src/postinstall.ts +++ b/code/addons/vitest/src/postinstall.ts @@ -32,7 +32,7 @@ export default async function postInstall(options: PostinstallOptions) { info.frameworkPackageName !== '@storybook/nextjs' && info.builderPackageName !== '@storybook/builder-vite' ) { - logger.info( + logger.plain( 'The Vitest addon can only be used with a Vite-based Storybook framework or Next.js.' ); return; @@ -52,7 +52,7 @@ export default async function postInstall(options: PostinstallOptions) { : null; if (!annotationsImport) { - logger.info('The Vitest addon cannot yet be used with: ' + info.frameworkPackageName); + logger.plain('The Vitest addon cannot yet be used with: ' + info.frameworkPackageName); return; } @@ -61,26 +61,26 @@ export default async function postInstall(options: PostinstallOptions) { const packages = ['vitest@latest', '@vitest/browser@latest', 'playwright@latest']; if (info.frameworkPackageName === '@storybook/nextjs') { - logger.info( + logger.plain( dedent` We detected that you're using Next.js. - We will configure the vite-plugin-storybook-nextjs plugin to allow you to run tests in Vitest. + We will configure the ${c.magenta`vite-plugin-storybook-nextjs`} plugin to allow you to run tests in Vitest. ` ); packages.push('vite-plugin-storybook-nextjs@latest'); } - logger.info(c.bold('Installing packages...')); - logger.info(packages.join(', ')); + logger.plain(c.bold('Installing packages...')); + logger.plain(packages.join(', ')); await packageManager.addDependencies({ installAsDevDependencies: true }, packages); - logger.info(c.bold('Executing npx playwright install chromium --with-deps ...')); + logger.plain(c.bold('Executing npx playwright install chromium --with-deps ...')); await packageManager.executeCommand({ command: 'npx', args: ['playwright', 'install', 'chromium', '--with-deps'], }); - logger.info(c.bold('Writing .storybook/vitest.setup.ts file...')); + logger.plain(c.bold(`Writing ${c.cyan`.storybook/vitest.setup.ts`} file...`)); const previewExists = extensions .map((ext) => path.resolve(options.configDir, `preview${ext}`)) @@ -110,15 +110,15 @@ export default async function postInstall(options: PostinstallOptions) { const extname = path.extname(rootConfig); const browserWorkspaceFile = resolve(dirname(rootConfig), `vitest.workspace${extname}`); if (existsSync(browserWorkspaceFile)) { - logger.info( + logger.plain( dedent` We can not automatically setup the plugin when you use Vitest with workspaces. Please refer to the documentation to complete the setup manually: - https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual + ${c.yellow`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} ` ); } else { - logger.info(c.bold('Writing vitest.workspace.ts file...')); + logger.plain(c.bold(`Writing ${c.cyan`vitest.workspace.ts`} file...`)); await writeFile( browserWorkspaceFile, dedent` @@ -149,7 +149,7 @@ export default async function postInstall(options: PostinstallOptions) { } } else { // If there's no existing Vitest/Vite config, we create a new Vitest config file. - logger.info(c.bold('Writing vitest.config.ts file...')); + logger.plain(c.bold(`Writing ${c.cyan`vitest.config.ts`} file...`)); await writeFile( resolve('vitest.config.ts'), dedent` @@ -175,11 +175,11 @@ export default async function postInstall(options: PostinstallOptions) { ); } - logger.info( + logger.plain( dedent` - The Vitest addon is now configured and you're ready to run your tests! + The Test addon is now configured and you're ready to run your tests! Check the documentation for more information about its features and options at: - https://storybook.js.org/docs/writing-tests/test-runner-with-vitest + ${c.yellow`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} ` ); } diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index df3ac84f140f..ab6b6a51c2bf 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -93,19 +93,20 @@ export async function add( if (typeof configDir === 'undefined') { throw new Error(dedent` - Unable to find storybook config directory + Unable to find storybook config directory. Please specify your Storybook config directory with the --config-dir flag. `); } if (!mainConfig) { - logger.error('Unable to find storybook main.js config'); + logger.error('Unable to find Storybook main.js config'); return; } if (checkInstalled(addonName, requireMain(configDir))) { - throw new Error(dedent` - Addon ${addonName} is already installed; we skipped adding it to your ${mainConfig}. + logger.error(dedent` + The Storybook Addon "${addonName}" is already present in ${mainConfig}; Its configuration will be skipped. `); + return; } const main = await readConfig(mainConfig); @@ -135,7 +136,7 @@ export async function add( logger.log(`Installing ${addonWithVersion}`); await packageManager.addDependencies({ installAsDevDependencies: true }, [addonWithVersion]); - logger.log(`Adding '${addon}' to main.js addons field.`); + logger.log(`Adding '${addon}' to the addons field in ${mainConfig}.`); main.appendValueToArray(['addons'], addonName); await writeConfig(main); From 9bd038fff1b7037ada0b61513ed18b2fa3feed6a Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Mon, 2 Sep 2024 14:56:09 +0200 Subject: [PATCH 04/26] wip: Better logging --- code/addons/vitest/package.json | 1 + code/addons/vitest/src/postinstall-logger.ts | 27 +++++++++ code/addons/vitest/src/postinstall.ts | 48 ++++++++++++--- code/yarn.lock | 64 +++++++++++++++++++- 4 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 code/addons/vitest/src/postinstall-logger.ts diff --git a/code/addons/vitest/package.json b/code/addons/vitest/package.json index efad1c346f50..236b5e79fa4f 100644 --- a/code/addons/vitest/package.json +++ b/code/addons/vitest/package.json @@ -74,6 +74,7 @@ }, "devDependencies": { "@vitest/browser": "^2.0.0", + "boxen": "^8.0.1", "find-up": "^7.0.0", "tinyrainbow": "^1.2.0", "ts-dedent": "^2.2.0", diff --git a/code/addons/vitest/src/postinstall-logger.ts b/code/addons/vitest/src/postinstall-logger.ts new file mode 100644 index 000000000000..b195d0923bea --- /dev/null +++ b/code/addons/vitest/src/postinstall-logger.ts @@ -0,0 +1,27 @@ +import { logger } from 'storybook/internal/node-logger'; + +import boxen, { type Options } from 'boxen'; + +const baseOptions: Options = { + borderStyle: 'round', + padding: 1, + titleAlignment: 'left', +}; + +export const print = (message: string, options: Options) => { + logger.line(1); + logger.plain(boxen(message, { ...baseOptions, ...options })); + logger.line(1); +}; + +export const printInfo = (title: string, message: string, options?: Options) => + print(message, { borderColor: 'blue', title, ...options }); + +export const printWarning = (title: string, message: string, options?: Options) => + print(message, { borderColor: 'yellow', title, ...options }); + +export const printError = (title: string, message: string, options?: Options) => + print(message, { borderColor: 'red', title, ...options }); + +export const printSuccess = (title: string, message: string, options?: Options) => + print(message, { borderColor: 'green', title, ...options }); diff --git a/code/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts index b48686a27186..43daea63f695 100644 --- a/code/addons/vitest/src/postinstall.ts +++ b/code/addons/vitest/src/postinstall.ts @@ -11,17 +11,28 @@ import { loadMainConfig, validateFrameworkName, } from 'storybook/internal/common'; -import { logger } from 'storybook/internal/node-logger'; +import { colors, logger } from 'storybook/internal/node-logger'; import { findUp } from 'find-up'; import c from 'tinyrainbow'; import dedent from 'ts-dedent'; import { type PostinstallOptions } from '../../../lib/cli-storybook/src/add'; +import { printError, printInfo, printSuccess } from './postinstall-logger'; +const addonName = '@storybook/experimental-addon-vitest'; const extensions = ['.js', '.jsx', '.ts', '.tsx', '.cts', '.mts', '.cjs', '.mjs']; export default async function postInstall(options: PostinstallOptions) { + printSuccess( + 'πŸ‘‹ Howdy!', + dedent` + I'm the installation helper for ${colors.orange.bold(addonName)} + + Hold on for a moment while I look at your project and get you all set up... + ` + ); + const packageManager = JsPackageManagerFactory.getPackageManager({ force: options.packageManager, }); @@ -32,8 +43,13 @@ export default async function postInstall(options: PostinstallOptions) { info.frameworkPackageName !== '@storybook/nextjs' && info.builderPackageName !== '@storybook/builder-vite' ) { - logger.plain( - 'The Vitest addon can only be used with a Vite-based Storybook framework or Next.js.' + printError( + '⛔️ Sorry!', + dedent` + The Vitest addon can only be used with a Vite-based Storybook framework or Next.js. + + To roll back the installation, remove ${colors.orange.bold(addonName)} from the addons in your main Storybook config file and your package.json file. + ` ); return; } @@ -52,7 +68,14 @@ export default async function postInstall(options: PostinstallOptions) { : null; if (!annotationsImport) { - logger.plain('The Vitest addon cannot yet be used with: ' + info.frameworkPackageName); + printError( + '⛔️ Sorry!', + dedent` + The Vitest addon cannot yet be used with ${colors.orange.bold(info.frameworkPackageName)} + + To roll back the installation, remove ${colors.orange.bold(addonName)} from the addons in your main Storybook config file and your package.json file. + ` + ); return; } @@ -61,16 +84,18 @@ export default async function postInstall(options: PostinstallOptions) { const packages = ['vitest@latest', '@vitest/browser@latest', 'playwright@latest']; if (info.frameworkPackageName === '@storybook/nextjs') { - logger.plain( + printInfo( + '🍦 Just so you know...', dedent` - We detected that you're using Next.js. - We will configure the ${c.magenta`vite-plugin-storybook-nextjs`} plugin to allow you to run tests in Vitest. + It looks like you're using Next.js. + + I'll add the ${colors.orange.bold(`vite-plugin-storybook-nextjs`)} plugin so you can use it with Vitest. ` ); packages.push('vite-plugin-storybook-nextjs@latest'); } - logger.plain(c.bold('Installing packages...')); + logger.plain(c.bold('βœ“ Ready to install packages:')); logger.plain(packages.join(', ')); await packageManager.addDependencies({ installAsDevDependencies: true }, packages); @@ -117,6 +142,7 @@ export default async function postInstall(options: PostinstallOptions) { ${c.yellow`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} ` ); + return; } else { logger.plain(c.bold(`Writing ${c.cyan`vitest.workspace.ts`} file...`)); await writeFile( @@ -175,11 +201,13 @@ export default async function postInstall(options: PostinstallOptions) { ); } - logger.plain( + printSuccess( + 'πŸŽ‰ All done!', dedent` The Test addon is now configured and you're ready to run your tests! + Check the documentation for more information about its features and options at: - ${c.yellow`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} ` ); } diff --git a/code/yarn.lock b/code/yarn.lock index e7023fe8c13f..431048a5adfe 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6132,6 +6132,7 @@ __metadata: dependencies: "@storybook/csf": "npm:^0.1.11" "@vitest/browser": "npm:^2.0.0" + boxen: "npm:^8.0.1" find-up: "npm:^7.0.0" tinyrainbow: "npm:^1.2.0" ts-dedent: "npm:^2.2.0" @@ -9760,7 +9761,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0": +"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c @@ -10716,6 +10717,22 @@ __metadata: languageName: node linkType: hard +"boxen@npm:^8.0.1": + version: 8.0.1 + resolution: "boxen@npm:8.0.1" + dependencies: + ansi-align: "npm:^3.0.1" + camelcase: "npm:^8.0.0" + chalk: "npm:^5.3.0" + cli-boxes: "npm:^3.0.0" + string-width: "npm:^7.2.0" + type-fest: "npm:^4.21.0" + widest-line: "npm:^5.0.0" + wrap-ansi: "npm:^9.0.0" + checksum: 10c0/8c54f9797bf59eec0b44c9043d9cb5d5b2783dc673e4650235e43a5155c43334e78ec189fd410cf92056c1054aee3758279809deed115b49e68f1a1c6b3faa32 + languageName: node + linkType: hard + "bplist-parser@npm:^0.2.0": version: 0.2.0 resolution: "bplist-parser@npm:0.2.0" @@ -13621,6 +13638,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^10.3.0": + version: 10.4.0 + resolution: "emoji-regex@npm:10.4.0" + checksum: 10c0/a3fcedfc58bfcce21a05a5f36a529d81e88d602100145fcca3dc6f795e3c8acc4fc18fe773fbf9b6d6e9371205edb3afa2668ec3473fa2aa7fd47d2a9d46482d + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -15907,6 +15931,13 @@ __metadata: languageName: node linkType: hard +"get-east-asian-width@npm:^1.0.0": + version: 1.2.0 + resolution: "get-east-asian-width@npm:1.2.0" + checksum: 10c0/914b1e217cf38436c24b4c60b4c45289e39a45bf9e65ef9fd343c2815a1a02b8a0215aeec8bf9c07c516089004b6e3826332481f40a09529fcadbf6e579f286b + languageName: node + linkType: hard + "get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": version: 2.0.2 resolution: "get-func-name@npm:2.0.2" @@ -26216,6 +26247,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^7.0.0, string-width@npm:^7.2.0": + version: 7.2.0 + resolution: "string-width@npm:7.2.0" + dependencies: + emoji-regex: "npm:^10.3.0" + get-east-asian-width: "npm:^1.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10c0/eb0430dd43f3199c7a46dcbf7a0b34539c76fe3aa62763d0b0655acdcbdf360b3f66f3d58ca25ba0205f42ea3491fa00f09426d3b7d3040e506878fc7664c9b9 + languageName: node + linkType: hard + "string.prototype.matchall@npm:^4.0.8": version: 4.0.10 resolution: "string.prototype.matchall@npm:4.0.10" @@ -29239,6 +29281,15 @@ __metadata: languageName: node linkType: hard +"widest-line@npm:^5.0.0": + version: 5.0.0 + resolution: "widest-line@npm:5.0.0" + dependencies: + string-width: "npm:^7.0.0" + checksum: 10c0/6bd6cca8cda502ef50e05353fd25de0df8c704ffc43ada7e0a9cf9a5d4f4e12520485d80e0b77cec8a21f6c3909042fcf732aa9281e5dbb98cc9384a138b2578 + languageName: node + linkType: hard + "wildcard@npm:^2.0.0": version: 2.0.1 resolution: "wildcard@npm:2.0.1" @@ -29318,6 +29369,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^9.0.0": + version: 9.0.0 + resolution: "wrap-ansi@npm:9.0.0" + dependencies: + ansi-styles: "npm:^6.2.1" + string-width: "npm:^7.0.0" + strip-ansi: "npm:^7.1.0" + checksum: 10c0/a139b818da9573677548dd463bd626a5a5286271211eb6e4e82f34a4f643191d74e6d4a9bb0a3c26ec90e6f904f679e0569674ac099ea12378a8b98e20706066 + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" From 8c97afac844ab32bcb4ef7664ae2ba3b385b8594 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Mon, 2 Sep 2024 17:20:45 +0200 Subject: [PATCH 05/26] Many improvements --- code/addons/vitest/package.json | 2 + code/addons/vitest/src/postinstall-logger.ts | 13 +- code/addons/vitest/src/postinstall.ts | 197 ++++++++++++++----- code/yarn.lock | 13 +- 4 files changed, 174 insertions(+), 51 deletions(-) diff --git a/code/addons/vitest/package.json b/code/addons/vitest/package.json index 236b5e79fa4f..5f36b3871e33 100644 --- a/code/addons/vitest/package.json +++ b/code/addons/vitest/package.json @@ -73,9 +73,11 @@ "@storybook/csf": "^0.1.11" }, "devDependencies": { + "@types/semver": "^7", "@vitest/browser": "^2.0.0", "boxen": "^8.0.1", "find-up": "^7.0.0", + "semver": "^7.6.3", "tinyrainbow": "^1.2.0", "ts-dedent": "^2.2.0", "vitest": "^2.0.0" diff --git a/code/addons/vitest/src/postinstall-logger.ts b/code/addons/vitest/src/postinstall-logger.ts index b195d0923bea..8bb7df027c0b 100644 --- a/code/addons/vitest/src/postinstall-logger.ts +++ b/code/addons/vitest/src/postinstall-logger.ts @@ -1,17 +1,24 @@ -import { logger } from 'storybook/internal/node-logger'; +import { colors, logger } from 'storybook/internal/node-logger'; import boxen, { type Options } from 'boxen'; +const fancy = + process.platform !== 'win32' || process.env.CI || process.env.TERM === 'xterm-256color'; + +export const step = colors.gray('β€Ί'); +export const info = colors.blue(fancy ? 'β„Ή' : 'i'); +export const success = colors.green(fancy ? 'βœ”' : '√'); +export const warning = colors.orange(fancy ? '⚠' : 'β€Ό'); +export const error = colors.red(fancy ? 'βœ–' : 'Γ—'); + const baseOptions: Options = { borderStyle: 'round', padding: 1, - titleAlignment: 'left', }; export const print = (message: string, options: Options) => { logger.line(1); logger.plain(boxen(message, { ...baseOptions, ...options })); - logger.line(1); }; export const printInfo = (title: string, message: string, options?: Options) => diff --git a/code/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts index 43daea63f695..dc8979abb57d 100644 --- a/code/addons/vitest/src/postinstall.ts +++ b/code/addons/vitest/src/postinstall.ts @@ -1,7 +1,7 @@ import { existsSync } from 'node:fs'; import * as fs from 'node:fs/promises'; import { writeFile } from 'node:fs/promises'; -import { dirname, join, relative, resolve } from 'node:path'; +import { dirname, join, relative } from 'node:path'; import * as path from 'node:path'; import { @@ -14,20 +14,23 @@ import { import { colors, logger } from 'storybook/internal/node-logger'; import { findUp } from 'find-up'; +import { satisfies } from 'semver'; import c from 'tinyrainbow'; import dedent from 'ts-dedent'; import { type PostinstallOptions } from '../../../lib/cli-storybook/src/add'; -import { printError, printInfo, printSuccess } from './postinstall-logger'; +import { printError, printInfo, printSuccess, step } from './postinstall-logger'; const addonName = '@storybook/experimental-addon-vitest'; +const dependencies = ['vitest', '@vitest/browser', 'playwright']; +const optionalDependencies = ['@vitest/coverage-istanbul', '@vitest/coverage-v8']; const extensions = ['.js', '.jsx', '.ts', '.tsx', '.cts', '.mts', '.cjs', '.mjs']; export default async function postInstall(options: PostinstallOptions) { printSuccess( 'πŸ‘‹ Howdy!', dedent` - I'm the installation helper for ${colors.orange.bold(addonName)} + I'm the installation helper for ${colors.pink.bold(addonName)} Hold on for a moment while I look at your project and get you all set up... ` @@ -39,21 +42,6 @@ export default async function postInstall(options: PostinstallOptions) { const info = await getFrameworkInfo(options); - if ( - info.frameworkPackageName !== '@storybook/nextjs' && - info.builderPackageName !== '@storybook/builder-vite' - ) { - printError( - '⛔️ Sorry!', - dedent` - The Vitest addon can only be used with a Vite-based Storybook framework or Next.js. - - To roll back the installation, remove ${colors.orange.bold(addonName)} from the addons in your main Storybook config file and your package.json file. - ` - ); - return; - } - const annotationsImport = [ '@storybook/nextjs', '@storybook/experimental-nextjs-vite', @@ -67,57 +55,151 @@ export default async function postInstall(options: PostinstallOptions) { ? info.rendererPackageName : null; - if (!annotationsImport) { - printError( - '⛔️ Sorry!', - dedent` - The Vitest addon cannot yet be used with ${colors.orange.bold(info.frameworkPackageName)} + const prerequisiteCheck = async () => { + const reasons = []; - To roll back the installation, remove ${colors.orange.bold(addonName)} from the addons in your main Storybook config file and your package.json file. - ` - ); + if ( + info.frameworkPackageName !== '@storybook/nextjs' && + info.builderPackageName !== '@storybook/builder-vite' + ) { + reasons.push( + 'The Vitest addon can only be used with a Vite-based Storybook framework or Next.js.' + ); + } + + if (!annotationsImport) { + reasons.push(dedent` + The Vitest addon cannot yet be used with ${colors.pink.bold(info.frameworkPackageName)} + `); + } + + const vitestVersion = await packageManager.getInstalledVersion('vitest'); + if (vitestVersion && !satisfies(vitestVersion, '>=2.0.0')) { + reasons.push(` + The Vitest addon requires Vitest 2.0.0 or later. + Please update your ${colors.pink.bold('vitest')} dependency and try again. + `); + } else { + const depRanges = await packageManager.getAllDependencies(); + const checkDependencies = [...dependencies, ...optionalDependencies]; + const latestVersions = await packageManager.getVersions(...checkDependencies); + const latestPackages = checkDependencies.map( + (pkg, index) => [pkg, latestVersions[index].replace('^', '')] as const + ); + + let hasInconsistentPackageVersions = false; + for (const [pkg, latestVersion] of latestPackages) { + if (depRanges[pkg]) { + if (!dependencies.includes(pkg)) { + // Add found optional dependency so it will be updated to the latest version + dependencies.push(pkg); + } + + if (!satisfies(latestVersion, depRanges[pkg])) { + hasInconsistentPackageVersions = true; + reasons.push(dedent` + The package ${colors.pink.bold(pkg)} is already installed and cannot be updated to ${colors.pink.bold(latestVersion)} because it would not satisfy "${colors.pink.bold(depRanges[pkg])}". + `); + } + } + } + + if (hasInconsistentPackageVersions) { + reasons.push( + 'Update your dependencies and try again, or manually install the Vitest addon.' + ); + } + } + + if (info.frameworkPackageName === '@storybook/nextjs') { + const nextVersion = await packageManager.getInstalledVersion('next'); + if (!nextVersion) { + reasons.push(dedent` + It seems like you are using ${colors.pink.bold('@storybook/nextjs')} without having ${colors.pink.bold('next')} installed. + Please install "next" or use a different Storybook framework integration and try again. + `); + } + } + + if (reasons.length > 0) { + reasons.unshift( + 'The Test addon is incompatible with your current set up and cannot be installed:' + ); + reasons.push( + dedent` + To roll back the installation, remove ${colors.pink.bold(addonName)} from the "addons" array + in your main Storybook config file and remove the dependency from your package.json file. + ` + ); + reasons.push( + dedent` + Please check the documentation for more information about its requirements and installation: + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} + ` + ); + return reasons.map((r) => r.trim()).join('\n\n'); + } + + return null; + }; + + const result = await prerequisiteCheck(); + + if (result) { + printError('⛔️ Sorry!', result); + logger.line(1); return; } const vitestInfo = getVitestPluginInfo(info.frameworkPackageName); - const packages = ['vitest@latest', '@vitest/browser@latest', 'playwright@latest']; - if (info.frameworkPackageName === '@storybook/nextjs') { printInfo( - '🍦 Just so you know...', + '🍿 Just so you know...', dedent` It looks like you're using Next.js. - I'll add the ${colors.orange.bold(`vite-plugin-storybook-nextjs`)} plugin so you can use it with Vitest. + I'll add the ${colors.pink.bold(`vite-plugin-storybook-nextjs`)} plugin so you can use it with Vitest. ` ); - packages.push('vite-plugin-storybook-nextjs@latest'); + dependencies.push('vite-plugin-storybook-nextjs'); } - logger.plain(c.bold('βœ“ Ready to install packages:')); - logger.plain(packages.join(', ')); - await packageManager.addDependencies({ installAsDevDependencies: true }, packages); + logger.line(1); + logger.plain(`${step} Installing dependencies:`); + logger.plain(colors.gray(' ' + dependencies.join(', '))); + + await packageManager.addDependencies( + { installAsDevDependencies: true }, + dependencies.map((p) => `${p}@latest`) + ); + + logger.line(1); + logger.plain(`${step} Configuring Playwright with Chromium:`); + logger.plain(colors.gray(' npx playwright install chromium --with-deps')); - logger.plain(c.bold('Executing npx playwright install chromium --with-deps ...')); await packageManager.executeCommand({ command: 'npx', args: ['playwright', 'install', 'chromium', '--with-deps'], }); - logger.plain(c.bold(`Writing ${c.cyan`.storybook/vitest.setup.ts`} file...`)); + const vitestSetupFile = join(options.configDir, 'vitest.setup.ts'); + logger.line(1); + logger.plain(`${step} Creating a Vitest setup file for Storybook:`); + logger.plain(colors.gray(` ${vitestSetupFile}`)); const previewExists = extensions .map((ext) => path.resolve(options.configDir, `preview${ext}`)) .some((config) => existsSync(config)); await writeFile( - resolve(options.configDir, 'vitest.setup.ts'), + vitestSetupFile, dedent` import { beforeAll } from 'vitest' import { setProjectAnnotations } from '${annotationsImport}' ${previewExists ? `import * as projectAnnotations from './preview'` : ''} + // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations const project = setProjectAnnotations(${previewExists ? 'projectAnnotations' : '[]'}) beforeAll(project.beforeAll) @@ -133,32 +215,44 @@ export default async function postInstall(options: PostinstallOptions) { if (rootConfig) { // If there's an existing config, we create a workspace file so we can run Storybook tests alongside. const extname = path.extname(rootConfig); - const browserWorkspaceFile = resolve(dirname(rootConfig), `vitest.workspace${extname}`); + const browserWorkspaceFile = path.resolve(dirname(rootConfig), `vitest.workspace${extname}`); if (existsSync(browserWorkspaceFile)) { - logger.plain( + printError( + '🚨 Oh no!', dedent` - We can not automatically setup the plugin when you use Vitest with workspaces. + Found an existing Vitest workspace file: + ${colors.gray(browserWorkspaceFile)} + + I cannot safely extend your existing workspace file automatically, you must do it yourself. + Please refer to the documentation to complete the setup manually: - ${c.yellow`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} ` ); + logger.line(1); return; } else { - logger.plain(c.bold(`Writing ${c.cyan`vitest.workspace.ts`} file...`)); + logger.line(1); + logger.plain(`${step} Creating a Vitest project workspace file:`); + logger.plain(colors.gray(` ${browserWorkspaceFile}`)); + await writeFile( browserWorkspaceFile, dedent` import { defineWorkspace } from 'vitest/config'; import { storybookTest } from '@storybook/experimental-addon-vitest/plugin'; ${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''} + + // More info at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest export default defineWorkspace([ '${relative(dirname(browserWorkspaceFile), rootConfig)}', { extends: '${viteConfig ? relative(dirname(browserWorkspaceFile), viteConfig) : ''}', plugins: [ - storybookTest(),${vitestInfo.frameworkPluginCall ? '\n' + vitestInfo.frameworkPluginCall : ''} + storybookTest(),${vitestInfo.frameworkPluginCall ? '\n' + vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall : ''} ], test: { + name: 'storybook', browser: { enabled: true, headless: true, @@ -175,16 +269,21 @@ export default async function postInstall(options: PostinstallOptions) { } } else { // If there's no existing Vitest/Vite config, we create a new Vitest config file. - logger.plain(c.bold(`Writing ${c.cyan`vitest.config.ts`} file...`)); + logger.line(1); + logger.plain(`${step} Creating a Vitest project config file:`); + logger.plain(colors.gray(` vitest.config.ts`)); + await writeFile( - resolve('vitest.config.ts'), + 'vitest.config.ts', dedent` import { defineConfig } from "vitest/config"; import { storybookTest } from "@storybook/experimental-addon-vitest/plugin"; ${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''} + + // More info at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest export default defineConfig({ plugins: [ - storybookTest(),${vitestInfo.frameworkPluginCall ? '\n' + vitestInfo.frameworkPluginCall : ''} + storybookTest(),${vitestInfo.frameworkPluginCall ? '\n' + vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall : ''} ], test: { browser: { @@ -210,15 +309,19 @@ export default async function postInstall(options: PostinstallOptions) { ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} ` ); + logger.line(1); } const getVitestPluginInfo = (framework: string) => { let frameworkPluginImport = ''; let frameworkPluginCall = ''; + let frameworkPluginDocs = ''; if (framework === '@storybook/nextjs') { frameworkPluginImport = "import vitePluginNext from 'vite-plugin-storybook-nextjs'"; frameworkPluginCall = 'vitePluginNext()'; + frameworkPluginDocs = + '// More info at: https://github.com/storybookjs/vite-plugin-storybook-nextjs\n'; } if (framework === '@storybook/sveltekit') { @@ -231,7 +334,7 @@ const getVitestPluginInfo = (framework: string) => { frameworkPluginCall = 'storybookVuePlugin()'; } - return { frameworkPluginImport, frameworkPluginCall }; + return { frameworkPluginImport, frameworkPluginCall, frameworkPluginDocs }; }; async function getFrameworkInfo({ configDir, packageManager: pkgMgr }: PostinstallOptions) { diff --git a/code/yarn.lock b/code/yarn.lock index 431048a5adfe..1edd2929bf89 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6131,9 +6131,11 @@ __metadata: resolution: "@storybook/experimental-addon-vitest@workspace:addons/vitest" dependencies: "@storybook/csf": "npm:^0.1.11" + "@types/semver": "npm:^7" "@vitest/browser": "npm:^2.0.0" boxen: "npm:^8.0.1" find-up: "npm:^7.0.0" + semver: "npm:^7.6.3" tinyrainbow: "npm:^1.2.0" ts-dedent: "npm:^2.2.0" vitest: "npm:^2.0.0" @@ -8176,7 +8178,7 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0, @types/semver@npm:^7.5.6, @types/semver@npm:^7.5.8": +"@types/semver@npm:^7, @types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.0, @types/semver@npm:^7.5.6, @types/semver@npm:^7.5.8": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" checksum: 10c0/8663ff927234d1c5fcc04b33062cb2b9fcfbe0f5f351ed26c4d1e1581657deebd506b41ff7fdf89e787e3d33ce05854bc01686379b89e9c49b564c4cfa988efa @@ -25351,6 +25353,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.6.3": + version: 7.6.3 + resolution: "semver@npm:7.6.3" + bin: + semver: bin/semver.js + checksum: 10c0/88f33e148b210c153873cb08cfe1e281d518aaa9a666d4d148add6560db5cd3c582f3a08ccb91f38d5f379ead256da9931234ed122057f40bb5766e65e58adaf + languageName: node + linkType: hard + "send@npm:0.18.0": version: 0.18.0 resolution: "send@npm:0.18.0" From d812d4dd7f2b4b96a46ac2ae69d7fd75e135ba62 Mon Sep 17 00:00:00 2001 From: Vitor Augusto Pinheiro <26413854+Vitorgus@users.noreply.github.com> Date: Mon, 2 Sep 2024 19:13:01 -0300 Subject: [PATCH 06/26] renamed vitest addon import file name to 'test' --- scripts/tasks/sandbox-parts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index 10a61d8dcec6..971bcb7e27c3 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -207,7 +207,7 @@ function addEsbuildLoaderToStories(mainConfig: ConfigFile) { }, }, // Handle MDX files per the addon-docs presets (ish) - { + { test: /template-stories\\/.*\\.mdx$/, exclude: /\\.stories\\.mdx$/, use: [ @@ -512,7 +512,7 @@ export async function setupVitest(details: TemplateDetails, options: PassedOptio // This workaround is needed because Vitest seems to have issues in link mode // so the /setup-file and /global-setup files from the vitest addon won't work in portal protocol if (options.link) { - const vitestAddonPath = relative(sandboxDir, join(CODE_DIRECTORY, 'addons', 'vitest')); + const vitestAddonPath = relative(sandboxDir, join(CODE_DIRECTORY, 'addons', 'test')); packageJson.resolutions = { ...packageJson.resolutions, '@storybook/experimental-addon-test': `file:${vitestAddonPath}`, From 7ca2804345896ca6f36d4c677064c70ae7c1d923 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 3 Sep 2024 11:54:27 +0200 Subject: [PATCH 07/26] Bail on auto config when a vitest workspace file exists or a vite config file exists with a 'test' property --- code/addons/test/src/postinstall.ts | 131 ++++++++++++++++++---------- 1 file changed, 85 insertions(+), 46 deletions(-) diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index 0f583f56892f..9e78176cf3dc 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -26,6 +26,8 @@ const dependencies = ['vitest', '@vitest/browser', 'playwright']; const optionalDependencies = ['@vitest/coverage-istanbul', '@vitest/coverage-v8']; const extensions = ['.js', '.jsx', '.ts', '.tsx', '.cts', '.mts', '.cjs', '.mjs']; +const findFile = async (basename: string) => findUp(extensions.map((ext) => basename + ext)); + export default async function postInstall(options: PostinstallOptions) { printSuccess( 'πŸ‘‹ Howdy!', @@ -191,6 +193,21 @@ export default async function postInstall(options: PostinstallOptions) { }); const vitestSetupFile = join(options.configDir, 'vitest.setup.ts'); + if (existsSync(vitestSetupFile)) { + printError( + '🚨 Oh no!', + dedent` + Found an existing Vitest setup file: + ${colors.gray(vitestSetupFile)} + + Please refer to the documentation to complete the setup manually: + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} + ` + ); + logger.line(1); + return; + } + logger.line(1); logger.plain(`${step} Creating a Vitest setup file for Storybook:`); logger.plain(colors.gray(` ${vitestSetupFile}`)); @@ -213,24 +230,37 @@ export default async function postInstall(options: PostinstallOptions) { ` ); - // Check for an existing config file. Can be from Vitest (preferred) or Vite (with `test` option). - const viteConfigFiles = extensions.map((ext) => 'vite.config' + ext); - const viteConfig = await findUp(viteConfigFiles, { cwd: process.cwd() }); - const vitestConfigFiles = extensions.map((ext) => 'vitest.config' + ext); - const rootConfig = (await findUp(vitestConfigFiles, { cwd: process.cwd() })) || viteConfig; + // Check for existing Vitest workspace. We can't extend it so manual setup is required. + const vitestWorkspaceFile = await findFile('vitest.workspace'); + if (vitestWorkspaceFile) { + printError( + '🚨 Oh no!', + dedent` + Found an existing Vitest workspace file: + ${colors.gray(vitestWorkspaceFile)} - if (rootConfig) { - // If there's an existing config, we create a workspace file so we can run Storybook tests alongside. - const extname = path.extname(rootConfig); - const browserWorkspaceFile = path.resolve(dirname(rootConfig), `vitest.workspace${extname}`); - if (existsSync(browserWorkspaceFile)) { + I cannot safely extend your existing workspace file automatically. + + Please refer to the documentation to complete the setup manually: + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} + ` + ); + logger.line(1); + return; + } + + // Check for an existing config file. Can be from Vitest (preferred) or Vite (with `test` option). + const viteConfigFile = await findFile('vite.config'); + if (viteConfigFile) { + const viteConfig = await fs.readFile(viteConfigFile, 'utf8'); + if (viteConfig.match(/\Wtest:\s*{/)) { printError( '🚨 Oh no!', dedent` - Found an existing Vitest workspace file: - ${colors.gray(browserWorkspaceFile)} + You seem to have an existing test configuration in your Vite config file: + ${colors.gray(vitestWorkspaceFile)} - I cannot safely extend your existing workspace file automatically, you must do it yourself. + I cannot safely extend your test configuration automatically. Please refer to the documentation to complete the setup manually: ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} @@ -238,42 +268,51 @@ export default async function postInstall(options: PostinstallOptions) { ); logger.line(1); return; - } else { - logger.line(1); - logger.plain(`${step} Creating a Vitest project workspace file:`); - logger.plain(colors.gray(` ${browserWorkspaceFile}`)); + } + } - await writeFile( - browserWorkspaceFile, - dedent` - import { defineWorkspace } from 'vitest/config'; - import { storybookTest } from '@storybook/experimental-addon-test/vite-plugin'; - ${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''} - - // More info at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest - export default defineWorkspace([ - '${relative(dirname(browserWorkspaceFile), rootConfig)}', - { - extends: '${viteConfig ? relative(dirname(browserWorkspaceFile), viteConfig) : ''}', - plugins: [ - storybookTest(),${vitestInfo.frameworkPluginCall ? '\n ' + vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall : ''} - ], - test: { - name: 'storybook', - browser: { - enabled: true, - headless: true, - name: 'chromium', - provider: 'playwright', - }, - include: ['**/*.stories.?(m)[jt]s?(x)'], - setupFiles: ['./.storybook/vitest.setup.ts'], + const vitestConfigFile = await findFile('vitest.config'); + const rootConfig = vitestConfigFile || viteConfigFile; + + if (rootConfig) { + // If there's an existing config, we create a workspace file so we can run Storybook tests alongside. + const extname = path.extname(rootConfig); + const browserWorkspaceFile = path.resolve(dirname(rootConfig), `vitest.workspace${extname}`); + + logger.line(1); + logger.plain(`${step} Creating a Vitest project workspace file:`); + logger.plain(colors.gray(` ${browserWorkspaceFile}`)); + + await writeFile( + browserWorkspaceFile, + dedent` + import { defineWorkspace } from 'vitest/config'; + import { storybookTest } from '@storybook/experimental-addon-test/vite-plugin'; + ${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''} + + // More info at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest + export default defineWorkspace([ + '${relative(dirname(browserWorkspaceFile), rootConfig)}', + { + extends: '${viteConfigFile ? relative(dirname(browserWorkspaceFile), viteConfigFile) : ''}', + plugins: [ + storybookTest(),${vitestInfo.frameworkPluginCall ? '\n ' + vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall : ''} + ], + test: { + name: 'storybook', + browser: { + enabled: true, + headless: true, + name: 'chromium', + provider: 'playwright', }, + include: ['**/*.stories.?(m)[jt]s?(x)'], + setupFiles: ['./.storybook/vitest.setup.ts'], }, - ]); - `.replace(/\s+extends: '',/, '') - ); - } + }, + ]); + `.replace(/\s+extends: '',/, '') + ); } else { // If there's no existing Vitest/Vite config, we create a new Vitest config file. logger.line(1); From 22d1814d35a0ed388f0fe7c3603448ea54a526f6 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 3 Sep 2024 12:05:15 +0200 Subject: [PATCH 08/26] Consistently use absolute paths --- code/addons/test/src/postinstall.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index 9e78176cf3dc..3a3aa5b7b2e8 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -192,7 +192,7 @@ export default async function postInstall(options: PostinstallOptions) { args: ['playwright', 'install', 'chromium', '--with-deps'], }); - const vitestSetupFile = join(options.configDir, 'vitest.setup.ts'); + const vitestSetupFile = path.resolve(options.configDir, 'vitest.setup.ts'); if (existsSync(vitestSetupFile)) { printError( '🚨 Oh no!', @@ -315,12 +315,14 @@ export default async function postInstall(options: PostinstallOptions) { ); } else { // If there's no existing Vitest/Vite config, we create a new Vitest config file. + const newVitestConfigFile = path.resolve('vitest.config.ts'); + logger.line(1); logger.plain(`${step} Creating a Vitest project config file:`); - logger.plain(colors.gray(` vitest.config.ts`)); + logger.plain(colors.gray(` ${newVitestConfigFile}`)); await writeFile( - 'vitest.config.ts', + newVitestConfigFile, dedent` import { defineConfig } from "vitest/config"; import { storybookTest } from "@storybook/experimental-addon-test/vite-plugin"; From c38d9611182b91fba105507dbaf6c9d8e84eff37 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 3 Sep 2024 12:20:43 +0200 Subject: [PATCH 09/26] CONSTANT_CASE for constants --- code/addons/test/src/postinstall.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index 3a3aa5b7b2e8..c65560099dc6 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -21,18 +21,16 @@ import dedent from 'ts-dedent'; import { type PostinstallOptions } from '../../../lib/cli-storybook/src/add'; import { printError, printInfo, printSuccess, step } from './postinstall-logger'; -const addonName = '@storybook/experimental-addon-vitest'; -const dependencies = ['vitest', '@vitest/browser', 'playwright']; -const optionalDependencies = ['@vitest/coverage-istanbul', '@vitest/coverage-v8']; -const extensions = ['.js', '.jsx', '.ts', '.tsx', '.cts', '.mts', '.cjs', '.mjs']; +const ADDON_NAME = '@storybook/experimental-addon-vitest' as const; +const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.cts', '.mts', '.cjs', '.mjs'] as const; -const findFile = async (basename: string) => findUp(extensions.map((ext) => basename + ext)); +const findFile = async (basename: string) => findUp(EXTENSIONS.map((ext) => basename + ext)); export default async function postInstall(options: PostinstallOptions) { printSuccess( 'πŸ‘‹ Howdy!', dedent` - I'm the installation helper for ${colors.pink.bold(addonName)} + I'm the installation helper for ${colors.pink.bold(ADDON_NAME)} Hold on for a moment while I look at your project and get you all set up... ` @@ -43,6 +41,8 @@ export default async function postInstall(options: PostinstallOptions) { }); const info = await getFrameworkInfo(options); + const dependencies = ['vitest', '@vitest/browser', 'playwright']; + const optionalDependencies = ['@vitest/coverage-istanbul', '@vitest/coverage-v8']; const annotationsImport = [ '@storybook/nextjs', @@ -129,7 +129,7 @@ export default async function postInstall(options: PostinstallOptions) { ); reasons.push( dedent` - To roll back the installation, remove ${colors.pink.bold(addonName)} from the "addons" array + To roll back the installation, remove ${colors.pink.bold(ADDON_NAME)} from the "addons" array in your main Storybook config file and remove the dependency from your package.json file. ` ); @@ -212,9 +212,9 @@ export default async function postInstall(options: PostinstallOptions) { logger.plain(`${step} Creating a Vitest setup file for Storybook:`); logger.plain(colors.gray(` ${vitestSetupFile}`)); - const previewExists = extensions - .map((ext) => path.resolve(options.configDir, `preview${ext}`)) - .some((config) => existsSync(config)); + const previewExists = EXTENSIONS.map((ext) => + path.resolve(options.configDir, `preview${ext}`) + ).some((config) => existsSync(config)); await writeFile( vitestSetupFile, From 07843b6a911057b5c1b4ee325bb5ebe7848fc492 Mon Sep 17 00:00:00 2001 From: Gert Hengeveld Date: Tue, 3 Sep 2024 12:26:53 +0200 Subject: [PATCH 10/26] Consistently naming the 'Storybook Test' addon --- code/addons/test/src/postinstall.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index c65560099dc6..8208426ea6ae 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -32,7 +32,7 @@ export default async function postInstall(options: PostinstallOptions) { dedent` I'm the installation helper for ${colors.pink.bold(ADDON_NAME)} - Hold on for a moment while I look at your project and get you all set up... + Hold on for a moment while I look at your project and get it set up... ` ); @@ -65,20 +65,20 @@ export default async function postInstall(options: PostinstallOptions) { info.builderPackageName !== '@storybook/builder-vite' ) { reasons.push( - 'The Vitest addon can only be used with a Vite-based Storybook framework or Next.js.' + 'The Storybook Test addon can only be used with a Vite-based Storybook framework or Next.js.' ); } if (!annotationsImport) { reasons.push(dedent` - The Vitest addon cannot yet be used with ${colors.pink.bold(info.frameworkPackageName)} + The Storybook Test addon cannot yet be used with ${colors.pink.bold(info.frameworkPackageName)} `); } const vitestVersion = await packageManager.getInstalledVersion('vitest'); if (vitestVersion && !satisfies(vitestVersion, '>=2.0.0')) { reasons.push(` - The Vitest addon requires Vitest 2.0.0 or later. + The Storybook Test addon requires Vitest 2.0.0 or later. Please update your ${colors.pink.bold('vitest')} dependency and try again. `); } else { @@ -108,7 +108,7 @@ export default async function postInstall(options: PostinstallOptions) { if (hasInconsistentPackageVersions) { reasons.push( - 'Update your dependencies and try again, or manually install the Vitest addon.' + 'Update your dependencies and try again, or manually install the Storybook Test addon.' ); } } @@ -125,7 +125,7 @@ export default async function postInstall(options: PostinstallOptions) { if (reasons.length > 0) { reasons.unshift( - 'The Test addon is incompatible with your current set up and cannot be installed:' + 'The Storybook Test addon is incompatible with your current set up and cannot be installed:' ); reasons.push( dedent` @@ -351,7 +351,7 @@ export default async function postInstall(options: PostinstallOptions) { printSuccess( 'πŸŽ‰ All done!', dedent` - The Test addon is now configured and you're ready to run your tests! + The Storybook Test addon is now configured and you're ready to run your tests! Check the documentation for more information about its features and options at: ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} From 493a9174248e0ce0823add470b622ceb78d9b11a Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 3 Sep 2024 16:02:55 +0200 Subject: [PATCH 11/26] additional fixes --- code/addons/test/src/postinstall.ts | 85 +++++++++++++---------------- 1 file changed, 38 insertions(+), 47 deletions(-) diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index 8208426ea6ae..8ae37bd55fcc 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -21,7 +21,7 @@ import dedent from 'ts-dedent'; import { type PostinstallOptions } from '../../../lib/cli-storybook/src/add'; import { printError, printInfo, printSuccess, step } from './postinstall-logger'; -const ADDON_NAME = '@storybook/experimental-addon-vitest' as const; +const ADDON_NAME = '@storybook/experimental-addon-test' as const; const EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.cts', '.mts', '.cjs', '.mjs'] as const; const findFile = async (basename: string) => findUp(EXTENSIONS.map((ext) => basename + ext)); @@ -43,6 +43,9 @@ export default async function postInstall(options: PostinstallOptions) { const info = await getFrameworkInfo(options); const dependencies = ['vitest', '@vitest/browser', 'playwright']; const optionalDependencies = ['@vitest/coverage-istanbul', '@vitest/coverage-v8']; + const vitestVersion = await packageManager.getInstalledVersion('vitest'); + // if Vitest is installed, we use the same version to keep consistency across Vitest packages + const vitestVersionToInstall = vitestVersion ?? 'latest'; const annotationsImport = [ '@storybook/nextjs', @@ -75,42 +78,11 @@ export default async function postInstall(options: PostinstallOptions) { `); } - const vitestVersion = await packageManager.getInstalledVersion('vitest'); - if (vitestVersion && !satisfies(vitestVersion, '>=2.0.0')) { + if (vitestVersion && !satisfies(vitestVersion as string, '>=2.0.0')) { reasons.push(` The Storybook Test addon requires Vitest 2.0.0 or later. Please update your ${colors.pink.bold('vitest')} dependency and try again. `); - } else { - const depRanges = await packageManager.getAllDependencies(); - const checkDependencies = [...dependencies, ...optionalDependencies]; - const latestVersions = await packageManager.getVersions(...checkDependencies); - const latestPackages = checkDependencies.map( - (pkg, index) => [pkg, latestVersions[index].replace('^', '')] as const - ); - - let hasInconsistentPackageVersions = false; - for (const [pkg, latestVersion] of latestPackages) { - if (depRanges[pkg]) { - if (!dependencies.includes(pkg)) { - // Add found optional dependency so it will be updated to the latest version - dependencies.push(pkg); - } - - if (!satisfies(latestVersion, depRanges[pkg])) { - hasInconsistentPackageVersions = true; - reasons.push(dedent` - The package ${colors.pink.bold(pkg)} is already installed and cannot be updated to ${colors.pink.bold(latestVersion)} because it would not satisfy "${colors.pink.bold(depRanges[pkg])}". - `); - } - } - } - - if (hasInconsistentPackageVersions) { - reasons.push( - 'Update your dependencies and try again, or manually install the Storybook Test addon.' - ); - } } if (info.frameworkPackageName === '@storybook/nextjs') { @@ -153,6 +125,13 @@ export default async function postInstall(options: PostinstallOptions) { return; } + const allDeps = await packageManager.getAllDependencies(); + optionalDependencies.forEach((dep) => { + if (allDeps[dep]) { + dependencies.push(dep); + } + }); + const vitestInfo = getVitestPluginInfo(info.frameworkPackageName); if (info.frameworkPackageName === '@storybook/nextjs') { @@ -162,6 +141,8 @@ export default async function postInstall(options: PostinstallOptions) { It looks like you're using Next.js. I'll add ${colors.pink.bold(`@storybook/experimental-nextjs-vite/vite-plugin`)} so you can use it with Vitest. + + More info about the plugin at: ${c.cyan`https://github.com/storybookjs/vite-plugin-storybook-nextjs`} ` ); try { @@ -175,12 +156,12 @@ export default async function postInstall(options: PostinstallOptions) { } logger.line(1); - logger.plain(`${step} Installing dependencies:`); + logger.plain(`${step} Installing/updating dependencies:`); logger.plain(colors.gray(' ' + dependencies.join(', '))); await packageManager.addDependencies( { installAsDevDependencies: true }, - dependencies.map((p) => `${p}@latest`) + dependencies.map((p) => `${p}@${p.includes('vitest') ? vitestVersionToInstall : 'latest'}`) ); logger.line(1); @@ -223,6 +204,7 @@ export default async function postInstall(options: PostinstallOptions) { import { setProjectAnnotations } from '${annotationsImport}' ${previewExists ? `import * as projectAnnotations from './preview'` : ''} + // This is an important step to apply the right configuration when testing your stories. // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations const project = setProjectAnnotations(${previewExists ? 'projectAnnotations' : '[]'}) @@ -239,7 +221,8 @@ export default async function postInstall(options: PostinstallOptions) { Found an existing Vitest workspace file: ${colors.gray(vitestWorkspaceFile)} - I cannot safely extend your existing workspace file automatically. + I was able to configure most of the addon but could not safely extend + your existing workspace file automatically, you must do it yourself. This was the last step. Please refer to the documentation to complete the setup manually: ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} @@ -260,7 +243,8 @@ export default async function postInstall(options: PostinstallOptions) { You seem to have an existing test configuration in your Vite config file: ${colors.gray(vitestWorkspaceFile)} - I cannot safely extend your test configuration automatically. + I was able to configure most of the addon but could not safely extend + your existing workspace file automatically, you must do it yourself. This was the last step. Please refer to the documentation to complete the setup manually: ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} @@ -287,8 +271,7 @@ export default async function postInstall(options: PostinstallOptions) { browserWorkspaceFile, dedent` import { defineWorkspace } from 'vitest/config'; - import { storybookTest } from '@storybook/experimental-addon-test/vite-plugin'; - ${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''} + import { storybookTest } from '@storybook/experimental-addon-test/vite-plugin';${vitestInfo.frameworkPluginImport} // More info at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest export default defineWorkspace([ @@ -296,7 +279,7 @@ export default async function postInstall(options: PostinstallOptions) { { extends: '${viteConfigFile ? relative(dirname(browserWorkspaceFile), viteConfigFile) : ''}', plugins: [ - storybookTest(),${vitestInfo.frameworkPluginCall ? '\n ' + vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall : ''} + storybookTest(),${vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall} ], test: { name: 'storybook', @@ -306,6 +289,7 @@ export default async function postInstall(options: PostinstallOptions) { name: 'chromium', provider: 'playwright', }, + // Make sure to adjust this pattern to match your stories files. include: ['**/*.stories.?(m)[jt]s?(x)'], setupFiles: ['./.storybook/vitest.setup.ts'], }, @@ -325,21 +309,22 @@ export default async function postInstall(options: PostinstallOptions) { newVitestConfigFile, dedent` import { defineConfig } from "vitest/config"; - import { storybookTest } from "@storybook/experimental-addon-test/vite-plugin"; - ${vitestInfo.frameworkPluginImport ? vitestInfo.frameworkPluginImport + '\n' : ''} + import { storybookTest } from '@storybook/experimental-addon-test/vite-plugin';${vitestInfo.frameworkPluginImport} // More info at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest export default defineConfig({ plugins: [ - storybookTest(),${vitestInfo.frameworkPluginCall ? '\n ' + vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall : ''} + storybookTest(),${vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall} ], test: { + name: 'storybook', browser: { enabled: true, headless: true, name: 'chromium', provider: 'playwright', }, + // Make sure to adjust this pattern to match your stories files. include: ['**/*.stories.?(m)[jt]s?(x)'], setupFiles: ['./.storybook/vitest.setup.ts'], }, @@ -367,23 +352,29 @@ const getVitestPluginInfo = (framework: string) => { if (framework === '@storybook/nextjs') { frameworkPluginImport = - "import { storybookNextJsPlugin } from '@storybook/experimental-nextjs-vite/vite-plugin'"; + "import { storybookNextJsPlugin } from '@storybook/experimental-nextjs-vite/vite-plugin';"; frameworkPluginCall = 'storybookNextJsPlugin()'; frameworkPluginDocs = - '// More info at: https://github.com/storybookjs/vite-plugin-storybook-nextjs\n '; + '// More info at: https://github.com/storybookjs/vite-plugin-storybook-nextjs\n'; } if (framework === '@storybook/sveltekit') { frameworkPluginImport = - "import { storybookSveltekitPlugin } from '@storybook/sveltekit/vite-plugin'"; + "import { storybookSveltekitPlugin } from '@storybook/sveltekit/vite-plugin';"; frameworkPluginCall = 'storybookSveltekitPlugin()'; } if (framework === '@storybook/vue3-vite') { - frameworkPluginImport = "import { storybookVuePlugin } from '@storybook/vue3-vite/vite-plugin'"; + frameworkPluginImport = + "import { storybookVuePlugin } from '@storybook/vue3-vite/vite-plugin';"; frameworkPluginCall = 'storybookVuePlugin()'; } + // spaces for file identation + frameworkPluginImport = `\n${frameworkPluginImport}`; + frameworkPluginDocs = frameworkPluginDocs ? `\n ${frameworkPluginDocs}` : ''; + frameworkPluginCall = frameworkPluginCall ? `\n ${frameworkPluginCall},` : ''; + return { frameworkPluginImport, frameworkPluginCall, frameworkPluginDocs }; }; From 581e00880c11ed87f8e68111c751e131564335d9 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 3 Sep 2024 18:11:46 +0200 Subject: [PATCH 12/26] add helpful links --- code/addons/test/src/postinstall.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index 8ae37bd55fcc..a4a16d58885f 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -279,6 +279,7 @@ export default async function postInstall(options: PostinstallOptions) { { extends: '${viteConfigFile ? relative(dirname(browserWorkspaceFile), viteConfigFile) : ''}', plugins: [ + // See options at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#storybooktest storybookTest(),${vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall} ], test: { @@ -314,6 +315,7 @@ export default async function postInstall(options: PostinstallOptions) { // More info at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest export default defineConfig({ plugins: [ + // See options at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#storybooktest storybookTest(),${vitestInfo.frameworkPluginDocs + vitestInfo.frameworkPluginCall} ], test: { @@ -353,9 +355,9 @@ const getVitestPluginInfo = (framework: string) => { if (framework === '@storybook/nextjs') { frameworkPluginImport = "import { storybookNextJsPlugin } from '@storybook/experimental-nextjs-vite/vite-plugin';"; - frameworkPluginCall = 'storybookNextJsPlugin()'; frameworkPluginDocs = - '// More info at: https://github.com/storybookjs/vite-plugin-storybook-nextjs\n'; + '// More info at: https://github.com/storybookjs/vite-plugin-storybook-nextjs'; + frameworkPluginCall = 'storybookNextJsPlugin()'; } if (framework === '@storybook/sveltekit') { From 2b2a83f21f1777aa4f19afc68f05c83879af82bb Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 3 Sep 2024 18:19:39 +0200 Subject: [PATCH 13/26] add explanation in sb add command --- code/lib/cli-storybook/src/add.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index a67128f911db..a2dbd23650bc 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -107,8 +107,8 @@ export async function add( } if (checkInstalled(addonName, requireMain(configDir))) { - logger.error(dedent` - The Storybook Addon "${addonName}" is already present in ${mainConfig}; Its configuration will be skipped. + logger.warn(dedent` + The Storybook addon "${addonName}" is already present in ${mainConfig}; Its configuration will be skipped. Please remove it and rerun this command if you want to reinstall this addon. `); return; } From 82281206f4917c92730f0bbdcf9cb02429bb1a9c Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 3 Sep 2024 20:23:46 +0200 Subject: [PATCH 14/26] use auto-retrying assertions in e2e tests --- code/e2e-tests/addon-docs.spec.ts | 18 ++++--- code/e2e-tests/addon-toolbars.spec.ts | 2 +- code/e2e-tests/tags.spec.ts | 74 ++++++++------------------- 3 files changed, 33 insertions(+), 61 deletions(-) diff --git a/code/e2e-tests/addon-docs.spec.ts b/code/e2e-tests/addon-docs.spec.ts index cce2c23c6082..ce3e281c2f26 100644 --- a/code/e2e-tests/addon-docs.spec.ts +++ b/code/e2e-tests/addon-docs.spec.ts @@ -179,7 +179,7 @@ test.describe('addon-docs', () => { const root = sbPage.previewRoot(); const stories = root.locator('.sb-story button'); - await expect(await stories.count()).toBe(3); + await expect(stories).toHaveCount(3); await expect(stories.first()).toHaveText('Basic'); await expect(stories.nth(1)).toHaveText('Basic'); await expect(stories.last()).toHaveText('Another'); @@ -265,12 +265,14 @@ test.describe('addon-docs', () => { const root = sbPage.previewRoot(); const storyHeadings = root.locator('.sb-anchor > h3'); - await expect(await storyHeadings.count()).toBe(6); - await expect(storyHeadings.nth(0)).toHaveText('Default A'); - await expect(storyHeadings.nth(1)).toHaveText('Span Content'); - await expect(storyHeadings.nth(2)).toHaveText('Code Content'); - await expect(storyHeadings.nth(3)).toHaveText('Default B'); - await expect(storyHeadings.nth(4)).toHaveText('H 1 Content'); - await expect(storyHeadings.nth(5)).toHaveText('H 2 Content'); + await expect(storyHeadings).toHaveCount(6); + await expect(storyHeadings).toHaveText([ + 'Default A', + 'Span Content', + 'Code Content', + 'Default B', + 'H 1 Content', + 'H 2 Content', + ]); }); }); diff --git a/code/e2e-tests/addon-toolbars.spec.ts b/code/e2e-tests/addon-toolbars.spec.ts index 493de06ce6f3..dea38ae7d720 100644 --- a/code/e2e-tests/addon-toolbars.spec.ts +++ b/code/e2e-tests/addon-toolbars.spec.ts @@ -32,6 +32,6 @@ test.describe('addon-toolbars', () => { await expect(sbPage.previewRoot()).toContainText('μ•ˆλ…•ν•˜μ„Έμš”'); const button = await sbPage.page.locator('[title="Internationalization locale"]'); - await expect(await button.getAttribute('disabled')).toBe(''); + await expect(button).toHaveAttribute('disabled', ''); }); }); diff --git a/code/e2e-tests/tags.spec.ts b/code/e2e-tests/tags.spec.ts index 3148f166104e..10048469fcdd 100644 --- a/code/e2e-tests/tags.spec.ts +++ b/code/e2e-tests/tags.spec.ts @@ -16,26 +16,16 @@ test.describe('tags', () => { await sbPage.navigateToStory('core/tags-config', 'docs'); // Sidebar should include dev-only and exclude docs-only and test-only - const devOnlyEntry = await page.locator('#core-tags-config--dev-only').all(); - expect(devOnlyEntry.length).toBe(1); - - const docsOnlyEntry = await page.locator('#core-tags-config--docs-only').all(); - expect(docsOnlyEntry.length).toBe(0); - - const testOnlyEntry = await page.locator('#core-tags-config--test-only').all(); - expect(testOnlyEntry.length).toBe(0); + await expect(page.locator('#core-tags-config--dev-only')).toHaveCount(1); + await expect(page.locator('#core-tags-config--docs-only')).toHaveCount(0); + await expect(page.locator('#core-tags-config--test-only')).toHaveCount(0); // Autodocs should include docs-only and exclude dev-only and test-only - const root = sbPage.previewRoot(); - - const devOnlyAnchor = await root.locator('#anchor--core-tags-config--dev-only').all(); - expect(devOnlyAnchor.length).toBe(0); - - const docsOnlyAnchor = await root.locator('#anchor--core-tags-config--docs-only').all(); - expect(docsOnlyAnchor.length).toBe(1); + const preview = sbPage.previewRoot(); - const testOnlyAnchor = await root.locator('#anchor--core-tags-config--test-only').all(); - expect(testOnlyAnchor.length).toBe(0); + await expect(preview.locator('#anchor--core-tags-config--dev-only')).toHaveCount(0); + await expect(preview.locator('#anchor--core-tags-config--docs-only')).toHaveCount(1); + await expect(preview.locator('#anchor--core-tags-config--test-only')).toHaveCount(0); }); test('should correctly add dev, autodocs, test stories', async ({ page }) => { @@ -44,27 +34,17 @@ test.describe('tags', () => { await sbPage.navigateToStory('core/tags-add', 'docs'); // Sidebar should include dev and exclude inheritance, autodocs, test - const devEntry = await page.locator('#core-tags-add--dev').all(); - expect(devEntry.length).toBe(1); - - const autodocsEntry = await page.locator('#core-tags-add--autodocs').all(); - expect(autodocsEntry.length).toBe(0); - - const testOnlyEntry = await page.locator('#core-tags-add--test').all(); - expect(testOnlyEntry.length).toBe(0); + await expect(page.locator('#core-tags-add--dev-only')).toHaveCount(1); + await expect(page.locator('#core-tags-add--docs-only')).toHaveCount(0); + await expect(page.locator('#core-tags-add--test-only')).toHaveCount(0); // Autodocs should include autodocs and exclude dev, test - const root = sbPage.previewRoot(); - - const devAnchor = await root.locator('#anchor--core-tags-add--dev').all(); - expect(devAnchor.length).toBe(0); + const preview = sbPage.previewRoot(); + await expect(preview.locator('#anchor--core-tags-add--dev')).toHaveCount(0); // FIXME: shows as primary story and also in stories, inconsistent btw dev/CI? - const autodocsAnchor = await root.locator('#anchor--core-tags-add--autodocs').all(); - expect(autodocsAnchor.length).not.toBe(0); - - const testAnchor = await root.locator('#anchor--core-tags-add--test').all(); - expect(testAnchor.length).toBe(0); + await expect(preview.locator('#anchor--core-tags-add--autodocs')).not.toHaveCount(0); + await expect(preview.locator('#anchor--core-tags-add--test')).toHaveCount(0); }); test('should correctly remove dev, autodocs, test stories', async ({ page }) => { @@ -73,25 +53,15 @@ test.describe('tags', () => { await sbPage.navigateToStory('core/tags-remove', 'docs'); // Sidebar should include inheritance, no-autodocs, no-test. and exclude no-dev - const noDevEntry = await page.locator('#core-tags-remove--no-dev').all(); - expect(noDevEntry.length).toBe(0); - - const noAutodocsEntry = await page.locator('#core-tags-remove--no-autodocs').all(); - expect(noAutodocsEntry.length).toBe(1); - - const noTestEntry = await page.locator('#core-tags-remove--no-test').all(); - expect(noTestEntry.length).toBe(1); + await expect(page.locator('#core-tags-remove--no-dev')).toHaveCount(0); + await expect(page.locator('#core-tags-remove--no-autodocs')).toHaveCount(1); + await expect(page.locator('#core-tags-remove--no-test')).toHaveCount(1); - // Autodocs should include inheritance, no-dev, no-test. and exclude no-autodocs - const root = sbPage.previewRoot(); - - const noDevAnchor = await root.locator('#anchor--core-tags-remove--no-dev').all(); - expect(noDevAnchor.length).toBe(1); - - const noAutodocsAnchor = await root.locator('#anchor--core-tags-remove--no-autodocs').all(); - expect(noAutodocsAnchor.length).toBe(0); + // Autodocs should include autodocs and exclude dev, test + const preview = sbPage.previewRoot(); - const noTestAnchor = await root.locator('#anchor--core-tags-remove--no-test').all(); - expect(noTestAnchor.length).toBe(1); + await expect(preview.locator('#anchor--core-tags-remove--no-dev')).toHaveCount(1); + await expect(preview.locator('#anchor--core-tags-remove--no-autodocs')).toHaveCount(0); + await expect(preview.locator('#anchor--core-tags-remove--no-test')).toHaveCount(1); }); }); From 01216e1dd7cbf2207823157346a227194501d5ae Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 3 Sep 2024 20:24:15 +0200 Subject: [PATCH 15/26] fix urls in addon-test package.json --- code/addons/test/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/addons/test/package.json b/code/addons/test/package.json index 70d918978626..2b16dc1b0bfe 100644 --- a/code/addons/test/package.json +++ b/code/addons/test/package.json @@ -8,14 +8,14 @@ "vitest", "testing" ], - "homepage": "https://github.com/storybookjs/storybook/tree/next/code/addons/vitest", + "homepage": "https://github.com/storybookjs/storybook/tree/next/code/addons/test", "bugs": { "url": "https://github.com/storybookjs/storybook/issues" }, "repository": { "type": "git", "url": "https://github.com/storybookjs/storybook.git", - "directory": "code/addons/vitest" + "directory": "code/addons/test" }, "funding": { "type": "opencollective", From 53ad1a18653dfdb47960bbda73ab807f601975e4 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 3 Sep 2024 20:44:44 +0200 Subject: [PATCH 16/26] fix tags e2e test --- code/e2e-tests/tags.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/code/e2e-tests/tags.spec.ts b/code/e2e-tests/tags.spec.ts index 10048469fcdd..b6aa96dafca2 100644 --- a/code/e2e-tests/tags.spec.ts +++ b/code/e2e-tests/tags.spec.ts @@ -34,9 +34,9 @@ test.describe('tags', () => { await sbPage.navigateToStory('core/tags-add', 'docs'); // Sidebar should include dev and exclude inheritance, autodocs, test - await expect(page.locator('#core-tags-add--dev-only')).toHaveCount(1); - await expect(page.locator('#core-tags-add--docs-only')).toHaveCount(0); - await expect(page.locator('#core-tags-add--test-only')).toHaveCount(0); + await expect(page.locator('#core-tags-add--dev')).toHaveCount(1); + await expect(page.locator('#core-tags-add--autodocs')).toHaveCount(0); + await expect(page.locator('#core-tags-add--test')).toHaveCount(0); // Autodocs should include autodocs and exclude dev, test const preview = sbPage.previewRoot(); From bb3301cd610950dea8be20223d5d1fdf1f650241 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 3 Sep 2024 21:49:36 +0200 Subject: [PATCH 17/26] add playwright eslint plugin, fix tests by rules --- code/.eslintrc.js | 23 +++++ code/e2e-tests/addon-actions.spec.ts | 8 +- code/e2e-tests/addon-controls.spec.ts | 8 +- code/e2e-tests/addon-docs.spec.ts | 41 ++++---- code/e2e-tests/addon-interactions.spec.ts | 24 ++--- code/e2e-tests/addon-toolbars.spec.ts | 2 +- code/e2e-tests/composition.spec.ts | 30 +++--- code/e2e-tests/framework-nextjs.spec.ts | 42 ++++---- code/e2e-tests/framework-svelte.spec.ts | 116 +++++++++++----------- code/e2e-tests/json-files.spec.ts | 2 +- code/e2e-tests/manager.spec.ts | 58 +++++------ code/e2e-tests/module-mocking.spec.ts | 4 +- code/e2e-tests/preview-api.spec.ts | 10 +- code/e2e-tests/save-from-controls.spec.ts | 20 ++-- code/e2e-tests/storybook-hooks.spec.ts | 2 + code/e2e-tests/util.ts | 18 ++-- code/package.json | 1 + code/yarn.lock | 18 +++- 18 files changed, 231 insertions(+), 196 deletions(-) diff --git a/code/.eslintrc.js b/code/.eslintrc.js index 6b27a23c57c6..e7294f412ede 100644 --- a/code/.eslintrc.js +++ b/code/.eslintrc.js @@ -177,5 +177,28 @@ module.exports = { 'local-rules/no-duplicated-error-codes': 'error', }, }, + { + files: ['./e2e-tests/*.ts'], + extends: ['plugin:playwright/recommended'], + rules: { + 'playwright/no-skipped-test': [ + 'warn', + { + allowConditional: true, + }, + ], + 'playwright/no-raw-locators': 'off', // TODO: enable this, requires the UI to actually be accessible + 'playwright/prefer-comparison-matcher': 'error', + 'playwright/prefer-equality-matcher': 'error', + 'playwright/prefer-hooks-on-top': 'error', + 'playwright/prefer-strict-equal': 'error', + 'playwright/prefer-to-be': 'error', + 'playwright/prefer-to-contain': 'error', + 'playwright/prefer-to-have-count': 'error', + 'playwright/prefer-to-have-length': 'error', + 'playwright/require-to-throw-message': 'error', + 'playwright/require-top-level-describe': 'error', + }, + }, ], }; diff --git a/code/e2e-tests/addon-actions.spec.ts b/code/e2e-tests/addon-actions.spec.ts index 6c3fa98034e6..501ed2c3a6a0 100644 --- a/code/e2e-tests/addon-actions.spec.ts +++ b/code/e2e-tests/addon-actions.spec.ts @@ -18,11 +18,11 @@ test.describe('addon-actions', () => { await sbPage.navigateToStory('example/button', 'primary'); const root = sbPage.previewRoot(); - const button = root.locator('button', { hasText: 'Button' }); + const button = root.getByRole('button', { name: 'Button' }); await button.click(); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content', { + const logItem = page.locator('#storybook-panel-root #panel-tab-content', { hasText: 'click', }); await expect(logItem).toBeVisible(); @@ -40,11 +40,11 @@ test.describe('addon-actions', () => { await sbPage.navigateToStory('addons/actions/spies', 'show-spy-on-in-actions'); const root = sbPage.previewRoot(); - const button = root.locator('button', { hasText: 'Button' }); + const button = root.getByRole('button', { name: 'Button' }); await button.click(); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content', { + const logItem = page.locator('#storybook-panel-root #panel-tab-content', { hasText: 'console.log', }); await expect(logItem).toBeVisible(); diff --git a/code/e2e-tests/addon-controls.spec.ts b/code/e2e-tests/addon-controls.spec.ts index 6499ef6c0742..add1ef866820 100644 --- a/code/e2e-tests/addon-controls.spec.ts +++ b/code/e2e-tests/addon-controls.spec.ts @@ -21,9 +21,7 @@ test.describe('addon-controls', () => { await expect(sbPage.previewRoot().locator('button')).toContainText('Hello world'); // Args in URL - await page.waitForTimeout(300); - const url = await page.url(); - await expect(url).toContain('args=label:Hello+world'); + await page.waitForURL((url) => url.search.includes('args=label:Hello+world')); // Boolean toggle: Primary/secondary await expect(sbPage.previewRoot().locator('button')).toHaveCSS( @@ -72,8 +70,8 @@ test.describe('addon-controls', () => { await expect(sbPage.previewRoot().locator('button')).toContainText('Hello world'); await sbPage.viewAddonPanel('Controls'); - const label = await sbPage.panelContent().locator('textarea[name=label]').inputValue(); - await expect(label).toEqual('Hello world'); + const label = sbPage.panelContent().locator('textarea[name=label]'); + await expect(label).toHaveValue('Hello world'); }); test('should set select option when value contains double spaces', async ({ page }) => { diff --git a/code/e2e-tests/addon-docs.spec.ts b/code/e2e-tests/addon-docs.spec.ts index ce3e281c2f26..c7486bc55923 100644 --- a/code/e2e-tests/addon-docs.spec.ts +++ b/code/e2e-tests/addon-docs.spec.ts @@ -1,3 +1,6 @@ +/* eslint-disable playwright/no-conditional-expect */ + +/* eslint-disable playwright/no-conditional-in-test */ import { expect, test } from '@playwright/test'; import process from 'process'; import { dedent } from 'ts-dedent'; @@ -94,14 +97,14 @@ test.describe('addon-docs', () => { const toggleCount = await toggles.count(); for (let i = 0; i < toggleCount; i += 1) { - const toggle = await toggles.nth(i); - await toggle.click({ force: true }); + const toggle = toggles.nth(i); + await toggle.click(); } const codes = root.locator('pre.prismjs'); const codeCount = await codes.count(); for (let i = 0; i < codeCount; i += 1) { - const code = await codes.nth(i); + const code = codes.nth(i); const text = await code.innerText(); await expect(text).not.toMatch(/^\(args\) => /); } @@ -132,13 +135,13 @@ test.describe('addon-docs', () => { const toggles = root.locator('.docblock-code-toggle'); // Open up the first and second code toggle (i.e the "Basic" story outside and inside the Stories block) - await (await toggles.nth(0)).click({ force: true }); - await (await toggles.nth(1)).click({ force: true }); + await toggles.nth(0).click(); + await toggles.nth(1).click(); // Check they both say "Basic" const codes = root.locator('pre.prismjs'); - const primaryCode = await codes.nth(0); - const storiesCode = await codes.nth(1); + const primaryCode = codes.nth(0); + const storiesCode = codes.nth(1); await expect(primaryCode).toContainText('Basic'); await expect(storiesCode).toContainText('Basic'); @@ -210,12 +213,12 @@ test.describe('addon-docs', () => { } // Arrange - Get the actual versions - const mdxReactVersion = await root.getByTestId('mdx-react'); - const mdxReactDomVersion = await root.getByTestId('mdx-react-dom'); - const mdxReactDomServerVersion = await root.getByTestId('mdx-react-dom-server'); - const componentReactVersion = await root.getByTestId('component-react'); - const componentReactDomVersion = await root.getByTestId('component-react-dom'); - const componentReactDomServerVersion = await root.getByTestId('component-react-dom-server'); + const mdxReactVersion = root.getByTestId('mdx-react'); + const mdxReactDomVersion = root.getByTestId('mdx-react-dom'); + const mdxReactDomServerVersion = root.getByTestId('mdx-react-dom-server'); + const componentReactVersion = root.getByTestId('component-react'); + const componentReactDomVersion = root.getByTestId('component-react-dom'); + const componentReactDomServerVersion = root.getByTestId('component-react-dom-server'); // Assert - The versions are in the expected range await expect(mdxReactVersion).toHaveText(expectedReactVersionRange); @@ -232,9 +235,9 @@ test.describe('addon-docs', () => { await sbPage.navigateToStory('addons/docs/docs2/resolvedreact', 'docs'); // Arrange - Get the actual versions - const autodocsReactVersion = await root.getByTestId('react'); - const autodocsReactDomVersion = await root.getByTestId('react-dom'); - const autodocsReactDomServerVersion = await root.getByTestId('react-dom-server'); + const autodocsReactVersion = root.getByTestId('react'); + const autodocsReactDomVersion = root.getByTestId('react-dom'); + const autodocsReactDomServerVersion = root.getByTestId('react-dom-server'); // Assert - The versions are in the expected range await expect(autodocsReactVersion).toHaveText(expectedReactVersionRange); @@ -247,9 +250,9 @@ test.describe('addon-docs', () => { await sbPage.navigateToStory('addons/docs/docs2/resolvedreact', 'story'); // Arrange - Get the actual versions - const storyReactVersion = await root.getByTestId('react'); - const storyReactDomVersion = await root.getByTestId('react-dom'); - const storyReactDomServerVersion = await root.getByTestId('react-dom-server'); + const storyReactVersion = root.getByTestId('react'); + const storyReactDomVersion = root.getByTestId('react-dom'); + const storyReactDomServerVersion = root.getByTestId('react-dom-server'); // Assert - The versions are in the expected range await expect(storyReactVersion).toHaveText(expectedReactVersionRange); diff --git a/code/e2e-tests/addon-interactions.spec.ts b/code/e2e-tests/addon-interactions.spec.ts index 4ff7a5b6f977..e2ec2e2ae61c 100644 --- a/code/e2e-tests/addon-interactions.spec.ts +++ b/code/e2e-tests/addon-interactions.spec.ts @@ -24,10 +24,10 @@ test.describe('addon-interactions', () => { await sbPage.navigateToStory('example/page', 'logged-in'); await sbPage.viewAddonPanel('Interactions'); - const welcome = await sbPage.previewRoot().locator('.welcome'); + const welcome = sbPage.previewRoot().locator('.welcome'); await expect(welcome).toContainText('Welcome, Jane Doe!', { timeout: 50000 }); - const interactionsTab = await page.locator('#tabbutton-storybook-interactions-panel'); + const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel'); await expect(interactionsTab).toContainText(/(\d)/); await expect(interactionsTab).toBeVisible(); @@ -36,7 +36,7 @@ test.describe('addon-interactions', () => { await expect(panel).toContainText(/userEvent.click/); await expect(panel).toBeVisible(); - const done = await panel.locator('[data-testid=icon-done]').nth(0); + const done = panel.locator('[data-testid=icon-done]').nth(0); await expect(done).toBeVisible(); }); @@ -57,16 +57,16 @@ test.describe('addon-interactions', () => { await sbPage.viewAddonPanel('Interactions'); // Test initial state - Interactions have run, count is correct and values are as expected - const formInput = await sbPage.previewRoot().locator('#interaction-test-form input'); + const formInput = sbPage.previewRoot().locator('#interaction-test-form input'); await expect(formInput).toHaveValue('final value', { timeout: 50000 }); - const interactionsTab = await page.locator('#tabbutton-storybook-interactions-panel'); + const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel'); await expect(interactionsTab.getByText('3')).toBeVisible(); await expect(interactionsTab).toBeVisible(); await expect(interactionsTab).toBeVisible(); const panel = sbPage.panelContent(); - const runStatusBadge = await panel.locator('[aria-label="Status of the test run"]'); + const runStatusBadge = panel.locator('[aria-label="Status of the test run"]'); await expect(runStatusBadge).toContainText(/Pass/); await expect(panel).toContainText(/"initial value"/); await expect(panel).toContainText(/clear/); @@ -74,18 +74,18 @@ test.describe('addon-interactions', () => { await expect(panel).toBeVisible(); // Test interactions debugger - Stepping through works, count is correct and values are as expected - const interactionsRow = await panel.locator('[aria-label="Interaction step"]'); + const interactionsRow = panel.locator('[aria-label="Interaction step"]'); await interactionsRow.first().isVisible(); - await expect(await interactionsRow.count()).toEqual(3); + await expect(interactionsRow).toHaveCount(3); const firstInteraction = interactionsRow.first(); await firstInteraction.click(); await expect(runStatusBadge).toContainText(/Runs/); await expect(formInput).toHaveValue('initial value'); - const goForwardBtn = await panel.locator('[aria-label="Go forward"]'); + const goForwardBtn = panel.locator('[aria-label="Go forward"]'); await goForwardBtn.click(); await expect(formInput).toHaveValue(''); await goForwardBtn.click(); @@ -94,7 +94,7 @@ test.describe('addon-interactions', () => { await expect(runStatusBadge).toContainText(/Pass/); // Test rerun state (from addon panel) - Interactions have rerun, count is correct and values are as expected - const rerunInteractionButton = await panel.locator('[aria-label="Rerun"]'); + const rerunInteractionButton = panel.locator('[aria-label="Rerun"]'); await rerunInteractionButton.click(); await expect(formInput).toHaveValue('final value'); @@ -107,7 +107,7 @@ test.describe('addon-interactions', () => { await expect(interactionsTab.getByText('3')).toBeVisible(); // Test remount state (from toolbar) - Interactions have rerun, count is correct and values are as expected - const remountComponentButton = await page.locator('[title="Remount component"]'); + const remountComponentButton = page.locator('[title="Remount component"]'); await remountComponentButton.click(); await interactionsRow.first().isVisible(); @@ -132,7 +132,7 @@ test.describe('addon-interactions', () => { await sbPage.deepLinkToStory(storybookUrl, 'addons/interactions/unhandled-errors', 'default'); await sbPage.viewAddonPanel('Interactions'); - const button = await sbPage.previewRoot().locator('button'); + const button = sbPage.previewRoot().locator('button'); await expect(button).toContainText('Button', { timeout: 50000 }); const panel = sbPage.panelContent(); diff --git a/code/e2e-tests/addon-toolbars.spec.ts b/code/e2e-tests/addon-toolbars.spec.ts index dea38ae7d720..0bcd779518b4 100644 --- a/code/e2e-tests/addon-toolbars.spec.ts +++ b/code/e2e-tests/addon-toolbars.spec.ts @@ -30,7 +30,7 @@ test.describe('addon-toolbars', () => { // Click on viewport button and select spanish await sbPage.navigateToStory('addons/toolbars/globals', 'override-locale'); await expect(sbPage.previewRoot()).toContainText('μ•ˆλ…•ν•˜μ„Έμš”'); - const button = await sbPage.page.locator('[title="Internationalization locale"]'); + const button = sbPage.page.locator('[title="Internationalization locale"]'); await expect(button).toHaveAttribute('disabled', ''); }); diff --git a/code/e2e-tests/composition.spec.ts b/code/e2e-tests/composition.spec.ts index 48ce183b33c9..71b206010b46 100644 --- a/code/e2e-tests/composition.spec.ts +++ b/code/e2e-tests/composition.spec.ts @@ -3,48 +3,42 @@ import { expect, test } from '@playwright/test'; import { SbPage } from './util'; const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:6006'; - -// This is a slow test, and (presumably) framework independent, so only run it on one sandbox -const skipTest = process.env.STORYBOOK_TEMPLATE_NAME !== 'react-vite/default-ts'; +const templateName = process.env.STORYBOOK_TEMPLATE_NAME || ''; test.describe('composition', () => { + test.skip( + templateName !== 'react-vite/default-ts', + 'Slow, framework independent test, so only run it on in react-vite/default-ts' + ); + test.beforeEach(async ({ page }) => { - if (skipTest) { - return; - } await page.goto(storybookUrl); await new SbPage(page).waitUntilLoaded(); }); test('should correctly filter composed stories', async ({ page }) => { - if (skipTest) { - return; - } - - // Expect that composed Storybooks are visible - // Expect that composed Storybooks are visible - await expect(await page.getByTitle('Storybook 8.0.0')).toBeVisible(); - await expect(await page.getByTitle('Storybook 7.6.18')).toBeVisible(); + await expect(page.getByTitle('Storybook 8.0.0')).toBeVisible(); + await expect(page.getByTitle('Storybook 7.6.18')).toBeVisible(); // Expect composed stories to be available in the sidebar await page.locator('[id="storybook\\@8\\.0\\.0_components-badge"]').click(); await expect( - await page.locator('[id="storybook\\@8\\.0\\.0_components-badge--default"]') + page.locator('[id="storybook\\@8\\.0\\.0_components-badge--default"]') ).toBeVisible(); await page.locator('[id="storybook\\@7\\.6\\.18_components-badge"]').click(); await expect( - await page.locator('[id="storybook\\@7\\.6\\.18_components-badge--default"]') + page.locator('[id="storybook\\@7\\.6\\.18_components-badge--default"]') ).toBeVisible(); // Expect composed stories `to be available in the search await page.getByPlaceholder('Find components').fill('Button'); await expect( - await page.getByRole('option', { name: 'Button Storybook 8.0.0 / @blocks / examples' }) + page.getByRole('option', { name: 'Button Storybook 8.0.0 / @blocks / examples' }) ).toBeVisible(); await expect( - await page.getByRole('option', { name: 'Button Storybook 7.6.18 / @blocks / examples' }) + page.getByRole('option', { name: 'Button Storybook 7.6.18 / @blocks / examples' }) ).toBeVisible(); }); }); diff --git a/code/e2e-tests/framework-nextjs.spec.ts b/code/e2e-tests/framework-nextjs.spec.ts index c88d723ce465..982952f4cb40 100644 --- a/code/e2e-tests/framework-nextjs.spec.ts +++ b/code/e2e-tests/framework-nextjs.spec.ts @@ -27,7 +27,7 @@ test.describe('Next.js', () => { sbPage = new SbPage(page); }); - // TODO: Test is flaky, investigate why + // eslint-disable-next-line playwright/no-skipped-test -- test is flaky, investigate why test.skip('should lazy load images by default', async () => { await sbPage.navigateToStory('stories/frameworks/nextjs/Image', 'lazy'); @@ -36,7 +36,7 @@ test.describe('Next.js', () => { expect(await img.evaluate((image) => image.complete)).toBeFalsy(); }); - // TODO: Test is flaky, investigate why + // eslint-disable-next-line playwright/no-skipped-test -- test is flaky, investigate why test.skip('should eager load images when loading parameter is set to eager', async () => { await sbPage.navigateToStory('stories/frameworks/nextjs/Image', 'eager'); @@ -50,29 +50,29 @@ test.describe('Next.js', () => { let root: Locator; let sbPage: SbPage; + test.beforeEach(async ({ page }) => { + sbPage = new SbPage(page); + + await sbPage.navigateToStory( + 'stories/frameworks/nextjs-nextjs-default-ts/Navigation', + 'default' + ); + root = sbPage.previewRoot(); + }); + function testRoutingBehaviour(buttonText: string, action: string) { test(`should trigger ${action} action`, async ({ page }) => { const button = root.locator('button', { hasText: buttonText }); await button.click(); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content', { + const logItem = page.locator('#storybook-panel-root #panel-tab-content', { hasText: `useRouter().${action}`, }); await expect(logItem).toBeVisible(); }); } - test.beforeEach(async ({ page }) => { - sbPage = new SbPage(page); - - await sbPage.navigateToStory( - 'stories/frameworks/nextjs-nextjs-default-ts/Navigation', - 'default' - ); - root = sbPage.previewRoot(); - }); - testRoutingBehaviour('Go back', 'back'); testRoutingBehaviour('Go forward', 'forward'); testRoutingBehaviour('Prefetch', 'prefetch'); @@ -85,26 +85,26 @@ test.describe('Next.js', () => { let root: Locator; let sbPage: SbPage; + test.beforeEach(async ({ page }) => { + sbPage = new SbPage(page); + + await sbPage.navigateToStory('stories/frameworks/nextjs-nextjs-default-ts/Router', 'default'); + root = sbPage.previewRoot(); + }); + function testRoutingBehaviour(buttonText: string, action: string) { test(`should trigger ${action} action`, async ({ page }) => { const button = root.locator('button', { hasText: buttonText }); await button.click(); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content', { + const logItem = page.locator('#storybook-panel-root #panel-tab-content', { hasText: `useRouter().${action}`, }); await expect(logItem).toBeVisible(); }); } - test.beforeEach(async ({ page }) => { - sbPage = new SbPage(page); - - await sbPage.navigateToStory('stories/frameworks/nextjs-nextjs-default-ts/Router', 'default'); - root = sbPage.previewRoot(); - }); - testRoutingBehaviour('Go back', 'back'); testRoutingBehaviour('Go forward', 'forward'); testRoutingBehaviour('Prefetch', 'prefetch'); diff --git a/code/e2e-tests/framework-svelte.spec.ts b/code/e2e-tests/framework-svelte.spec.ts index a56bb2db5f0c..d5b3354fb5d0 100644 --- a/code/e2e-tests/framework-svelte.spec.ts +++ b/code/e2e-tests/framework-svelte.spec.ts @@ -6,13 +6,13 @@ import { SbPage } from './util'; const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:6006'; const templateName = process.env.STORYBOOK_TEMPLATE_NAME; -test.beforeEach(async ({ page }) => { - await page.goto(storybookUrl); - await new SbPage(page).waitUntilLoaded(); -}); - test.describe('Svelte', () => { - test.skip(!templateName?.includes('svelte'), 'Only run this test on Svelte'); + test.beforeEach(async ({ page }) => { + await page.goto(storybookUrl); + await new SbPage(page).waitUntilLoaded(); + }); + + test.skip(!templateName?.includes('svelte'), 'Only run these tests on Svelte'); test('JS story has auto-generated args table', async ({ page }) => { const sbPage = new SbPage(page); @@ -58,76 +58,76 @@ test.describe('Svelte', () => { await sbPage.navigateToStory('stories/renderers/svelte/decorators-runs-once', 'default'); expect(lines).toHaveLength(1); }); -}); -test.describe('SvelteKit', () => { - test.skip(!templateName?.includes('svelte-kit'), 'Only run this test on SvelteKit'); + test.describe('SvelteKit', () => { + test.skip(!templateName?.includes('svelte-kit'), 'Only run this test on SvelteKit'); - test('Links are logged in Actions panel', async ({ page }) => { - const sbPage = new SbPage(page); + test('Links are logged in Actions panel', async ({ page }) => { + const sbPage = new SbPage(page); - await sbPage.navigateToStory('stories/sveltekit/modules/hrefs', 'default-actions'); - const root = sbPage.previewRoot(); - const link = root.locator('a', { hasText: 'Link to /basic-href' }); - await link.click(); + await sbPage.navigateToStory('stories/sveltekit/modules/hrefs', 'default-actions'); + const root = sbPage.previewRoot(); + const link = root.locator('a', { hasText: 'Link to /basic-href' }); + await link.click(); - await sbPage.viewAddonPanel('Actions'); - const basicLogItem = await page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/basic-href`, - }); + await sbPage.viewAddonPanel('Actions'); + const basicLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/basic-href`, + }); - await expect(basicLogItem).toBeVisible(); - const complexLogItem = await page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/deep/nested`, + await expect(basicLogItem).toBeVisible(); + const complexLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/deep/nested`, + }); + await expect(complexLogItem).toBeVisible(); }); - await expect(complexLogItem).toBeVisible(); - }); - test('goto are logged in Actions panel', async ({ page }) => { - const sbPage = new SbPage(page); + test('goto are logged in Actions panel', async ({ page }) => { + const sbPage = new SbPage(page); - await sbPage.navigateToStory('stories/sveltekit/modules/navigation', 'default-actions'); - const root = sbPage.previewRoot(); - await sbPage.viewAddonPanel('Actions'); + await sbPage.navigateToStory('stories/sveltekit/modules/navigation', 'default-actions'); + const root = sbPage.previewRoot(); + await sbPage.viewAddonPanel('Actions'); - const goto = root.locator('button', { hasText: 'goto' }); - await goto.click(); + const goto = root.locator('button', { hasText: 'goto' }); + await goto.click(); - const gotoLogItem = page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/storybook-goto`, - }); - await expect(gotoLogItem).toBeVisible(); + const gotoLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/storybook-goto`, + }); + await expect(gotoLogItem).toBeVisible(); - const invalidate = root.getByRole('button', { name: 'invalidate', exact: true }); - await invalidate.click(); + const invalidate = root.getByRole('button', { name: 'invalidate', exact: true }); + await invalidate.click(); - const invalidateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/storybook-invalidate`, - }); - await expect(invalidateLogItem).toBeVisible(); + const invalidateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/storybook-invalidate`, + }); + await expect(invalidateLogItem).toBeVisible(); - const invalidateAll = root.getByRole('button', { name: 'invalidateAll' }); - await invalidateAll.click(); + const invalidateAll = root.getByRole('button', { name: 'invalidateAll' }); + await invalidateAll.click(); - const invalidateAllLogItem = page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `"invalidateAll"`, - }); - await expect(invalidateAllLogItem).toBeVisible(); + const invalidateAllLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `"invalidateAll"`, + }); + await expect(invalidateAllLogItem).toBeVisible(); - const replaceState = root.getByRole('button', { name: 'replaceState' }); - await replaceState.click(); + const replaceState = root.getByRole('button', { name: 'replaceState' }); + await replaceState.click(); - const replaceStateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/storybook-replace-state`, - }); - await expect(replaceStateLogItem).toBeVisible(); + const replaceStateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/storybook-replace-state`, + }); + await expect(replaceStateLogItem).toBeVisible(); - const pushState = root.getByRole('button', { name: 'pushState' }); - await pushState.click(); + const pushState = root.getByRole('button', { name: 'pushState' }); + await pushState.click(); - const pushStateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { - hasText: `/storybook-push-state`, + const pushStateLogItem = page.locator('#storybook-panel-root #panel-tab-content', { + hasText: `/storybook-push-state`, + }); + await expect(pushStateLogItem).toBeVisible(); }); - await expect(pushStateLogItem).toBeVisible(); }); }); diff --git a/code/e2e-tests/json-files.spec.ts b/code/e2e-tests/json-files.spec.ts index 920b072c093a..9769fb4f7905 100644 --- a/code/e2e-tests/json-files.spec.ts +++ b/code/e2e-tests/json-files.spec.ts @@ -11,7 +11,7 @@ test.describe('JSON files', () => { test('should have index.json', async ({ page }) => { const json = await page.evaluate(() => fetch('/index.json').then((res) => res.json())); - expect(json).toEqual({ + expect(json).toStrictEqual({ v: expect.any(Number), entries: expect.objectContaining({ 'example-button--primary': expect.objectContaining({ diff --git a/code/e2e-tests/manager.spec.ts b/code/e2e-tests/manager.spec.ts index f863b1cddf2a..e0156786f11c 100644 --- a/code/e2e-tests/manager.spec.ts +++ b/code/e2e-tests/manager.spec.ts @@ -23,14 +23,14 @@ test.describe('Manager UI', () => { // toggle with keyboard shortcut await sbPage.page.locator('html').press('Alt+s'); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); await sbPage.page.locator('html').press('Alt+s'); await expect(sbPage.page.locator('.sidebar-container')).toBeVisible(); // toggle with menu item await sbPage.page.locator('[aria-label="Shortcuts"]').click(); await sbPage.page.locator('#list-item-S').click(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); // toggle with "show sidebar" button await sbPage.page.locator('[aria-label="Show sidebar"]').click(); @@ -40,8 +40,8 @@ test.describe('Manager UI', () => { test('Toolbar toggling', async ({ page }) => { const sbPage = new SbPage(page); const expectToolbarVisibility = async (visible: boolean) => { - expect(async () => { - const toolbar = await sbPage.page.locator(`[data-test-id="sb-preview-toolbar"]`); + await expect(async () => { + const toolbar = sbPage.page.locator(`[data-test-id="sb-preview-toolbar"]`); const marginTop = await toolbar.evaluate( (element) => window.getComputedStyle(element).marginTop ); @@ -73,13 +73,13 @@ test.describe('Manager UI', () => { // navigate to docs to hide panel await sbPage.navigateToStory('example/button', 'docs'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); // toggle with keyboard shortcut await sbPage.page.locator('html').press('Alt+a'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); await sbPage.page.locator('html').press('Alt+a'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); }); test('Toggling', async ({ page }) => { @@ -92,14 +92,14 @@ test.describe('Manager UI', () => { // toggle with keyboard shortcut await sbPage.page.locator('html').press('Alt+a'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); await sbPage.page.locator('html').press('Alt+a'); await expect(sbPage.page.locator('#storybook-panel-root')).toBeVisible(); // toggle with menu item await sbPage.page.locator('[aria-label="Shortcuts"]').click(); await sbPage.page.locator('#list-item-A').click(); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); // toggle with "show addons" button await sbPage.page.locator('[aria-label="Show addons"]').click(); @@ -121,7 +121,7 @@ test.describe('Manager UI', () => { // hide with keyboard shortcut await sbPage.page.locator('html').press('Alt+a'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); // toggling position should also show the panel again await sbPage.page.locator('html').press('Alt+d'); @@ -140,8 +140,8 @@ test.describe('Manager UI', () => { // toggle with keyboard shortcut await sbPage.page.locator('html').press('Alt+f'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); await sbPage.page.locator('html').press('Alt+f'); await expect(sbPage.page.locator('#storybook-panel-root')).toBeVisible(); @@ -150,8 +150,8 @@ test.describe('Manager UI', () => { // toggle with menu item await sbPage.page.locator('[aria-label="Shortcuts"]').click(); await sbPage.page.locator('#list-item-F').click(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); // toggle with "go/exit fullscreen" button await sbPage.page.locator('[aria-label="Exit full screen"]').click(); @@ -159,20 +159,20 @@ test.describe('Manager UI', () => { await expect(sbPage.page.locator('.sidebar-container')).toBeVisible(); await sbPage.page.locator('[aria-label="Go full screen"]').click(); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); // go fullscreen when sidebar is shown but panel is hidden await sbPage.page.locator('[aria-label="Show sidebar"]').click(); await sbPage.page.locator('[aria-label="Go full screen"]').click(); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); // go fullscreen when panel is shown but sidebar is hidden await sbPage.page.locator('[aria-label="Show addons"]').click(); await sbPage.page.locator('[aria-label="Go full screen"]').click(); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); }); test('Settings page', async ({ page }) => { @@ -182,7 +182,7 @@ test.describe('Manager UI', () => { await expect(sbPage.page.url()).toContain('/settings/about'); - await expect(sbPage.page.locator('#storybook-panel-root')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden(); await sbPage.page.locator('[title="Close settings page"]').click(); await expect(sbPage.page.url()).not.toContain('/settings/about'); @@ -202,12 +202,12 @@ test.describe('Manager UI', () => { test('Navigate to story', async ({ page }) => { const sbPage = new SbPage(page); - const closeNavigationButton = await sbPage.page.locator('[title="Close navigation menu"]'); - const mobileNavigationHeading = await sbPage.page.locator('[title="Open navigation menu"]'); + const closeNavigationButton = sbPage.page.locator('[title="Close navigation menu"]'); + const mobileNavigationHeading = sbPage.page.locator('[title="Open navigation menu"]'); // navigation menu is closed - await expect(closeNavigationButton).not.toBeVisible(); - await expect(sbPage.page.locator('#storybook-explorer-menu')).not.toBeVisible(); + await expect(closeNavigationButton).toBeHidden(); + await expect(sbPage.page.locator('#storybook-explorer-menu')).toBeHidden(); // open navigation menu await mobileNavigationHeading.click(); @@ -223,7 +223,7 @@ test.describe('Manager UI', () => { // navigation menu is closed await expect(mobileNavigationHeading).toHaveText('Example/Button/Secondary'); - await expect(sbPage.page.locator('#storybook-explorer-menu')).not.toBeVisible(); + await expect(sbPage.page.locator('#storybook-explorer-menu')).toBeHidden(); // story has changed await expect(sbPage.page.url()).toContain('example-button--secondary'); }); @@ -231,13 +231,13 @@ test.describe('Manager UI', () => { test('Open and close addon panel', async ({ page }) => { const sbPage = new SbPage(page); - const mobileNavigationHeading = await sbPage.page.locator('[title="Open navigation menu"]'); + const mobileNavigationHeading = sbPage.page.locator('[title="Open navigation menu"]'); await mobileNavigationHeading.click(); await sbPage.navigateToStory('Example/Button', 'Secondary'); // panel is closed await expect(mobileNavigationHeading).toHaveText('Example/Button/Secondary'); - await expect(sbPage.page.locator('#tabbutton-addon-controls')).not.toBeVisible(); + await expect(sbPage.page.locator('#tabbutton-addon-controls')).toBeHidden(); // open panel await sbPage.page.locator('[title="Open addon panel"]').click(); @@ -250,7 +250,7 @@ test.describe('Manager UI', () => { // panel is closed await expect(mobileNavigationHeading).toHaveText('Example/Button/Secondary'); - await expect(sbPage.page.locator('#tabbutton-addon-controls')).not.toBeVisible(); + await expect(sbPage.page.locator('#tabbutton-addon-controls')).toBeHidden(); }); }); }); diff --git a/code/e2e-tests/module-mocking.spec.ts b/code/e2e-tests/module-mocking.spec.ts index 7a9048e6cc2b..1c8de1778e36 100644 --- a/code/e2e-tests/module-mocking.spec.ts +++ b/code/e2e-tests/module-mocking.spec.ts @@ -18,7 +18,7 @@ test.describe('module-mocking', () => { await sbPage.navigateToStory('lib/test/before-each', 'before-each-order'); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content'); + const logItem = page.locator('#storybook-panel-root #panel-tab-content'); await expect(logItem).toBeVisible(); const expectedTexts = [ @@ -42,7 +42,7 @@ test.describe('module-mocking', () => { await sbPage.navigateToStory('lib/test/module-mocking', 'basic'); await sbPage.viewAddonPanel('Actions'); - const logItem = await page.locator('#storybook-panel-root #panel-tab-content', { + const logItem = page.locator('#storybook-panel-root #panel-tab-content', { hasText: 'foo: []', }); await expect(logItem).toBeVisible(); diff --git a/code/e2e-tests/preview-api.spec.ts b/code/e2e-tests/preview-api.spec.ts index bb7b3dc2be9a..5130e78e1694 100644 --- a/code/e2e-tests/preview-api.spec.ts +++ b/code/e2e-tests/preview-api.spec.ts @@ -27,16 +27,16 @@ test.describe('preview-api', () => { // wait for the play function to complete await sbPage.viewAddonPanel('Interactions'); - const interactionsTab = await page.locator('#tabbutton-storybook-interactions-panel'); + const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel'); await expect(interactionsTab).toBeVisible(); const panel = sbPage.panelContent(); - const runStatusBadge = await panel.locator('[aria-label="Status of the test run"]'); + const runStatusBadge = panel.locator('[aria-label="Status of the test run"]'); await expect(runStatusBadge).toContainText(/Pass/); // click outside, to remove focus from the input of the story, then press S to toggle sidebar await sbPage.previewRoot().click(); await sbPage.previewRoot().press('Alt+s'); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); }); test('should pass over shortcuts, but not from play functions, docs', async ({ page }) => { @@ -51,7 +51,7 @@ test.describe('preview-api', () => { await expect(sbPage.page.locator('.sidebar-container')).toBeVisible(); await sbPage.previewRoot().getByRole('button').getByText('Submit').first().press('Alt+s'); - await expect(sbPage.page.locator('.sidebar-container')).not.toBeVisible(); + await expect(sbPage.page.locator('.sidebar-container')).toBeHidden(); }); // if rerenders were interleaved the button would have rendered "Error: Interleaved loaders. Changed arg" @@ -61,7 +61,7 @@ test.describe('preview-api', () => { const root = sbPage.previewRoot(); - const labelControl = await sbPage.page.locator('#control-label'); + const labelControl = sbPage.page.locator('#control-label'); await expect(root.getByText('Loaded. Click me')).toBeVisible(); await expect(labelControl).toBeVisible(); diff --git a/code/e2e-tests/save-from-controls.spec.ts b/code/e2e-tests/save-from-controls.spec.ts index eeba0ab5ed4c..cf5ad28ef363 100644 --- a/code/e2e-tests/save-from-controls.spec.ts +++ b/code/e2e-tests/save-from-controls.spec.ts @@ -35,11 +35,11 @@ test.describe('save-from-controls', () => { await sbPage.panelContent().locator('[data-short-label="Unsaved changes"]').isVisible(); // update the story - await sbPage.panelContent().locator('button').getByText('Update story').click({ force: true }); + await sbPage.panelContent().locator('button').getByText('Update story').click(); // Assert the file is saved - const notification1 = await sbPage.page.waitForSelector('[title="Story saved"]'); - await notification1.isVisible(); + const notification1 = sbPage.page.getByTitle('Story saved'); + await expect(notification1).toBeVisible(); // dismiss await notification1.click(); @@ -52,21 +52,19 @@ test.describe('save-from-controls', () => { // Assert the footer is shown await sbPage.panelContent().locator('[data-short-label="Unsaved changes"]').isVisible(); - const buttons = await sbPage + const buttons = sbPage .panelContent() .locator('[aria-label="Create new story with these settings"]'); // clone the story - await buttons.click({ force: true }); + await buttons.click(); - const input = await sbPage.page.waitForSelector('[placeholder="Story export name"]'); - await input.fill('ClonedStory' + id); - const submit = await sbPage.page.waitForSelector('[type="submit"]'); - await submit.click(); + await sbPage.page.getByPlaceholder('Story export name').fill('ClonedStory' + id); + await sbPage.page.getByRole('button', { name: 'Create' }).click(); // Assert the file is saved - const notification2 = await sbPage.page.waitForSelector('[title="Story created"]'); - await notification2.isVisible(); + const notification2 = sbPage.page.getByTitle('Story created'); + await expect(notification2).toBeVisible(); await notification2.click(); // Assert the Button components is rendered in the preview diff --git a/code/e2e-tests/storybook-hooks.spec.ts b/code/e2e-tests/storybook-hooks.spec.ts index 4dbb0ccf6f1e..b9c1ed1be4d2 100644 --- a/code/e2e-tests/storybook-hooks.spec.ts +++ b/code/e2e-tests/storybook-hooks.spec.ts @@ -1,3 +1,5 @@ +/* eslint-disable playwright/expect-expect */ + /* eslint-disable no-underscore-dangle */ import { promises as fs } from 'node:fs'; import { join } from 'node:path'; diff --git a/code/e2e-tests/util.ts b/code/e2e-tests/util.ts index da0ff313431b..00122ef595d2 100644 --- a/code/e2e-tests/util.ts +++ b/code/e2e-tests/util.ts @@ -43,9 +43,9 @@ export class SbPage { const titleId = toId(title); const storyId = toId(name); const storyLinkId = `#${titleId}--${storyId}`; - await this.page.waitForSelector(storyLinkId); + await this.page.locator(storyLinkId).waitFor(); const storyLink = this.page.locator('*', { has: this.page.locator(`> ${storyLinkId}`) }); - await storyLink.click({ force: true }); + await storyLink.click(); await this.page.waitForURL((url) => url.search.includes( @@ -53,8 +53,8 @@ export class SbPage { ) ); - const selected = await storyLink.getAttribute('data-selected'); - await expect(selected).toBe('true'); + const selected = storyLink; + await expect(selected).toHaveAttribute('data-selected', 'true'); await this.previewRoot(); } @@ -65,16 +65,16 @@ export class SbPage { const titleId = toId(title); const storyId = toId(name); const storyLinkId = `#${titleId}-${storyId}--docs`; - await this.page.waitForSelector(storyLinkId); + await this.page.locator(storyLinkId).waitFor(); const storyLink = this.page.locator('*', { has: this.page.locator(`> ${storyLinkId}`) }); - await storyLink.click({ force: true }); + await storyLink.click(); await this.page.waitForURL((url) => url.search.includes(`path=/docs/${titleId}-${storyId}--docs`) ); - const selected = await storyLink.getAttribute('data-selected'); - await expect(selected).toBe('true'); + const selected = storyLink; + await expect(selected).toHaveAttribute('data-selected', 'true'); await this.previewRoot(); } @@ -124,7 +124,7 @@ export class SbPage { } async viewAddonPanel(name: string) { - const tabs = await this.page.locator('[role=tablist] button[role=tab]'); + const tabs = this.page.locator('[role=tablist] button[role=tab]'); const tab = tabs.locator(`text=/^${name}/`); await tab.click(); } diff --git a/code/package.json b/code/package.json index 4090d4ac381c..40355bc18e1b 100644 --- a/code/package.json +++ b/code/package.json @@ -192,6 +192,7 @@ "eslint": "^8.56.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-local-rules": "portal:../scripts/eslint-plugin-local-rules", + "eslint-plugin-playwright": "^1.6.2", "eslint-plugin-storybook": "^0.8.0", "fs-extra": "^11.1.0", "github-release-from-changelog": "^2.1.1", diff --git a/code/yarn.lock b/code/yarn.lock index 9b31926f1155..83b4b2ba2bdc 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6785,6 +6785,7 @@ __metadata: eslint: "npm:^8.56.0" eslint-import-resolver-typescript: "npm:^3.6.1" eslint-plugin-local-rules: "portal:../scripts/eslint-plugin-local-rules" + eslint-plugin-playwright: "npm:^1.6.2" eslint-plugin-storybook: "npm:^0.8.0" fs-extra: "npm:^11.1.0" github-release-from-changelog: "npm:^2.1.1" @@ -14346,6 +14347,21 @@ __metadata: languageName: node linkType: soft +"eslint-plugin-playwright@npm:^1.6.2": + version: 1.6.2 + resolution: "eslint-plugin-playwright@npm:1.6.2" + dependencies: + globals: "npm:^13.23.0" + peerDependencies: + eslint: ">=8.40.0" + eslint-plugin-jest: ">=25" + peerDependenciesMeta: + eslint-plugin-jest: + optional: true + checksum: 10c0/0785b7031507699eac6a45fdcd90705d9759d5943a4033354b735ae856c9d71345ecacb6a7ff0c4cd0e24f523e9d59dee7081dc96c7b5c492fcbed77496a0a19 + languageName: node + linkType: hard + "eslint-plugin-prettier@npm:^5.1.3": version: 5.1.3 resolution: "eslint-plugin-prettier@npm:5.1.3" @@ -16157,7 +16173,7 @@ __metadata: languageName: node linkType: hard -"globals@npm:^13.19.0, globals@npm:^13.6.0": +"globals@npm:^13.19.0, globals@npm:^13.23.0, globals@npm:^13.6.0": version: 13.24.0 resolution: "globals@npm:13.24.0" dependencies: From 7d2e30bfae27109b07298a99129134d8022877a6 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 4 Sep 2024 08:48:37 +0200 Subject: [PATCH 18/26] review improvements --- code/addons/test/src/postinstall.ts | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index a4a16d58885f..c000bd172fcc 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -68,20 +68,20 @@ export default async function postInstall(options: PostinstallOptions) { info.builderPackageName !== '@storybook/builder-vite' ) { reasons.push( - 'The Storybook Test addon can only be used with a Vite-based Storybook framework or Next.js.' + '- The addon can only be used with a Vite-based Storybook framework or Next.js.' ); } if (!annotationsImport) { reasons.push(dedent` - The Storybook Test addon cannot yet be used with ${colors.pink.bold(info.frameworkPackageName)} + - The addon cannot yet be used with ${colors.pink.bold(info.frameworkPackageName)} `); } if (vitestVersion && !satisfies(vitestVersion as string, '>=2.0.0')) { reasons.push(` - The Storybook Test addon requires Vitest 2.0.0 or later. - Please update your ${colors.pink.bold('vitest')} dependency and try again. + - The addon requires Vitest 2.0.0 or later. + Please update your ${colors.pink.bold('vitest')} dependency and try again. `); } @@ -89,15 +89,15 @@ export default async function postInstall(options: PostinstallOptions) { const nextVersion = await packageManager.getInstalledVersion('next'); if (!nextVersion) { reasons.push(dedent` - It seems like you are using ${colors.pink.bold('@storybook/nextjs')} without having ${colors.pink.bold('next')} installed. - Please install "next" or use a different Storybook framework integration and try again. + - You are using ${colors.pink.bold('@storybook/nextjs')} without having ${colors.pink.bold('next')} installed. + Please install "next" or use a different Storybook framework integration and try again. `); } } if (reasons.length > 0) { reasons.unshift( - 'The Storybook Test addon is incompatible with your current set up and cannot be installed:' + `Storybook Test's automated setup failed due to the following package incompatibilities:` ); reasons.push( dedent` @@ -107,7 +107,7 @@ export default async function postInstall(options: PostinstallOptions) { ); reasons.push( dedent` - Please check the documentation for more information about its requirements and installation: + Fear not, however, you can follow the manual installation process instead at: ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} ` ); @@ -140,7 +140,7 @@ export default async function postInstall(options: PostinstallOptions) { dedent` It looks like you're using Next.js. - I'll add ${colors.pink.bold(`@storybook/experimental-nextjs-vite/vite-plugin`)} so you can use it with Vitest. + Adding ${colors.pink.bold(`@storybook/experimental-nextjs-vite/vite-plugin`)} so you can use it with Vitest. More info about the plugin at: ${c.cyan`https://github.com/storybookjs/vite-plugin-storybook-nextjs`} ` @@ -200,15 +200,15 @@ export default async function postInstall(options: PostinstallOptions) { await writeFile( vitestSetupFile, dedent` - import { beforeAll } from 'vitest' - import { setProjectAnnotations } from '${annotationsImport}' - ${previewExists ? `import * as projectAnnotations from './preview'` : ''} + import { beforeAll } from 'vitest'; + import { setProjectAnnotations } from '${annotationsImport}'; + ${previewExists ? `import * as projectAnnotations from './preview';` : ''} // This is an important step to apply the right configuration when testing your stories. // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations - const project = setProjectAnnotations(${previewExists ? 'projectAnnotations' : '[]'}) + const project = setProjectAnnotations(${previewExists ? 'projectAnnotations' : '[]'}); - beforeAll(project.beforeAll) + beforeAll(project.beforeAll); ` ); @@ -309,7 +309,7 @@ export default async function postInstall(options: PostinstallOptions) { await writeFile( newVitestConfigFile, dedent` - import { defineConfig } from "vitest/config"; + import { defineConfig } from 'vitest/config'; import { storybookTest } from '@storybook/experimental-addon-test/vite-plugin';${vitestInfo.frameworkPluginImport} // More info at: https://storybook.js.org/docs/writing-tests/test-runner-with-vitest From b6b6c2489dd6a3ef1d820b58b7b324ac7f2ab6be Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 4 Sep 2024 11:21:26 +0200 Subject: [PATCH 19/26] use named import from dedent --- code/addons/test/src/postinstall.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index c000bd172fcc..3d2c0f1b5b01 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -16,7 +16,7 @@ import { colors, logger } from 'storybook/internal/node-logger'; import { findUp } from 'find-up'; import { satisfies } from 'semver'; import c from 'tinyrainbow'; -import dedent from 'ts-dedent'; +import { dedent } from 'ts-dedent'; import { type PostinstallOptions } from '../../../lib/cli-storybook/src/add'; import { printError, printInfo, printSuccess, step } from './postinstall-logger'; From b2c4baaf49d338444db8a41231735d5d20ff7fbf Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Wed, 4 Sep 2024 13:02:15 +0200 Subject: [PATCH 20/26] Next.js-Vite: Fix vite plugin exports --- code/frameworks/experimental-nextjs-vite/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/frameworks/experimental-nextjs-vite/package.json b/code/frameworks/experimental-nextjs-vite/package.json index 098bbc990196..a53a795a58b2 100644 --- a/code/frameworks/experimental-nextjs-vite/package.json +++ b/code/frameworks/experimental-nextjs-vite/package.json @@ -55,8 +55,8 @@ }, "./vite-plugin": { "types": "./dist/vite-plugin/index.d.ts", - "import": "./dist/vite-plugin/index.js", - "require": "./dist/vite-plugin/index.cjs" + "import": "./dist/vite-plugin/index.mjs", + "require": "./dist/vite-plugin/index.js" }, "./package.json": "./package.json" }, From 30d951f0afa57cb5d017532c099a670e1c4ee665 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 4 Sep 2024 13:39:52 +0200 Subject: [PATCH 21/26] only install the necessary dependencies --- code/addons/test/src/postinstall.ts | 61 ++++++++++++++++++----------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index 3d2c0f1b5b01..3624aefe14fd 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -14,7 +14,7 @@ import { import { colors, logger } from 'storybook/internal/node-logger'; import { findUp } from 'find-up'; -import { satisfies } from 'semver'; +import { satisfies, coerce } from 'semver'; import c from 'tinyrainbow'; import { dedent } from 'ts-dedent'; @@ -41,11 +41,13 @@ export default async function postInstall(options: PostinstallOptions) { }); const info = await getFrameworkInfo(options); - const dependencies = ['vitest', '@vitest/browser', 'playwright']; - const optionalDependencies = ['@vitest/coverage-istanbul', '@vitest/coverage-v8']; - const vitestVersion = await packageManager.getInstalledVersion('vitest'); + const allDeps = await packageManager.getAllDependencies(); + // only install these dependencies if they are not already installed + const dependencies = ['vitest', '@vitest/browser', 'playwright'].filter(p => !allDeps[p]); + const vitestVersionSpecifier = allDeps.vitest || await packageManager.getInstalledVersion('vitest'); + const coercedVitestVersion = vitestVersionSpecifier ? coerce(vitestVersionSpecifier) : null; // if Vitest is installed, we use the same version to keep consistency across Vitest packages - const vitestVersionToInstall = vitestVersion ?? 'latest'; + const vitestVersionToInstall = vitestVersionSpecifier ?? 'latest'; const annotationsImport = [ '@storybook/nextjs', @@ -59,6 +61,7 @@ export default async function postInstall(options: PostinstallOptions) { ) ? info.rendererPackageName : null; + const isRendererSupported = !!annotationsImport; const prerequisiteCheck = async () => { const reasons = []; @@ -72,15 +75,15 @@ export default async function postInstall(options: PostinstallOptions) { ); } - if (!annotationsImport) { + if (!isRendererSupported) { reasons.push(dedent` - The addon cannot yet be used with ${colors.pink.bold(info.frameworkPackageName)} `); } - if (vitestVersion && !satisfies(vitestVersion as string, '>=2.0.0')) { + if (coercedVitestVersion && !satisfies(coercedVitestVersion, '>=2.0.0')) { reasons.push(` - - The addon requires Vitest 2.0.0 or later. + - The addon requires Vitest 2.0.0 or later. You are currently using ${vitestVersionSpecifier}. Please update your ${colors.pink.bold('vitest')} dependency and try again. `); } @@ -105,12 +108,23 @@ export default async function postInstall(options: PostinstallOptions) { in your main Storybook config file and remove the dependency from your package.json file. ` ); - reasons.push( - dedent` - Fear not, however, you can follow the manual installation process instead at: - ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} - ` - ); + + if (!isRendererSupported) { + reasons.push( + dedent` + Please check the documentation for more information about its requirements and installation: + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} + ` + ); + } else { + reasons.push( + dedent` + Fear not, however, you can follow the manual installation process instead at: + ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest#manual`} + ` + ); + } + return reasons.map((r) => r.trim()).join('\n\n'); } @@ -125,13 +139,6 @@ export default async function postInstall(options: PostinstallOptions) { return; } - const allDeps = await packageManager.getAllDependencies(); - optionalDependencies.forEach((dep) => { - if (allDeps[dep]) { - dependencies.push(dep); - } - }); - const vitestInfo = getVitestPluginInfo(info.frameworkPackageName); if (info.frameworkPackageName === '@storybook/nextjs') { @@ -155,13 +162,21 @@ export default async function postInstall(options: PostinstallOptions) { } } + const versionedDependencies = dependencies.map((p) => { + if(p.includes('vitest')) { + return `${p}@${vitestVersionToInstall ?? 'latest'}` + } + + return p; + }) + logger.line(1); logger.plain(`${step} Installing/updating dependencies:`); - logger.plain(colors.gray(' ' + dependencies.join(', '))); + logger.plain(colors.gray(' ' + versionedDependencies.join(', '))); await packageManager.addDependencies( { installAsDevDependencies: true }, - dependencies.map((p) => `${p}@${p.includes('vitest') ? vitestVersionToInstall : 'latest'}`) + versionedDependencies ); logger.line(1); From addcce3fed48e36e7735c35fbcb3e175f68e6cca Mon Sep 17 00:00:00 2001 From: Valentin Palkovic Date: Thu, 5 Sep 2024 09:44:42 +0200 Subject: [PATCH 22/26] Next.js: Update dependencies --- code/frameworks/experimental-nextjs-vite/package.json | 1 + code/frameworks/nextjs/package.json | 2 +- code/yarn.lock | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/code/frameworks/experimental-nextjs-vite/package.json b/code/frameworks/experimental-nextjs-vite/package.json index a53a795a58b2..c0a70eaecffd 100644 --- a/code/frameworks/experimental-nextjs-vite/package.json +++ b/code/frameworks/experimental-nextjs-vite/package.json @@ -107,6 +107,7 @@ "typescript": "^5.3.2" }, "peerDependencies": { + "@storybook/test": "workspace:*", "next": "^14.1.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json index e0f5cbb511e0..155f10344112 100644 --- a/code/frameworks/nextjs/package.json +++ b/code/frameworks/nextjs/package.json @@ -160,7 +160,7 @@ "sass-loader": "^12.4.0", "semver": "^7.3.5", "style-loader": "^3.3.1", - "styled-jsx": "5.1.1", + "styled-jsx": "^5.1.6", "ts-dedent": "^2.0.0", "tsconfig-paths": "^4.0.0", "tsconfig-paths-webpack-plugin": "^4.0.1" diff --git a/code/yarn.lock b/code/yarn.lock index 83b4b2ba2bdc..3407d75d49a6 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6157,6 +6157,7 @@ __metadata: typescript: "npm:^5.3.2" vite-plugin-storybook-nextjs: "npm:^1.0.10" peerDependencies: + "@storybook/test": "workspace:*" next: ^14.1.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta @@ -6336,7 +6337,7 @@ __metadata: semver: "npm:^7.3.5" sharp: "npm:^0.33.3" style-loader: "npm:^3.3.1" - styled-jsx: "npm:5.1.1" + styled-jsx: "npm:^5.1.6" ts-dedent: "npm:^2.0.0" tsconfig-paths: "npm:^4.0.0" tsconfig-paths-webpack-plugin: "npm:^4.0.1" @@ -26458,7 +26459,7 @@ __metadata: languageName: node linkType: hard -"styled-jsx@npm:5.1.6": +"styled-jsx@npm:5.1.6, styled-jsx@npm:^5.1.6": version: 5.1.6 resolution: "styled-jsx@npm:5.1.6" dependencies: From bd0942c8f16196a2e77340c33e328f51aae1fb1c Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 5 Sep 2024 11:27:12 +0200 Subject: [PATCH 23/26] final fixes --- code/addons/test/src/postinstall.ts | 48 ++++++++++++++++------------- code/lib/cli-storybook/src/add.ts | 14 ++++++--- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/code/addons/test/src/postinstall.ts b/code/addons/test/src/postinstall.ts index 3624aefe14fd..c1d8cf54709b 100644 --- a/code/addons/test/src/postinstall.ts +++ b/code/addons/test/src/postinstall.ts @@ -14,7 +14,7 @@ import { import { colors, logger } from 'storybook/internal/node-logger'; import { findUp } from 'find-up'; -import { satisfies, coerce } from 'semver'; +import { coerce, satisfies } from 'semver'; import c from 'tinyrainbow'; import { dedent } from 'ts-dedent'; @@ -43,8 +43,9 @@ export default async function postInstall(options: PostinstallOptions) { const info = await getFrameworkInfo(options); const allDeps = await packageManager.getAllDependencies(); // only install these dependencies if they are not already installed - const dependencies = ['vitest', '@vitest/browser', 'playwright'].filter(p => !allDeps[p]); - const vitestVersionSpecifier = allDeps.vitest || await packageManager.getInstalledVersion('vitest'); + const dependencies = ['vitest', '@vitest/browser', 'playwright'].filter((p) => !allDeps[p]); + const vitestVersionSpecifier = + allDeps.vitest || (await packageManager.getInstalledVersion('vitest')); const coercedVitestVersion = vitestVersionSpecifier ? coerce(vitestVersionSpecifier) : null; // if Vitest is installed, we use the same version to keep consistency across Vitest packages const vitestVersionToInstall = vitestVersionSpecifier ?? 'latest'; @@ -71,19 +72,19 @@ export default async function postInstall(options: PostinstallOptions) { info.builderPackageName !== '@storybook/builder-vite' ) { reasons.push( - '- The addon can only be used with a Vite-based Storybook framework or Next.js.' + 'β€’ The addon can only be used with a Vite-based Storybook framework or Next.js.' ); } if (!isRendererSupported) { reasons.push(dedent` - - The addon cannot yet be used with ${colors.pink.bold(info.frameworkPackageName)} + β€’ The addon cannot yet be used with ${colors.pink.bold(info.frameworkPackageName)} `); } if (coercedVitestVersion && !satisfies(coercedVitestVersion, '>=2.0.0')) { reasons.push(` - - The addon requires Vitest 2.0.0 or later. You are currently using ${vitestVersionSpecifier}. + β€’ The addon requires Vitest 2.0.0 or later. You are currently using ${vitestVersionSpecifier}. Please update your ${colors.pink.bold('vitest')} dependency and try again. `); } @@ -92,7 +93,7 @@ export default async function postInstall(options: PostinstallOptions) { const nextVersion = await packageManager.getInstalledVersion('next'); if (!nextVersion) { reasons.push(dedent` - - You are using ${colors.pink.bold('@storybook/nextjs')} without having ${colors.pink.bold('next')} installed. + β€’ You are using ${colors.pink.bold('@storybook/nextjs')} without having ${colors.pink.bold('next')} installed. Please install "next" or use a different Storybook framework integration and try again. `); } @@ -163,24 +164,23 @@ export default async function postInstall(options: PostinstallOptions) { } const versionedDependencies = dependencies.map((p) => { - if(p.includes('vitest')) { - return `${p}@${vitestVersionToInstall ?? 'latest'}` + if (p.includes('vitest')) { + return `${p}@${vitestVersionToInstall ?? 'latest'}`; } return p; - }) + }); - logger.line(1); - logger.plain(`${step} Installing/updating dependencies:`); - logger.plain(colors.gray(' ' + versionedDependencies.join(', '))); + if (versionedDependencies.length > 0) { + logger.line(1); + logger.plain(`${step} Installing dependencies:`); + logger.plain(colors.gray(' ' + versionedDependencies.join(', '))); - await packageManager.addDependencies( - { installAsDevDependencies: true }, - versionedDependencies - ); + await packageManager.addDependencies({ installAsDevDependencies: true }, versionedDependencies); + } logger.line(1); - logger.plain(`${step} Configuring Playwright with Chromium:`); + logger.plain(`${step} Configuring Playwright with Chromium (this might take some time):`); logger.plain(colors.gray(' npx playwright install chromium --with-deps')); await packageManager.executeCommand({ @@ -221,7 +221,7 @@ export default async function postInstall(options: PostinstallOptions) { // This is an important step to apply the right configuration when testing your stories. // More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations - const project = setProjectAnnotations(${previewExists ? 'projectAnnotations' : '[]'}); + const project = setProjectAnnotations(${previewExists ? '[projectAnnotations]' : '[]'}); beforeAll(project.beforeAll); ` @@ -350,11 +350,17 @@ export default async function postInstall(options: PostinstallOptions) { ); } + const runCommand = rootConfig ? `npx vitest --project=storybook` : `npx vitest`; + printSuccess( 'πŸŽ‰ All done!', dedent` The Storybook Test addon is now configured and you're ready to run your tests! + Here are a couple of tips to get you started: + β€’ You can run tests with ${colors.gray(runCommand)} + β€’ When using the Vitest extension in your editor, all of your stories will be shown as tests! + Check the documentation for more information about its features and options at: ${c.cyan`https://storybook.js.org/docs/writing-tests/test-runner-with-vitest`} ` @@ -389,8 +395,8 @@ const getVitestPluginInfo = (framework: string) => { // spaces for file identation frameworkPluginImport = `\n${frameworkPluginImport}`; - frameworkPluginDocs = frameworkPluginDocs ? `\n ${frameworkPluginDocs}` : ''; - frameworkPluginCall = frameworkPluginCall ? `\n ${frameworkPluginCall},` : ''; + frameworkPluginDocs = frameworkPluginDocs ? `\n ${frameworkPluginDocs}` : ''; + frameworkPluginCall = frameworkPluginCall ? `\n ${frameworkPluginCall},` : ''; return { frameworkPluginImport, frameworkPluginCall, frameworkPluginDocs }; }; diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index a2dbd23650bc..1d5150b42411 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -10,6 +10,7 @@ import { } from 'storybook/internal/common'; import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; +import prompts from 'prompts'; import SemVer from 'semver'; import { dedent } from 'ts-dedent'; @@ -107,10 +108,15 @@ export async function add( } if (checkInstalled(addonName, requireMain(configDir))) { - logger.warn(dedent` - The Storybook addon "${addonName}" is already present in ${mainConfig}; Its configuration will be skipped. Please remove it and rerun this command if you want to reinstall this addon. - `); - return; + const { shouldForceInstall } = await prompts({ + type: 'confirm', + name: 'shouldForceInstall', + message: `The Storybook addon "${addonName}" is already present in ${mainConfig}. Do you wish to install it again?`, + }); + + if (!shouldForceInstall) { + return; + } } const main = await readConfig(mainConfig); From 8170e25ae5f68a038d855fde96706ffebf50fab6 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 5 Sep 2024 12:11:28 +0200 Subject: [PATCH 24/26] only append addon to main.js if not already added --- code/lib/cli-storybook/src/add.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index 1d5150b42411..661c4fb7d064 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -107,6 +107,7 @@ export async function add( return; } + let shouldAddToMain = true; if (checkInstalled(addonName, requireMain(configDir))) { const { shouldForceInstall } = await prompts({ type: 'confirm', @@ -117,6 +118,8 @@ export async function add( if (!shouldForceInstall) { return; } + + shouldAddToMain = false; } const main = await readConfig(mainConfig); @@ -146,18 +149,20 @@ export async function add( logger.log(`Installing ${addonWithVersion}`); await packageManager.addDependencies({ installAsDevDependencies: true }, [addonWithVersion]); - logger.log(`Adding '${addon}' to the "addons" field in ${mainConfig}`); + if(shouldAddToMain) { + logger.log(`Adding '${addon}' to the "addons" field in ${mainConfig}`); - const mainConfigAddons = main.getFieldNode(['addons']); - if (mainConfigAddons && getRequireWrapperName(main) !== null) { - const addonNode = main.valueToNode(addonName); - main.appendNodeToArray(['addons'], addonNode as any); - wrapValueWithRequireWrapper(main, addonNode as any); - } else { - main.appendValueToArray(['addons'], addonName); - } + const mainConfigAddons = main.getFieldNode(['addons']); + if (mainConfigAddons && getRequireWrapperName(main) !== null) { + const addonNode = main.valueToNode(addonName); + main.appendNodeToArray(['addons'], addonNode as any); + wrapValueWithRequireWrapper(main, addonNode as any); + } else { + main.appendValueToArray(['addons'], addonName); + } - await writeConfig(main); + await writeConfig(main); + } if (!skipPostinstall && isCoreAddon(addonName)) { await postinstallAddon(addonName, { packageManager: packageManager.type, configDir }); From f286a1a32c2d50727adcdb28e3f21120b1a11eea Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 5 Sep 2024 12:54:07 +0200 Subject: [PATCH 25/26] fix lint --- code/lib/cli-storybook/src/add.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index 661c4fb7d064..522fb4b6899e 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -149,7 +149,7 @@ export async function add( logger.log(`Installing ${addonWithVersion}`); await packageManager.addDependencies({ installAsDevDependencies: true }, [addonWithVersion]); - if(shouldAddToMain) { + if (shouldAddToMain) { logger.log(`Adding '${addon}' to the "addons" field in ${mainConfig}`); const mainConfigAddons = main.getFieldNode(['addons']); From 615b74ce25fdea94f96953315139137a2d0477f3 Mon Sep 17 00:00:00 2001 From: storybook-bot <32066757+storybook-bot@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:57:50 +0000 Subject: [PATCH 26/26] Write changelog for 8.3.0-beta.3 [skip ci] --- CHANGELOG.prerelease.md | 7 +++++++ code/package.json | 3 ++- docs/versions/next.json | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 7b7e15e5fb6a..46ccccaafa33 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,10 @@ +## 8.3.0-beta.3 + +- Addon Test: Improve messages and post install script handling - [#29036](https://github.com/storybookjs/storybook/pull/29036), thanks @yannbf! +- Next.js-Vite: Fix vite plugin exports - [#29046](https://github.com/storybookjs/storybook/pull/29046), thanks @valentinpalkovic! +- Next.js: Update dependencies - [#29052](https://github.com/storybookjs/storybook/pull/29052), thanks @valentinpalkovic! +- UI: Fix conditional hooks usage in sidebar - [#28979](https://github.com/storybookjs/storybook/pull/28979), thanks @JReinhold! + ## 8.3.0-beta.2 - Addon Vitest: Fix indentation of 'vitePluginNext' in generated Vitest config file - [#29011](https://github.com/storybookjs/storybook/pull/29011), thanks @ghengeveld! diff --git a/code/package.json b/code/package.json index 40355bc18e1b..ee0e082db909 100644 --- a/code/package.json +++ b/code/package.json @@ -295,5 +295,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.3.0-beta.3" } diff --git a/docs/versions/next.json b/docs/versions/next.json index cee5922cb222..fb27aaa8217b 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"8.3.0-beta.2","info":{"plain":"- Addon Vitest: Fix indentation of 'vitePluginNext' in generated Vitest config file - [#29011](https://github.com/storybookjs/storybook/pull/29011), thanks @ghengeveld!\n- Backgrounds/Viewports: Make defaults overridable in `StoryGlobals`-mode - [#29025](https://github.com/storybookjs/storybook/pull/29025), thanks @JReinhold!\n- CLI: Handle Yarn PnP wrapper scenario when adding an addon - [#29027](https://github.com/storybookjs/storybook/pull/29027), thanks @yannbf!\n- Maintenance: Rename addon-vitest to addon-test - [#29014](https://github.com/storybookjs/storybook/pull/29014), thanks @yannbf!\n- Nextjs-Vite: Re-export vite-plugin-storybook-nextjs - [#29012](https://github.com/storybookjs/storybook/pull/29012), thanks @valentinpalkovic!\n- SvelteKit/Vue3: Refactor plugin export paths - [#29016](https://github.com/storybookjs/storybook/pull/29016), thanks @yannbf!"}} +{"version":"8.3.0-beta.3","info":{"plain":"- Addon Test: Improve messages and post install script handling - [#29036](https://github.com/storybookjs/storybook/pull/29036), thanks @yannbf!\n- Next.js-Vite: Fix vite plugin exports - [#29046](https://github.com/storybookjs/storybook/pull/29046), thanks @valentinpalkovic!\n- Next.js: Update dependencies - [#29052](https://github.com/storybookjs/storybook/pull/29052), thanks @valentinpalkovic!\n- UI: Fix conditional hooks usage in sidebar - [#28979](https://github.com/storybookjs/storybook/pull/28979), thanks @JReinhold!"}}