diff --git a/.github/workflows/prepare-patch-release.yml b/.github/workflows/prepare-patch-release.yml index 5cdc1ba44fcf..91be3acf6f92 100644 --- a/.github/workflows/prepare-patch-release.yml +++ b/.github/workflows/prepare-patch-release.yml @@ -33,7 +33,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" - name: Cache dependencies uses: actions/cache@v3 @@ -89,7 +89,7 @@ jobs: yarn release:pick-patches - name: Cancel when no patches to pick - if: steps.pick-patches.outputs.pr-count == '0' && steps.pick-patches.outputs.pr-count != null + if: steps.pick-patches.outputs.no-patch-prs == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # From https://stackoverflow.com/a/75809743 @@ -123,7 +123,7 @@ jobs: run: | yarn release:write-changelog ${{ steps.versions.outputs.next }} --unpicked-patches --verbose - - name: 'Commit changes to branch: version-patch-from-${{ steps.versions.outputs.current }}' + - name: "Commit changes to branch: version-patch-from-${{ steps.versions.outputs.current }}" working-directory: . run: | git config --global user.name 'storybook-bot' @@ -185,4 +185,4 @@ jobs: DISCORD_WEBHOOK: ${{ secrets.DISCORD_MONITORING_URL }} uses: Ilshidur/action-discord@master with: - args: 'The GitHub Action for preparing the release pull request bumping from v${{ steps.versions.outputs.current }} to v${{ steps.versions.outputs.next }} (triggered by ${{ github.triggering_actor }}) failed! See run at: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' + args: "The GitHub Action for preparing the release pull request bumping from v${{ steps.versions.outputs.current }} to v${{ steps.versions.outputs.next }} (triggered by ${{ github.triggering_actor }}) failed! See run at: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index c1a25029e92c..918bef245067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 7.6.13 + +- Next.js: Fix frameworkOptions resolution - [#25907](https://github.com/storybookjs/storybook/pull/25907), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! +- React Native: Fix init fails when package is already installed - [#25908](https://github.com/storybookjs/storybook/pull/25908), thanks [@dannyhw](https://github.com/dannyhw)! +- React Native: Remove watcher from init - [#25895](https://github.com/storybookjs/storybook/pull/25895), thanks [@dannyhw](https://github.com/dannyhw)! +- Webpack: Update StorybookConfig import in core-webpack types.ts - [#25740](https://github.com/storybookjs/storybook/pull/25740), thanks [@valentinpalkovic](https://github.com/valentinpalkovic)! + ## 7.6.12 - CLI: Fix `upgrade` detecting the wrong version of existing Storybooks - [#25752](https://github.com/storybookjs/storybook/pull/25752), thanks [@JReinhold](https://github.com/JReinhold)! diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index b3cf463bf917..620e9307a8a7 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,13 @@ +## 8.0.0-beta.2 + +- Core: Fix boolean `true` args in URL getting ignored - [#25950](https://github.com/storybookjs/storybook/pull/25950), thanks [@JReinhold](https://github.com/JReinhold)! +- Core: Move @types packages to dev deps in core-common - [#25387](https://github.com/storybookjs/storybook/pull/25387), thanks [@kyletsang](https://github.com/kyletsang)! +- Maintenance: Rename testing-utils paths to portable-stories - [#25888](https://github.com/storybookjs/storybook/pull/25888), thanks [@yannbf](https://github.com/yannbf)! +- Portable stories: Pass story context to the play function of a composed story - [#25943](https://github.com/storybookjs/storybook/pull/25943), thanks [@yannbf](https://github.com/yannbf)! +- UI: Fix `display=true` warning in console - [#25951](https://github.com/storybookjs/storybook/pull/25951), thanks [@JReinhold](https://github.com/JReinhold)! +- UI: Update deprecated Icons with the new @storybook/icons in addons - [#25822](https://github.com/storybookjs/storybook/pull/25822), thanks [@cdedreuille](https://github.com/cdedreuille)! +- Vite: Add a `rollup-plugin-webpack-stats` to allow stats from preview builds - [#25923](https://github.com/storybookjs/storybook/pull/25923), thanks [@tmeasday](https://github.com/tmeasday)! + ## 8.0.0-beta.1 - Addon-docs: Fix MDX components not applied in Vite and theme loading twice - [#25925](https://github.com/storybookjs/storybook/pull/25925), thanks [@JReinhold](https://github.com/JReinhold)! diff --git a/MIGRATION.md b/MIGRATION.md index 15b17681953b..62a85f2a14c9 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -7,6 +7,7 @@ - [Removal of `storiesOf`-API](#removal-of-storiesof-api) - [Removed deprecated shim packages](#removed-deprecated-shim-packages) - [Framework-specific Vite plugins have to be explicitly added](#framework-specific-vite-plugins-have-to-be-explicitly-added) + - [TurboSnap Vite plugin is no longer needed](#turbosnap-vite-plugin-is-no-longer-needed) - [Implicit actions can not be used during rendering (for example in the play function)](#implicit-actions-can-not-be-used-during-rendering-for-example-in-the-play-function) - [MDX related changes](#mdx-related-changes) - [MDX is upgraded to v3](#mdx-is-upgraded-to-v3) @@ -460,6 +461,12 @@ export default defineConfig({ }); ``` +### TurboSnap Vite plugin is no longer needed + +At least in build mode, `builder-vite` now supports the `--webpack-stats-json` flag and will output `preview-stats.json`. + +This means https://github.com/IanVS/vite-plugin-turbosnap is no longer necessary, and duplicative, and the plugin will automatically be removed if found. + ### Implicit actions can not be used during rendering (for example in the play function) In Storybook 7, we inferred if the component accepts any action props, @@ -1493,7 +1500,7 @@ Storybook uses `react` in a variety of docs-related packages. In the past, we've To upgrade manually, add any version of `react` and `react-dom` as devDependencies using your package manager of choice, e.g. ``` -npm add react react-dom --dev +npm add react react-dom --save-dev ``` #### start-storybook / build-storybook binaries removed diff --git a/code/addons/a11y/package.json b/code/addons/a11y/package.json index 51666014253b..e0f45868d5d2 100644 --- a/code/addons/a11y/package.json +++ b/code/addons/a11y/package.json @@ -60,7 +60,7 @@ "@storybook/client-logger": "workspace:*", "@storybook/components": "workspace:*", "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.2.3", + "@storybook/icons": "^1.2.5", "@storybook/manager-api": "workspace:*", "@storybook/preview-api": "workspace:*", "@storybook/theming": "workspace:*", diff --git a/code/addons/a11y/src/components/Report/Item.tsx b/code/addons/a11y/src/components/Report/Item.tsx index 2d34ee58d745..1830a10771fc 100644 --- a/code/addons/a11y/src/components/Report/Item.tsx +++ b/code/addons/a11y/src/components/Report/Item.tsx @@ -1,7 +1,6 @@ import React, { Fragment, useState } from 'react'; import { styled } from '@storybook/theming'; -import { Icons } from '@storybook/components'; import type { Result } from 'axe-core'; import { Info } from './Info'; @@ -11,6 +10,7 @@ import { Tags } from './Tags'; import type { RuleType } from '../A11YPanel'; import HighlightToggle from './HighlightToggle'; +import { ChevronSmallDownIcon } from '@storybook/icons'; const Wrapper = styled.div(({ theme }) => ({ display: 'flex', @@ -21,10 +21,7 @@ const Wrapper = styled.div(({ theme }) => ({ }, })); -const Icon = styled(Icons)({ - height: 10, - width: 10, - minWidth: 10, +const Icon = styled(ChevronSmallDownIcon)({ marginRight: 10, transition: 'transform 0.1s ease-in-out', verticalAlign: 'inherit', @@ -75,7 +72,6 @@ export const Item = (props: ItemProps) => { onToggle(!open)} role="button"> ({ - height: 10, - width: 10, - minWidth: 10, +const Icon = styled(ChevronSmallDownIcon)({ color: convert(themes.light).textMutedColor, marginRight: 10, transition: 'transform 0.1s ease-in-out', @@ -68,7 +65,6 @@ export const ListItem: React.FC = ({ item }) => { onToggle(!open)} role="button"> (({ theme, status }) => { - const color = { - [CallStates.DONE]: theme.color.positive, - [CallStates.ERROR]: theme.color.negative, - [CallStates.ACTIVE]: theme.color.secondary, - [CallStates.WAITING]: transparentize(0.5, gray[500]), - }[status]; - return { - width: status === CallStates.WAITING ? 6 : 12, - height: status === CallStates.WAITING ? 6 : 12, - color, - justifySelf: 'center', - }; +const WarningContainer = styled.div({ + width: 14, + height: 14, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }); -export const StatusIcon: React.FC = ({ status, className }) => { - const icon = { - [CallStates.DONE]: 'check', - [CallStates.ERROR]: 'stopalt', - [CallStates.ACTIVE]: 'play', - [CallStates.WAITING]: 'circle', - }[status] as IconsProps['icon']; - return ( - - ); +export const StatusIcon: React.FC = ({ status }) => { + const theme = useTheme(); + + switch (status) { + case CallStates.DONE: { + return ; + } + case CallStates.ERROR: { + return ; + } + case CallStates.ACTIVE: { + return ; + } + case CallStates.WAITING: { + return ( + + + + ); + } + default: { + return null; + } + } }; diff --git a/code/addons/jest/package.json b/code/addons/jest/package.json index c35b6b08d53b..88e5024ebeef 100644 --- a/code/addons/jest/package.json +++ b/code/addons/jest/package.json @@ -62,6 +62,7 @@ "@storybook/client-logger": "workspace:*", "@storybook/components": "workspace:*", "@storybook/core-events": "workspace:*", + "@storybook/icons": "^1.2.5", "@storybook/manager-api": "workspace:*", "@storybook/preview-api": "workspace:*", "@storybook/theming": "workspace:*", diff --git a/code/addons/jest/src/components/Result.tsx b/code/addons/jest/src/components/Result.tsx index d50fa6f34a1a..61bacd9e6af7 100644 --- a/code/addons/jest/src/components/Result.tsx +++ b/code/addons/jest/src/components/Result.tsx @@ -1,8 +1,8 @@ import React, { Fragment, useState } from 'react'; import { styled, themes, convert } from '@storybook/theming'; -import { Icons } from '@storybook/components'; // eslint-disable-next-line import/no-named-as-default import Message from './Message'; +import { ChevronSmallDownIcon } from '@storybook/icons'; const Wrapper = styled.div<{ status: string }>(({ theme, status }) => ({ display: 'flex', @@ -30,10 +30,7 @@ const HeaderBar = styled.div<{ status: string }>(({ theme, status }) => ({ }, })); -const Icon = styled(Icons)(({ theme }) => ({ - height: 10, - width: 10, - minWidth: 10, +const Icon = styled(ChevronSmallDownIcon)(({ theme }) => ({ color: theme.textMutedColor, marginRight: 10, transition: 'transform 0.1s ease-in-out', @@ -66,7 +63,6 @@ export function Result(props: ResultProps) { {status === `failed` ? ( void; } +// We can't remove the Icons component just yet because there's no way for now to import icons +// in the preview directly. Before having a better solution, we are going to keep the Icons component +// for now and remove the deprecated warning. + export const ToolbarMenuButton: FC = ({ active, title, @@ -19,7 +23,7 @@ export const ToolbarMenuButton: FC = ({ }) => { return ( - {icon && } + {icon && } {title ? `\xa0${title}` : null} ); diff --git a/code/addons/viewport/package.json b/code/addons/viewport/package.json index d35deb4fb890..0a11772f6b07 100644 --- a/code/addons/viewport/package.json +++ b/code/addons/viewport/package.json @@ -55,7 +55,7 @@ "@storybook/components": "workspace:*", "@storybook/core-events": "workspace:*", "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.2.3", + "@storybook/icons": "^1.2.5", "@storybook/manager-api": "workspace:*", "@storybook/preview-api": "workspace:*", "@storybook/theming": "workspace:*", diff --git a/code/builders/builder-vite/package.json b/code/builders/builder-vite/package.json index 819b73ee6eb6..f87231f5195d 100644 --- a/code/builders/builder-vite/package.json +++ b/code/builders/builder-vite/package.json @@ -57,7 +57,8 @@ "express": "^4.17.3", "find-cache-dir": "^3.0.0", "fs-extra": "^11.1.0", - "magic-string": "^0.30.0" + "magic-string": "^0.30.0", + "ts-dedent": "^2.0.0" }, "devDependencies": { "@types/express": "^4.17.13", diff --git a/code/builders/builder-vite/src/build.ts b/code/builders/builder-vite/src/build.ts index ccf9f9476a5f..ebf4f030e8b8 100644 --- a/code/builders/builder-vite/src/build.ts +++ b/code/builders/builder-vite/src/build.ts @@ -1,7 +1,17 @@ import type { Options } from '@storybook/types'; -import { commonConfig } from './vite-config'; +import { logger } from '@storybook/node-logger'; +import dedent from 'ts-dedent'; +import { commonConfig } from './vite-config'; import { sanitizeEnvVars } from './envs'; +import type { WebpackStatsPlugin } from './plugins'; +import type { InlineConfig } from 'vite'; +import { hasVitePlugins } from './utils/has-vite-plugins'; +import { withoutVitePlugins } from './utils/without-vite-plugins'; + +function findPlugin(config: InlineConfig, name: string) { + return config.plugins?.find((p) => p && 'name' in p && p.name === name); +} export async function build(options: Options) { const { build: viteBuild, mergeConfig } = await import('vite'); @@ -28,5 +38,21 @@ export async function build(options: Options) { }).build; const finalConfig = await presets.apply('viteFinal', config, options); + + const turbosnapPluginName = 'rollup-plugin-turbosnap'; + const hasTurbosnapPlugin = + finalConfig.plugins && hasVitePlugins(finalConfig.plugins, [turbosnapPluginName]); + if (hasTurbosnapPlugin) { + logger.warn(dedent`Found '${turbosnapPluginName}' which is now included by default in Storybook 8. + Removing from your plugins list. Ensure you pass \`--webpack-stats-json\` to generate stats. + + For more information, see https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#turbosnap-vite-plugin-is-no-longer-needed`); + + finalConfig.plugins = await withoutVitePlugins(finalConfig.plugins, [turbosnapPluginName]); + } + await viteBuild(await sanitizeEnvVars(options, finalConfig)); + + const statsPlugin = findPlugin(finalConfig, 'rollup-plugin-webpack-stats') as WebpackStatsPlugin; + return statsPlugin?.storybookGetStats(); } diff --git a/code/builders/builder-vite/src/plugins/index.ts b/code/builders/builder-vite/src/plugins/index.ts index 68e540908dc6..bc72dc8755d5 100644 --- a/code/builders/builder-vite/src/plugins/index.ts +++ b/code/builders/builder-vite/src/plugins/index.ts @@ -3,3 +3,4 @@ export * from './strip-story-hmr-boundaries'; export * from './code-generator-plugin'; export * from './csf-plugin'; export * from './external-globals-plugin'; +export * from './webpack-stats-plugin'; diff --git a/code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts b/code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts new file mode 100644 index 000000000000..affb130c07e1 --- /dev/null +++ b/code/builders/builder-vite/src/plugins/webpack-stats-plugin.ts @@ -0,0 +1,116 @@ +// This plugin is a direct port of https://github.com/IanVS/vite-plugin-turbosnap + +import type { BuilderStats } from '@storybook/types'; +import path from 'path'; +import type { Plugin } from 'vite'; + +/* + * Reason, Module are copied from chromatic types + * https://github.com/chromaui/chromatic-cli/blob/145a5e295dde21042e96396c7e004f250d842182/bin-src/types.ts#L265-L276 + */ +interface Reason { + moduleName: string; +} +interface Module { + id: string | number; + name: string; + modules?: Array>; + reasons?: Reason[]; +} + +type WebpackStatsPluginOptions = { + workingDir: string; +}; + +/** + * Strips off query params added by rollup/vite to ids, to make paths compatible for comparison with git. + */ +function stripQueryParams(filePath: string): string { + return filePath.split('?')[0]; +} + +/** + * We only care about user code, not node_modules, vite files, or (most) virtual files. + */ +function isUserCode(moduleName: string) { + return Boolean( + moduleName && + !moduleName.startsWith('vite/') && + !moduleName.startsWith('\x00') && + !moduleName.startsWith('\u0000') && + moduleName !== 'react/jsx-runtime' && + !moduleName.match(/node_modules\//) + ); +} + +export type WebpackStatsPlugin = Plugin & { storybookGetStats: () => BuilderStats }; + +export function pluginWebpackStats({ workingDir }: WebpackStatsPluginOptions): WebpackStatsPlugin { + /** + * Convert an absolute path name to a path relative to the vite root, with a starting `./` + */ + function normalize(filename: string) { + // Do not try to resolve virtual files + if (filename.startsWith('/virtual:')) { + return filename; + } + // Otherwise, we need them in the format `./path/to/file.js`. + else { + const relativePath = path.relative(workingDir, stripQueryParams(filename)); + // This seems hacky, got to be a better way to add a `./` to the start of a path. + return `./${relativePath}`; + } + } + + /** + * Helper to create Reason objects out of a list of string paths + */ + function createReasons(importers?: readonly string[]): Reason[] { + return (importers || []).map((i) => ({ moduleName: normalize(i) })); + } + + /** + * Helper function to build a `Module` given a filename and list of files that import it + */ + function createStatsMapModule(filename: string, importers?: readonly string[]): Module { + return { + id: filename, + name: filename, + reasons: createReasons(importers), + }; + } + + const statsMap = new Map(); + + return { + name: 'storybook:rollup-plugin-webpack-stats', + // We want this to run after the vite build plugins (https://vitejs.dev/guide/api-plugin.html#plugin-ordering) + enforce: 'post', + moduleParsed: function (mod) { + if (isUserCode(mod.id)) { + mod.importedIds + .concat(mod.dynamicallyImportedIds) + .filter((name) => isUserCode(name)) + .forEach((depIdUnsafe) => { + const depId = normalize(depIdUnsafe); + if (statsMap.has(depId)) { + const m = statsMap.get(depId); + if (m) { + m.reasons = (m.reasons ?? []) + .concat(createReasons([mod.id])) + .filter((r) => r.moduleName !== depId); + statsMap.set(depId, m); + } + } else { + statsMap.set(depId, createStatsMapModule(depId, [mod.id])); + } + }); + } + }, + + storybookGetStats() { + const stats = { modules: Array.from(statsMap.values()) }; + return { ...stats, toJson: () => stats }; + }, + }; +} diff --git a/code/builders/builder-vite/src/vite-config.ts b/code/builders/builder-vite/src/vite-config.ts index 2383ebd6e5e5..4a1c2fb3ee44 100644 --- a/code/builders/builder-vite/src/vite-config.ts +++ b/code/builders/builder-vite/src/vite-config.ts @@ -20,6 +20,7 @@ import { injectExportOrderPlugin, stripStoryHMRBoundary, externalGlobalsPlugin, + pluginWebpackStats, } from './plugins'; import type { BuilderOptions } from './types'; @@ -112,6 +113,7 @@ export async function pluginConfig(options: Options) { }, }, await externalGlobalsPlugin(externals), + pluginWebpackStats({ workingDir: process.cwd() }), ] as PluginOption[]; // TODO: framework doesn't exist, should move into framework when/if built diff --git a/code/lib/core-common/package.json b/code/lib/core-common/package.json index eaf88cb1cc3a..63844f6fae8f 100644 --- a/code/lib/core-common/package.json +++ b/code/lib/core-common/package.json @@ -48,10 +48,6 @@ "@storybook/csf-tools": "workspace:*", "@storybook/node-logger": "workspace:*", "@storybook/types": "workspace:*", - "@types/find-cache-dir": "^3.2.1", - "@types/node": "^18.0.0", - "@types/node-fetch": "^2.6.4", - "@types/pretty-hrtime": "^1.0.0", "@yarnpkg/fslib": "2.10.3", "@yarnpkg/libzip": "2.3.0", "chalk": "^4.1.0", @@ -78,9 +74,13 @@ "util": "^0.12.4" }, "devDependencies": { + "@types/find-cache-dir": "^3.2.1", "@types/fs-extra": "^11.0.1", "@types/mock-fs": "^4.13.1", + "@types/node": "^18.0.0", + "@types/node-fetch": "^2.6.4", "@types/picomatch": "^2.3.0", + "@types/pretty-hrtime": "^1.0.0", "mock-fs": "^5.2.0", "slash": "^5.0.0", "type-fest": "~2.19", diff --git a/code/lib/preview-api/src/modules/store/args.test.ts b/code/lib/preview-api/src/modules/store/args.test.ts index 0bfc4ab41a20..0d39874c766f 100644 --- a/code/lib/preview-api/src/modules/store/args.test.ts +++ b/code/lib/preview-api/src/modules/store/args.test.ts @@ -67,8 +67,9 @@ describe('mapArgsToTypes', () => { }); it('maps booleans', () => { + expect(mapArgsToTypes({ a: true }, { a: { type: booleanType } })).toStrictEqual({ a: true }); expect(mapArgsToTypes({ a: 'true' }, { a: { type: booleanType } })).toStrictEqual({ a: true }); - expect(mapArgsToTypes({ a: 'false' }, { a: { type: booleanType } })).toStrictEqual({ + expect(mapArgsToTypes({ a: false }, { a: { type: booleanType } })).toStrictEqual({ a: false, }); expect(mapArgsToTypes({ a: 'yes' }, { a: { type: booleanType } })).toStrictEqual({ a: false }); @@ -127,7 +128,7 @@ describe('mapArgsToTypes', () => { { key: { arr: ['1', '2'], - obj: { bool: 'true' }, + obj: { bool: true }, }, }, { @@ -157,7 +158,7 @@ describe('mapArgsToTypes', () => { key: [ { arr: ['1', '2'], - obj: { bool: 'true' }, + obj: { bool: true }, }, ], }, diff --git a/code/lib/preview-api/src/modules/store/args.ts b/code/lib/preview-api/src/modules/store/args.ts index 2634015bce76..7ce46f94a512 100644 --- a/code/lib/preview-api/src/modules/store/args.ts +++ b/code/lib/preview-api/src/modules/store/args.ts @@ -19,7 +19,7 @@ const map = (arg: unknown, argType: InputType): any => { case 'number': return Number(arg); case 'boolean': - return arg === 'true'; + return String(arg) === 'true'; case 'array': if (!type.value || !Array.isArray(arg)) return INCOMPATIBLE; return arg.reduce((acc, item, index) => { diff --git a/code/lib/preview-api/src/modules/store/csf/index.ts b/code/lib/preview-api/src/modules/store/csf/index.ts index 4b3aa8fa8c97..3ce7ca25109e 100644 --- a/code/lib/preview-api/src/modules/store/csf/index.ts +++ b/code/lib/preview-api/src/modules/store/csf/index.ts @@ -7,4 +7,4 @@ export * from './normalizeProjectAnnotations'; export * from './getValuesFromArgTypes'; export * from './composeConfigs'; export * from './stepRunners'; -export * from './testing-utils'; +export * from './portable-stories'; diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts new file mode 100644 index 000000000000..eae92f09aac8 --- /dev/null +++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts @@ -0,0 +1,178 @@ +import { describe, expect, vi, it } from 'vitest'; +import { composeStory, composeStories } from './portable-stories'; + +// Most integration tests for this functionality are located under renderers/react +describe('composeStory', () => { + const meta = { + title: 'Button', + parameters: { + firstAddon: true, + }, + args: { + label: 'Hello World', + primary: true, + }, + }; + + it('should return story with composed args and parameters', () => { + const Story = () => {}; + Story.args = { primary: true }; + Story.parameters = { + parameters: { + secondAddon: true, + }, + }; + + const composedStory = composeStory(Story, meta); + expect(composedStory.args).toEqual({ ...Story.args, ...meta.args }); + expect(composedStory.parameters).toEqual( + expect.objectContaining({ ...Story.parameters, ...meta.parameters }) + ); + }); + + it('should compose with a play function', async () => { + const spy = vi.fn(); + const Story = () => {}; + Story.args = { + primary: true, + }; + Story.play = async (context: any) => { + spy(context); + }; + + const composedStory = composeStory(Story, meta); + await composedStory.play({ canvasElement: null }); + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + args: { + ...Story.args, + ...meta.args, + }, + }) + ); + }); + + it('should throw when executing the play function but the story does not have one', async () => { + const Story = () => {}; + Story.args = { + primary: true, + }; + + const composedStory = composeStory(Story, meta); + expect(composedStory.play({ canvasElement: null })).rejects.toThrow(); + }); + + it('should throw an error if Story is undefined', () => { + expect(() => { + // @ts-expect-error (invalid input) + composeStory(undefined, meta); + }).toThrow(); + }); + + describe('Id of the story', () => { + it('is exposed correctly when composeStories is used', () => { + const module = { + default: { + title: 'Example/Button', + }, + CSF3Primary: () => {}, + }; + const Primary = composeStory(module.CSF3Primary, module.default, {}); + expect(Primary.id).toBe('example-button--csf-3-primary'); + }); + it('is exposed correctly when composeStory is used and exportsName is passed', () => { + const module = { + default: { + title: 'Example/Button', + }, + CSF3Primary: () => {}, + }; + const Primary = composeStory(module.CSF3Primary, module.default, {}, {}, 'overwritten'); + expect(Primary.id).toBe('example-button--overwritten'); + }); + it("is not unique when composeStory is used and exportsName isn't passed", () => { + const Primary = composeStory({ render: () => {} }, {}); + expect(Primary.id).toContain('unknown'); + }); + }); +}); + +describe('composeStories', () => { + const composeStoryFn = vi.fn((v) => v); + const defaultAnnotations = { render: () => '' }; + it('should call composeStoryFn with stories', () => { + const composeStorySpy = vi.fn((v) => v); + const module = { + default: { + title: 'Button', + }, + StoryOne: () => {}, + StoryTwo: () => {}, + }; + const globalConfig = {}; + composeStories(module, globalConfig, composeStorySpy); + expect(composeStorySpy).toHaveBeenCalledWith( + module.StoryOne, + module.default, + globalConfig, + 'StoryOne' + ); + expect(composeStorySpy).toHaveBeenCalledWith( + module.StoryTwo, + module.default, + globalConfig, + 'StoryTwo' + ); + }); + + it('should not call composeStoryFn for non-story exports', () => { + const composeStorySpy = vi.fn((v) => v); + const module = { + default: { + title: 'Button', + excludeStories: /Data/, + }, + mockData: {}, + }; + composeStories(module, defaultAnnotations, composeStoryFn); + expect(composeStorySpy).not.toHaveBeenCalled(); + }); + + describe('non-story exports', () => { + it('should filter non-story exports with excludeStories', () => { + const StoryModuleWithNonStoryExports = { + default: { + title: 'Some/Component', + excludeStories: /.*Data/, + }, + LegitimateStory: () => 'hello world', + mockData: {}, + }; + + const result = composeStories( + StoryModuleWithNonStoryExports, + defaultAnnotations, + composeStoryFn + ); + expect(Object.keys(result)).not.toContain('mockData'); + }); + + it('should filter non-story exports with includeStories', () => { + const StoryModuleWithNonStoryExports = { + default: { + title: 'Some/Component', + includeStories: /.*Story/, + }, + LegitimateStory: () => 'hello world', + mockData: {}, + }; + + const result = composeStories( + StoryModuleWithNonStoryExports, + defaultAnnotations, + composeStoryFn + ); + expect(Object.keys(result)).not.toContain('mockData'); + }); + }); +}); diff --git a/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts similarity index 72% rename from code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts rename to code/lib/preview-api/src/modules/store/csf/portable-stories.ts index b15a3345ed0c..0ebf858e96e8 100644 --- a/code/lib/preview-api/src/modules/store/csf/testing-utils/index.ts +++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts @@ -12,15 +12,16 @@ import type { Parameters, ComposedStoryFn, StrictArgTypes, + ComposedStoryPlayContext, } from '@storybook/types'; import { HooksContext } from '../../../addons'; -import { composeConfigs } from '../composeConfigs'; -import { prepareContext, prepareStory } from '../prepareStory'; -import { normalizeStory } from '../normalizeStory'; -import { normalizeComponentAnnotations } from '../normalizeComponentAnnotations'; -import { getValuesFromArgTypes } from '../getValuesFromArgTypes'; -import { normalizeProjectAnnotations } from '../normalizeProjectAnnotations'; +import { composeConfigs } from './composeConfigs'; +import { prepareContext, prepareStory } from './prepareStory'; +import { normalizeStory } from './normalizeStory'; +import { normalizeComponentAnnotations } from './normalizeComponentAnnotations'; +import { getValuesFromArgTypes } from './getValuesFromArgTypes'; +import { normalizeProjectAnnotations } from './normalizeProjectAnnotations'; let GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS = composeConfigs([]); @@ -74,24 +75,42 @@ export function composeStory = { + hooks: new HooksContext(), + globals: defaultGlobals, + args: { ...story.initialArgs }, + viewMode: 'story', + loaded: {}, + abortSignal: null as unknown as AbortSignal, + canvasElement: null, + ...story, + }; + const composedStory: ComposedStoryFn> = Object.assign( (extraArgs?: Partial) => { - const context: Partial = { - ...story, - hooks: new HooksContext(), - globals: defaultGlobals, - args: { ...story.initialArgs, ...extraArgs }, + const finalContext: StoryContext = { + ...context, + args: { ...context.initialArgs, ...extraArgs }, }; - return story.unboundStoryFn(prepareContext(context as StoryContext)); + return story.unboundStoryFn(prepareContext(finalContext)); }, { storyName, args: story.initialArgs as Partial, - play: story.playFunction as ComposedStoryPlayFn>, parameters: story.parameters as Parameters, argTypes: story.argTypes as StrictArgTypes, id: story.id, + play: (async (extraContext: ComposedStoryPlayContext) => { + if (story.playFunction === undefined) { + throw new Error('The story does not have a play function. Make sure to add one.'); + } + + await story.playFunction({ + ...context, + ...extraContext, + }); + }) as unknown as ComposedStoryPlayFn>, } ); diff --git a/code/lib/preview-api/src/modules/store/csf/testing-utils/index.test.ts b/code/lib/preview-api/src/modules/store/csf/testing-utils/index.test.ts deleted file mode 100644 index f0c811aa5abc..000000000000 --- a/code/lib/preview-api/src/modules/store/csf/testing-utils/index.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { describe, expect, vi, it } from 'vitest'; -import { composeStory, composeStories } from './index'; - -// Most integration tests for this functionality are located under renderers/react -describe('composeStory', () => { - const meta = { - title: 'Button', - parameters: { - firstAddon: true, - }, - args: { - label: 'Hello World', - primary: true, - }, - }; - - it('should return story with composed args and parameters', () => { - const Story = () => {}; - Story.args = { primary: true }; - Story.parameters = { - parameters: { - secondAddon: true, - }, - }; - - const composedStory = composeStory(Story, meta); - expect(composedStory.args).toEqual({ ...Story.args, ...meta.args }); - expect(composedStory.parameters).toEqual( - expect.objectContaining({ ...Story.parameters, ...meta.parameters }) - ); - }); - - it('should throw an error if Story is undefined', () => { - expect(() => { - // @ts-expect-error (invalid input) - composeStory(undefined, meta); - }).toThrow(); - }); -}); - -describe('composeStories', () => { - it('should call composeStoryFn with stories', () => { - const composeConfigFn = vi.fn((v) => v); - const module = { - default: { - title: 'Button', - }, - StoryOne: () => {}, - StoryTwo: () => {}, - }; - const globalConfig = {}; - composeStories(module, globalConfig, composeConfigFn); - expect(composeConfigFn).toHaveBeenCalledWith( - module.StoryOne, - module.default, - globalConfig, - 'StoryOne' - ); - expect(composeConfigFn).toHaveBeenCalledWith( - module.StoryTwo, - module.default, - globalConfig, - 'StoryTwo' - ); - }); - - it('should not call composeStoryFn for non-story exports', () => { - const composeConfigFn = vi.fn((v) => v); - const module = { - default: { - title: 'Button', - excludeStories: /Data/, - }, - mockData: {}, - }; - composeStories(module, {}, composeConfigFn); - expect(composeConfigFn).not.toHaveBeenCalled(); - }); -}); diff --git a/code/package.json b/code/package.json index e80b1ff1d5d4..474e0f22ae74 100644 --- a/code/package.json +++ b/code/package.json @@ -183,7 +183,7 @@ "@typescript-eslint/experimental-utils": "^5.62.0", "@typescript-eslint/parser": "^6.18.1", "@vitejs/plugin-react": "^3.0.1", - "@vitest/coverage-v8": "^1.0.1", + "@vitest/coverage-v8": "^1.2.2", "chromatic": "7.1.0", "concurrently": "^5.3.0", "cross-env": "^7.0.3", @@ -220,7 +220,7 @@ "util": "^0.12.4", "vite": "^4.0.0", "vite-plugin-turbosnap": "^1.0.1", - "vitest": "^1.0.1", + "vitest": "^1.2.2", "wait-on": "^7.0.1" }, "devDependencies": { @@ -292,5 +292,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.0.0-beta.2" } diff --git a/code/renderers/react/src/__test__/__snapshots__/internals.test.tsx.snap b/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap similarity index 100% rename from code/renderers/react/src/__test__/__snapshots__/internals.test.tsx.snap rename to code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap diff --git a/code/renderers/react/src/__test__/internals.test.tsx b/code/renderers/react/src/__test__/internals.test.tsx deleted file mode 100644 index 8bed037619be..000000000000 --- a/code/renderers/react/src/__test__/internals.test.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import React from 'react'; -import { addons } from '@storybook/preview-api'; -import { cleanup, render, screen } from '@testing-library/react'; - -import { composeStories, composeStory } from '..'; - -import * as stories from './Button.stories'; - -const { CSF2StoryWithParamsAndDecorator } = composeStories(stories); - -it('returns composed args including default values from argtypes', () => { - expect(CSF2StoryWithParamsAndDecorator.args).toEqual({ - ...stories.CSF2StoryWithParamsAndDecorator.args, - }); -}); - -it('returns composed parameters from story', () => { - expect(CSF2StoryWithParamsAndDecorator.parameters).toEqual( - expect.objectContaining({ - ...stories.CSF2StoryWithParamsAndDecorator.parameters, - }) - ); -}); - -describe('Id of the story', () => { - it('is exposed correctly when composeStories is used', () => { - expect(CSF2StoryWithParamsAndDecorator.id).toBe( - 'example-button--csf-2-story-with-params-and-decorator' - ); - }); - it('is exposed correctly when composeStory is used and exportsName is passed', () => { - const exportName = Object.entries(stories).filter( - ([_, story]) => story === stories.CSF3Primary - )[0][0]; - const Primary = composeStory(stories.CSF3Primary, stories.default, {}, exportName); - expect(Primary.id).toBe('example-button--csf-3-primary'); - }); - it("is not unique when composeStory is used and exportsName isn't passed", () => { - const Primary = composeStory(stories.CSF3Primary, stories.default); - expect(Primary.id).toContain('unknown'); - }); -}); - -// common in addons that need to communicate between manager and preview -it('should pass with decorators that need addons channel', () => { - const PrimaryWithChannels = composeStory(stories.CSF3Primary, stories.default, { - decorators: [ - (StoryFn: any) => { - addons.getChannel(); - return ; - }, - ], - }); - render(Hello world); - const buttonElement = screen.getByText(/Hello world/i); - expect(buttonElement).not.toBeNull(); - cleanup(); -}); - -describe('Unsupported formats', () => { - it('should throw error if story is undefined', () => { - const UnsupportedStory = () =>
hello world
; - UnsupportedStory.story = { parameters: {} }; - - const UnsupportedStoryModule: any = { - default: {}, - UnsupportedStory: undefined, - }; - - expect(() => { - composeStories(UnsupportedStoryModule); - }).toThrow(); - }); -}); - -describe('non-story exports', () => { - it('should filter non-story exports with excludeStories', () => { - const StoryModuleWithNonStoryExports = { - default: { - title: 'Some/Component', - excludeStories: /.*Data/, - }, - LegitimateStory: () =>
hello world
, - mockData: {}, - }; - - const result = composeStories(StoryModuleWithNonStoryExports); - expect(Object.keys(result)).not.toContain('mockData'); - }); - - it('should filter non-story exports with includeStories', () => { - const StoryModuleWithNonStoryExports = { - default: { - title: 'Some/Component', - includeStories: /.*Story/, - }, - LegitimateStory: () =>
hello world
, - mockData: {}, - }; - - const result = composeStories(StoryModuleWithNonStoryExports); - expect(Object.keys(result)).not.toContain('mockData'); - }); -}); - -// Batch snapshot testing -const testCases = Object.values(composeStories(stories)).map((Story) => [ - // The ! is necessary in Typescript only, as the property is part of a partial type - Story.storyName!, - Story, -]); -it.each(testCases)('Renders %s story', async (_storyName, Story) => { - cleanup(); - const tree = await render(); - expect(tree.baseElement).toMatchSnapshot(); -}); diff --git a/code/renderers/react/src/__test__/composeStories.test.tsx b/code/renderers/react/src/__test__/portable-stories.test.tsx similarity index 83% rename from code/renderers/react/src/__test__/composeStories.test.tsx rename to code/renderers/react/src/__test__/portable-stories.test.tsx index a8cc360e751b..221404d5ee42 100644 --- a/code/renderers/react/src/__test__/composeStories.test.tsx +++ b/code/renderers/react/src/__test__/portable-stories.test.tsx @@ -1,11 +1,11 @@ import { vi, it, expect, afterEach, describe } from 'vitest'; import React from 'react'; import { render, screen, cleanup } from '@testing-library/react'; - +import { addons } from '@storybook/preview-api'; import type { Meta } from '@storybook/react'; import { expectTypeOf } from 'expect-type'; -import { setProjectAnnotations, composeStories, composeStory } from '..'; +import { setProjectAnnotations, composeStories, composeStory } from '..'; import type { Button } from './Button'; import * as stories from './Button.stories'; @@ -106,6 +106,21 @@ describe('CSF3', () => { }); }); +// common in addons that need to communicate between manager and preview +it('should pass with decorators that need addons channel', () => { + const PrimaryWithChannels = composeStory(stories.CSF3Primary, stories.default, { + decorators: [ + (StoryFn: any) => { + addons.getChannel(); + return StoryFn(); + }, + ], + }); + render(Hello world); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).not.toBeNull(); +}); + describe('ComposeStories types', () => { // this file tests Typescript types that's why there are no assertions it('Should support typescript operators', () => { @@ -122,3 +137,11 @@ describe('ComposeStories types', () => { }).toMatchTypeOf(); }); }); + +// Batch snapshot testing +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName, Story]); +it.each(testCases)('Renders %s story', async (_storyName, Story) => { + cleanup(); + const tree = await render(); + expect(tree.baseElement).toMatchSnapshot(); +}); diff --git a/code/renderers/react/src/index.ts b/code/renderers/react/src/index.ts index 07a118109716..bad002de9370 100644 --- a/code/renderers/react/src/index.ts +++ b/code/renderers/react/src/index.ts @@ -4,7 +4,7 @@ import './globals'; export * from './public-types'; -export * from './testing-api'; +export * from './portable-stories'; // optimization: stop HMR propagation in webpack if (typeof module !== 'undefined') module?.hot?.decline(); diff --git a/code/renderers/react/src/testing-api.ts b/code/renderers/react/src/portable-stories.ts similarity index 100% rename from code/renderers/react/src/testing-api.ts rename to code/renderers/react/src/portable-stories.ts diff --git a/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts b/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts index 1b14978a9976..239416df5c35 100644 --- a/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts +++ b/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts @@ -53,7 +53,6 @@ const getCaptionForLocale = (locale: string) => { export const CSF2StoryWithLocale: CSF2Story = (args, { globals }) => ({ components: { Button }, setup() { - console.log({ globals }); const label = getCaptionForLocale(globals.locale); return { args: { ...args, label } }; }, diff --git a/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/internals.test.tsx.snap b/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap similarity index 100% rename from code/renderers/vue3/src/__tests__/composeStories/__snapshots__/internals.test.tsx.snap rename to code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap diff --git a/code/renderers/vue3/src/__tests__/composeStories/internals.test.tsx b/code/renderers/vue3/src/__tests__/composeStories/internals.test.tsx deleted file mode 100644 index 3be07251c9c0..000000000000 --- a/code/renderers/vue3/src/__tests__/composeStories/internals.test.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from 'react'; -import { addons } from '@storybook/preview-api'; -import { render, screen } from '@testing-library/vue'; -import { describe, it, expect } from 'vitest'; - -import { composeStories, composeStory } from '../../testing-api'; - -import * as stories from './Button.stories'; - -const { CSF2StoryWithParamsAndDecorator } = composeStories(stories); - -it('returns composed args including default values from argtypes', () => { - expect({ - ...stories.default.args, - ...CSF2StoryWithParamsAndDecorator.args, - }).toEqual(expect.objectContaining(CSF2StoryWithParamsAndDecorator.args)); -}); - -it('returns composed parameters from story', () => { - expect(CSF2StoryWithParamsAndDecorator.parameters).toEqual( - expect.objectContaining({ - ...stories.CSF2StoryWithParamsAndDecorator.parameters, - }) - ); -}); - -describe('Id of the story', () => { - it('is exposed correctly when composeStories is used', () => { - expect(CSF2StoryWithParamsAndDecorator.id).toBe( - 'example-button--csf-2-story-with-params-and-decorator' - ); - }); - it('is exposed correctly when composeStory is used and exportsName is passed', () => { - const exportName = Object.entries(stories).filter( - ([_, story]) => story === stories.CSF3Primary - )[0][0]; - const Primary = composeStory(stories.CSF3Primary, stories.default, {}, exportName); - expect(Primary.id).toBe('example-button--csf-3-primary'); - }); - it("is not unique when composeStory is used and exportsName isn't passed", () => { - const Primary = composeStory(stories.CSF3Primary, stories.default); - expect(Primary.id).toContain('unknown'); - }); -}); - -// common in addons that need to communicate between manager and preview -it('should pass with decorators that need addons channel', () => { - const PrimaryWithChannels = composeStory(stories.CSF3Primary, stories.default, { - decorators: [ - (StoryFn: any) => { - addons.getChannel(); - return StoryFn(); - }, - ], - }); - render(PrimaryWithChannels({ label: 'Hello world' })); - const buttonElement = screen.getByText(/Hello world/i); - expect(buttonElement).not.toBeNull(); -}); - -describe('Unsupported formats', () => { - it('should throw error if story is undefined', () => { - const UnsupportedStory = () =>
hello world
; - UnsupportedStory.story = { parameters: {} }; - - const UnsupportedStoryModule: any = { - default: {}, - UnsupportedStory: undefined, - }; - - expect(() => { - composeStories(UnsupportedStoryModule); - }).toThrow(); - }); -}); - -describe('non-story exports', () => { - it('should filter non-story exports with excludeStories', () => { - const StoryModuleWithNonStoryExports = { - default: { - title: 'Some/Component', - excludeStories: /.*Data/, - }, - LegitimateStory: () =>
hello world
, - mockData: {}, - }; - - const result = composeStories(StoryModuleWithNonStoryExports); - expect(Object.keys(result)).not.toContain('mockData'); - }); - - it('should filter non-story exports with includeStories', () => { - const StoryModuleWithNonStoryExports = { - default: { - title: 'Some/Component', - includeStories: /.*Story/, - }, - LegitimateStory: () =>
hello world
, - mockData: {}, - }; - - const result = composeStories(StoryModuleWithNonStoryExports); - expect(Object.keys(result)).not.toContain('mockData'); - }); -}); - -// Batch snapshot testing -const testCases = Object.values(composeStories(stories)).map((Story) => [ - // The ! is necessary in Typescript only, as the property is part of a partial type - Story.storyName!, - Story, -]); -it.each(testCases)('Renders %s story', async (_storyName, Story) => { - if (typeof Story === 'string' || _storyName === 'CSF2StoryWithParamsAndDecorator') { - return; - } - - await new Promise((resolve) => setTimeout(resolve, 0)); - - const tree = await render(Story()); - expect(tree.baseElement).toMatchSnapshot(); -}); diff --git a/code/renderers/vue3/src/__tests__/composeStories/composeStories.test.ts b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts similarity index 79% rename from code/renderers/vue3/src/__tests__/composeStories/composeStories.test.ts rename to code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts index cb2372a4c4e9..a56866fd6227 100644 --- a/code/renderers/vue3/src/__tests__/composeStories/composeStories.test.ts +++ b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts @@ -1,11 +1,13 @@ /// ; import { it, expect, vi, describe } from 'vitest'; import { render, screen } from '@testing-library/vue'; +import { addons } from '@storybook/preview-api'; import { expectTypeOf } from 'expect-type'; import type { Meta } from '@storybook/vue3'; + import * as stories from './Button.stories'; import type Button from './Button.vue'; -import { composeStories, composeStory, setProjectAnnotations } from '../../testing-api'; +import { composeStories, composeStory, setProjectAnnotations } from '../../portable-stories'; // example with composeStories, returns an object with all stories composed with args/decorators const { CSF3Primary } = composeStories(stories); @@ -91,6 +93,21 @@ describe('CSF3', () => { }); }); +// common in addons that need to communicate between manager and preview +it('should pass with decorators that need addons channel', () => { + const PrimaryWithChannels = composeStory(stories.CSF3Primary, stories.default, { + decorators: [ + (StoryFn: any) => { + addons.getChannel(); + return StoryFn(); + }, + ], + }); + render(PrimaryWithChannels({ label: 'Hello world' })); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).not.toBeNull(); +}); + describe('ComposeStories types', () => { it('Should support typescript operators', () => { type ComposeStoriesParam = Parameters[0]; @@ -106,3 +123,16 @@ describe('ComposeStories types', () => { }).toMatchTypeOf(); }); }); + +// Batch snapshot testing +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName, Story]); +it.each(testCases)('Renders %s story', async (_storyName, Story) => { + if (typeof Story === 'string' || _storyName === 'CSF2StoryWithParamsAndDecorator') { + return; + } + + await new Promise((resolve) => setTimeout(resolve, 0)); + + const tree = await render(Story()); + expect(tree.baseElement).toMatchSnapshot(); +}); diff --git a/code/renderers/vue3/src/index.ts b/code/renderers/vue3/src/index.ts index d3c7431a00e1..87e6074fb708 100644 --- a/code/renderers/vue3/src/index.ts +++ b/code/renderers/vue3/src/index.ts @@ -4,7 +4,7 @@ import './globals'; export { setup } from './render'; export * from './public-types'; -export * from './testing-api'; +export * from './portable-stories'; // optimization: stop HMR propagation in webpack try { diff --git a/code/renderers/vue3/src/testing-api.ts b/code/renderers/vue3/src/portable-stories.ts similarity index 100% rename from code/renderers/vue3/src/testing-api.ts rename to code/renderers/vue3/src/portable-stories.ts diff --git a/code/ui/.storybook/preview.tsx b/code/ui/.storybook/preview.tsx index 8d2bfc9aa908..1054d62a5d59 100644 --- a/code/ui/.storybook/preview.tsx +++ b/code/ui/.storybook/preview.tsx @@ -11,7 +11,6 @@ import { useTheme, } from '@storybook/theming'; import { useArgs, DocsContext as DocsContextProps } from '@storybook/preview-api'; -import { Symbols } from '@storybook/components'; import type { PreviewWeb } from '@storybook/preview-api'; import type { ReactRenderer } from '@storybook/react'; import type { Channel } from '@storybook/channels'; diff --git a/code/ui/blocks/package.json b/code/ui/blocks/package.json index bb26603fce09..3b65bd392a3a 100644 --- a/code/ui/blocks/package.json +++ b/code/ui/blocks/package.json @@ -51,7 +51,7 @@ "@storybook/csf": "^0.1.2", "@storybook/docs-tools": "workspace:*", "@storybook/global": "^5.0.0", - "@storybook/icons": "^1.2.3", + "@storybook/icons": "^1.2.5", "@storybook/manager-api": "workspace:*", "@storybook/preview-api": "workspace:*", "@storybook/theming": "workspace:*", diff --git a/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx b/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx index 131aab285a1b..7d549c5f45a7 100644 --- a/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx +++ b/code/ui/blocks/src/components/ArgsTable/ArgValue.tsx @@ -3,8 +3,9 @@ import React, { useState } from 'react'; import memoize from 'memoizerific'; import uniq from 'lodash/uniq.js'; import { styled } from '@storybook/theming'; -import { WithTooltipPure, Icons, SyntaxHighlighter, codeCommon } from '@storybook/components'; +import { WithTooltipPure, SyntaxHighlighter, codeCommon } from '@storybook/components'; import type { PropSummaryValue } from './types'; +import { ChevronSmallDownIcon, ChevronSmallUpIcon } from '@storybook/icons'; interface ArgValueProps { value?: PropSummaryValue; @@ -86,10 +87,11 @@ const Detail = styled.div<{ width: string }>(({ theme, width }) => ({ }, })); -const ArrowIcon = styled(Icons)({ - height: 10, - width: 10, - minWidth: 10, +const ChevronUpIcon = styled(ChevronSmallUpIcon)({ + marginLeft: 4, +}); + +const ChevronDownIcon = styled(ChevronSmallDownIcon)({ marginLeft: 4, }); @@ -176,7 +178,7 @@ const ArgSummary: FC = ({ value, initialExpandedArgs }) => { > {summaryAsString} - + {isOpen ? : } ); diff --git a/code/ui/blocks/src/components/ArgsTable/SectionRow.tsx b/code/ui/blocks/src/components/ArgsTable/SectionRow.tsx index b23f225584db..450075b56aa7 100644 --- a/code/ui/blocks/src/components/ArgsTable/SectionRow.tsx +++ b/code/ui/blocks/src/components/ArgsTable/SectionRow.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react'; import React, { useState } from 'react'; import { transparentize, lighten } from 'polished'; import { styled } from '@storybook/theming'; -import { Icons } from '@storybook/components'; +import { ChevronDownIcon, ChevronRightIcon } from '@storybook/icons'; type Level = 'section' | 'subsection'; @@ -14,7 +14,21 @@ export interface SectionRowProps { colSpan: number; } -const ExpanderIcon = styled(Icons)(({ theme }) => ({ +const ExpanderIconDown = styled(ChevronDownIcon)(({ theme }) => ({ + marginRight: 8, + marginLeft: -10, + marginTop: -2, // optical alignment + height: 12, + width: 12, + color: + theme.base === 'light' + ? transparentize(0.25, theme.color.defaultText) + : transparentize(0.3, theme.color.defaultText), + border: 'none', + display: 'inline-block', +})); + +const ExpanderIconRight = styled(ChevronRightIcon)(({ theme }) => ({ marginRight: 8, marginLeft: -10, marginTop: -2, // optical alignment @@ -100,7 +114,6 @@ export const SectionRow: FC = ({ // @ts-expect-error (Converted from ts-ignore) const itemCount = children?.length || 0; const caption = level === 'subsection' ? `${itemCount} item${itemCount !== 1 ? 's' : ''}` : ''; - const icon = expanded ? 'arrowdown' : 'arrowright'; const helperText = `${expanded ? 'Hide' : 'Show'} ${ level === 'subsection' ? itemCount : label @@ -114,7 +127,7 @@ export const SectionRow: FC = ({ {helperText} - + {expanded ? : } {label} diff --git a/code/ui/blocks/src/components/IconGallery.stories.tsx b/code/ui/blocks/src/components/IconGallery.stories.tsx index b14417402fdd..0dc183e79311 100644 --- a/code/ui/blocks/src/components/IconGallery.stories.tsx +++ b/code/ui/blocks/src/components/IconGallery.stories.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Icons as ExampleIcon } from '@storybook/components'; import { IconItem, IconGallery } from './IconGallery'; +import { AddIcon, FaceHappyIcon, HomeIcon, SubtractIcon } from '@storybook/icons'; export default { component: IconGallery, @@ -9,16 +9,16 @@ export default { export const DefaultStyle = () => ( - + - + - + - + example diff --git a/code/ui/blocks/src/controls/Color.tsx b/code/ui/blocks/src/controls/Color.tsx index c9bf13a526c9..ff2eae90a49b 100644 --- a/code/ui/blocks/src/controls/Color.tsx +++ b/code/ui/blocks/src/controls/Color.tsx @@ -4,10 +4,11 @@ import { HexColorPicker, HslaStringColorPicker, RgbaStringColorPicker } from 're import convert from 'color-convert'; import throttle from 'lodash/throttle.js'; import { styled } from '@storybook/theming'; -import { TooltipNote, WithTooltip, Form, Icons } from '@storybook/components'; +import { TooltipNote, WithTooltip, Form } from '@storybook/components'; import type { ControlProps, ColorValue, ColorConfig, PresetColor } from './types'; import { getControlId } from './helpers'; +import { MarkupIcon } from '@storybook/icons'; const Wrapper = styled.div({ position: 'relative', @@ -74,7 +75,7 @@ const Input = styled(Form.Input)(({ theme }) => ({ fontFamily: theme.typography.fonts.base, })); -const ToggleIcon = styled(Icons)(({ theme }) => ({ +const ToggleIcon = styled(MarkupIcon)(({ theme }) => ({ position: 'absolute', zIndex: 1, top: 6, @@ -358,7 +359,7 @@ export const ColorControl: FC = ({ onFocus={(e: FocusEvent) => e.target.select()} placeholder="Choose color..." /> - {value ? : null} + {value ? : null}
); }; diff --git a/code/ui/blocks/src/controls/Object.tsx b/code/ui/blocks/src/controls/Object.tsx index c6b94fc23951..d6c4d2328af0 100644 --- a/code/ui/blocks/src/controls/Object.tsx +++ b/code/ui/blocks/src/controls/Object.tsx @@ -3,8 +3,8 @@ import cloneDeep from 'lodash/cloneDeep.js'; import type { ComponentProps, SyntheticEvent, FC, FocusEvent } from 'react'; import React, { useCallback, useMemo, useState, useEffect, useRef } from 'react'; import { styled, useTheme, type Theme } from '@storybook/theming'; -import { Form, Icons, IconButton, Button } from '@storybook/components'; -import { EyeCloseIcon, EyeIcon } from '@storybook/icons'; +import { Form, IconButton, Button } from '@storybook/components'; +import { AddIcon, EyeCloseIcon, EyeIcon, SubtractIcon } from '@storybook/icons'; import { JsonTree, getObjectType } from './react-editable-json-tree'; import { getControlId, getControlSetterButtonId } from './helpers'; import type { ControlProps, ObjectValue, ObjectConfig } from './types'; @@ -133,7 +133,7 @@ const ButtonInline = styled.button<{ primary?: boolean }>(({ theme, primary }) = order: primary ? 'initial' : 9, })); -const ActionIcon = styled(Icons)<{ disabled?: boolean }>(({ theme, icon, disabled }) => ({ +const ActionAddIcon = styled(AddIcon)<{ disabled?: boolean }>(({ theme, disabled }) => ({ display: 'inline-block', verticalAlign: 'middle', width: 15, @@ -142,11 +142,22 @@ const ActionIcon = styled(Icons)<{ disabled?: boolean }>(({ theme, icon, disable marginLeft: 5, cursor: disabled ? 'not-allowed' : 'pointer', color: theme.textMutedColor, - '&:hover': disabled - ? {} - : { - color: icon === 'subtract' ? theme.color.negative : theme.color.ancillary, - }, + '&:hover': disabled ? {} : { color: theme.color.ancillary }, + 'svg + &': { + marginLeft: 0, + }, +})); + +const ActionSubstractIcon = styled(SubtractIcon)<{ disabled?: boolean }>(({ theme, disabled }) => ({ + display: 'inline-block', + verticalAlign: 'middle', + width: 15, + height: 15, + padding: 3, + marginLeft: 5, + cursor: disabled ? 'not-allowed' : 'pointer', + color: theme.textMutedColor, + '&:hover': disabled ? {} : { color: theme.color.negative }, 'svg + &': { marginLeft: 0, }, @@ -309,8 +320,8 @@ export const ObjectControl: FC = ({ name, value, onChange }) => { Save } - plusMenuElement={} - minusMenuElement={} + plusMenuElement={} + minusMenuElement={} inputElement={(_: any, __: any, ___: any, key: string) => key ? : } diff --git a/code/ui/blocks/src/controls/options/Select.tsx b/code/ui/blocks/src/controls/options/Select.tsx index e2f9835a7c47..574c604b180b 100644 --- a/code/ui/blocks/src/controls/options/Select.tsx +++ b/code/ui/blocks/src/controls/options/Select.tsx @@ -3,12 +3,12 @@ import React from 'react'; import { styled } from '@storybook/theming'; import type { CSSObject } from '@storybook/theming'; import { logger } from '@storybook/client-logger'; -import { Icons } from '@storybook/components'; import type { ControlProps, OptionsSelection, NormalizedOptionsConfig } from '../types'; import { selectedKey, selectedKeys, selectedValues } from './helpers'; import { getControlId } from '../helpers'; +import { ChevronSmallDownIcon } from '@storybook/icons'; const styleResets: CSSObject = { // resets @@ -102,7 +102,7 @@ const SingleSelect: FC = ({ name, value, options, onChange }) => { return ( - +