diff --git a/CHANGELOG.md b/CHANGELOG.md index 3115c6b4c070..dd75af133731 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 8.3.4 + +- Addon Test: Support story name as test description - [#29147](https://github.com/storybookjs/storybook/pull/29147), thanks @InfiniteXyy! +- Addon-Interactions: Use ansi-to-html for colored test errors - [#29110](https://github.com/storybookjs/storybook/pull/29110), thanks @kasperpeulen! + ## 8.3.3 - CLI: Show constraints in error when getting depndencies - [#29187](https://github.com/storybookjs/storybook/pull/29187), thanks @andrasczeh! diff --git a/code/addons/interactions/package.json b/code/addons/interactions/package.json index a3daa94041bf..f93ac2093555 100644 --- a/code/addons/interactions/package.json +++ b/code/addons/interactions/package.json @@ -62,6 +62,7 @@ "@devtools-ds/object-inspector": "^1.1.2", "@storybook/icons": "^1.2.5", "@types/node": "^22.0.0", + "ansi-to-html": "^0.7.2", "formik": "^2.2.9", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/code/addons/interactions/src/components/Interaction.tsx b/code/addons/interactions/src/components/Interaction.tsx index c9b557cddc68..610d0c1b032b 100644 --- a/code/addons/interactions/src/components/Interaction.tsx +++ b/code/addons/interactions/src/components/Interaction.tsx @@ -8,7 +8,7 @@ import { type Call, CallStates, type ControlStates } from '@storybook/instrument import { transparentize } from 'polished'; -import { isChaiError, isJestError } from '../utils'; +import { isChaiError, isJestError, useAnsiToHtmlFilter } from '../utils'; import type { Controls } from './InteractionsPanel'; import { MatcherResult } from './MatcherResult'; import { MethodCall } from './MethodCall'; @@ -116,6 +116,7 @@ const RowMessage = styled('div')(({ theme }) => ({ })); export const Exception = ({ exception }: { exception: Call['exception'] }) => { + const filter = useAnsiToHtmlFilter(); if (isJestError(exception)) { return ; } @@ -135,7 +136,7 @@ export const Exception = ({ exception }: { exception: Call['exception'] }) => { const more = paragraphs.length > 1; return ( -
{paragraphs[0]}
+

       {more && 

See the full stack trace in the browser console.

}
); diff --git a/code/addons/interactions/src/components/InteractionsPanel.tsx b/code/addons/interactions/src/components/InteractionsPanel.tsx index c86a1df8ffde..643732f250ca 100644 --- a/code/addons/interactions/src/components/InteractionsPanel.tsx +++ b/code/addons/interactions/src/components/InteractionsPanel.tsx @@ -6,7 +6,7 @@ import { type Call, CallStates, type ControlStates } from '@storybook/instrument import { transparentize } from 'polished'; -import { isTestAssertionError } from '../utils'; +import { isTestAssertionError, useAnsiToHtmlFilter } from '../utils'; import { Empty } from './EmptyState'; import { Interaction } from './Interaction'; import { Subnav } from './Subnav'; @@ -97,6 +97,7 @@ export const InteractionsPanel: React.FC = React.memo( onScrollToEnd, endRef, }) { + const filter = useAnsiToHtmlFilter(); return ( {(interactions.length > 0 || hasException) && ( @@ -131,9 +132,12 @@ export const InteractionsPanel: React.FC = React.memo( Caught exception in play function - - {printSerializedError(caughtException)} - + )} {unhandledErrors && ( diff --git a/code/addons/interactions/src/components/MatcherResult.tsx b/code/addons/interactions/src/components/MatcherResult.tsx index beae8b2146ff..46b5e540ad8d 100644 --- a/code/addons/interactions/src/components/MatcherResult.tsx +++ b/code/addons/interactions/src/components/MatcherResult.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { styled, typography } from 'storybook/internal/theming'; +import { useAnsiToHtmlFilter } from '../utils'; import { Node } from './MethodCall'; const getParams = (line: string, fromIndex = 0): string => { @@ -59,6 +60,7 @@ export const MatcherResult = ({ message: string; style?: React.CSSProperties; }) => { + const filter = useAnsiToHtmlFilter(); const lines = message.split('\n'); return (
{line}, 
]; + return [ + , +
, + ]; })}
); diff --git a/code/addons/interactions/src/utils.ts b/code/addons/interactions/src/utils.ts index 1b08eca12a24..d80d9f4cdbee 100644 --- a/code/addons/interactions/src/utils.ts +++ b/code/addons/interactions/src/utils.ts @@ -1,3 +1,7 @@ +import { type StorybookTheme, useTheme } from 'storybook/internal/theming'; + +import Filter from 'ansi-to-html'; + export function isTestAssertionError(error: unknown) { return isChaiError(error) || isJestError(error); } @@ -21,3 +25,15 @@ export function isJestError(error: unknown) { error.message.startsWith('expect(') ); } + +export function createAnsiToHtmlFilter(theme: StorybookTheme) { + return new Filter({ + fg: theme.color.defaultText, + bg: theme.background.content, + }); +} + +export function useAnsiToHtmlFilter() { + const theme = useTheme(); + return createAnsiToHtmlFilter(theme); +} diff --git a/code/core/src/csf-tools/vitest-plugin/transformer.test.ts b/code/core/src/csf-tools/vitest-plugin/transformer.test.ts index 72a1d05ab403..84d9ac273718 100644 --- a/code/core/src/csf-tools/vitest-plugin/transformer.test.ts +++ b/code/core/src/csf-tools/vitest-plugin/transformer.test.ts @@ -213,6 +213,55 @@ describe('transformer', () => { `); }); + describe("use the story's name as test title", () => { + it('should support CSF v3 via name property', async () => { + const code = ` + export default { component: Button } + export const Primary = { name: "custom name" };`; + const result = await transform({ code }); + + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + component: Button, + title: "automatic/calculated/title" + }; + export default _meta; + export const Primary = { + name: "custom name" + }; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("custom name", _testStory("Primary", Primary, _meta, [])); + } + `); + }); + + it('should support CSF v1/v2 via storyName property', async () => { + const code = ` + export default { component: Button } + export const Story = () => {} + Story.storyName = 'custom name';`; + const result = await transform({ code: code }); + expect(result.code).toMatchInlineSnapshot(` + import { test as _test, expect as _expect } from "vitest"; + import { testStory as _testStory } from "@storybook/experimental-addon-test/internal/test-utils"; + const _meta = { + component: Button, + title: "automatic/calculated/title" + }; + export default _meta; + export const Story = () => {}; + Story.storyName = 'custom name'; + const _isRunningFromThisFile = import.meta.url.includes(globalThis.__vitest_worker__.filepath ?? _expect.getState().testPath); + if (_isRunningFromThisFile) { + _test("custom name", _testStory("Story", Story, _meta, [])); + } + `); + }); + }); + it('should add test statement to const declared exported stories', async () => { const code = ` export default {}; diff --git a/code/core/src/csf-tools/vitest-plugin/transformer.ts b/code/core/src/csf-tools/vitest-plugin/transformer.ts index 51ea1169f6c3..778ea752f1d0 100644 --- a/code/core/src/csf-tools/vitest-plugin/transformer.ts +++ b/code/core/src/csf-tools/vitest-plugin/transformer.ts @@ -202,15 +202,17 @@ export async function vitestTransform({ const getTestStatementForStory = ({ exportName, + testTitle, node, }: { exportName: string; + testTitle: string; node: t.Node; }): t.ExpressionStatement => { // Create the _test expression directly using the exportName identifier const testStoryCall = t.expressionStatement( t.callExpression(vitestTestId, [ - t.stringLiteral(exportName), + t.stringLiteral(testTitle), t.callExpression(testStoryId, [ t.stringLiteral(exportName), t.identifier(exportName), @@ -239,10 +241,9 @@ export async function vitestTransform({ return; } - return getTestStatementForStory({ - exportName, - node, - }); + // use the story's name as the test title for vitest, and fallback to exportName + const testTitle = parsed._stories[exportName].name ?? exportName; + return getTestStatementForStory({ testTitle, exportName, node }); }) .filter((st) => !!st) as t.ExpressionStatement[]; diff --git a/code/package.json b/code/package.json index b57cdde80abd..2d85ebfe44f5 100644 --- a/code/package.json +++ b/code/package.json @@ -295,5 +295,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "8.3.4" } diff --git a/code/renderers/react/src/__test__/portable-stories.test.tsx b/code/renderers/react/src/__test__/portable-stories.test.tsx index 94de89e093a5..85f33c9a714a 100644 --- a/code/renderers/react/src/__test__/portable-stories.test.tsx +++ b/code/renderers/react/src/__test__/portable-stories.test.tsx @@ -8,7 +8,8 @@ import React from 'react'; import { addons } from 'storybook/internal/preview-api'; -import type { Meta } from '@storybook/react'; +import type { ProjectAnnotations } from '@storybook/csf'; +import type { Meta, ReactRenderer } from '@storybook/react'; import * as addonActionsPreview from '@storybook/addon-actions/preview'; @@ -124,7 +125,7 @@ describe('projectAnnotations', () => { const Story = composeStory( ButtonStories.WithActionArgType, ButtonStories.default, - addonActionsPreview + addonActionsPreview as ProjectAnnotations ); expect(Story.args.someActionArg).toHaveProperty('isAction', true); }); diff --git a/code/yarn.lock b/code/yarn.lock index e36544c79acb..eff28eb79381 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5492,6 +5492,7 @@ __metadata: "@storybook/instrumenter": "workspace:*" "@storybook/test": "workspace:*" "@types/node": "npm:^22.0.0" + ansi-to-html: "npm:^0.7.2" formik: "npm:^2.2.9" polished: "npm:^4.2.2" react: "npm:^18.2.0" diff --git a/docs/versions/latest.json b/docs/versions/latest.json index b809ee18030e..9d5e8bbe13fc 100644 --- a/docs/versions/latest.json +++ b/docs/versions/latest.json @@ -1 +1 @@ -{"version":"8.3.3","info":{"plain":"- CLI: Show constraints in error when getting depndencies - [#29187](https://github.com/storybookjs/storybook/pull/29187), thanks @andrasczeh!\n- React-Vite: Downgrade react-docgen-typescript plugin - [#29184](https://github.com/storybookjs/storybook/pull/29184), thanks @shilman!\n- UI: Fix composed storybook TooltipLinkList bug where href isn't passed forward - [#29175](https://github.com/storybookjs/storybook/pull/29175), thanks @JSMike!"}} +{"version":"8.3.4","info":{"plain":"- Addon Test: Support story name as test description - [#29147](https://github.com/storybookjs/storybook/pull/29147), thanks @InfiniteXyy!\n- Addon-Interactions: Use ansi-to-html for colored test errors - [#29110](https://github.com/storybookjs/storybook/pull/29110), thanks @kasperpeulen!"}} diff --git a/docs/writing-tests/vitest-plugin.mdx b/docs/writing-tests/vitest-plugin.mdx index 09940ef230d9..1a0e4b9708b2 100644 --- a/docs/writing-tests/vitest-plugin.mdx +++ b/docs/writing-tests/vitest-plugin.mdx @@ -379,6 +379,16 @@ We recommend running tests in a browser using Playwright, but you can use WebDri We recommend using Chromium, because it is most likely to best match the experience of a majority of your users. However, you can use other browsers by adjusting the [browser name in the Vitest configuration file](https://vitest.dev/config/#browser-name). Note that [Playwright and WebDriverIO support different browsers](https://vitest.dev/guide/browser/#browser-option-types). +### How do I customize a test name? + +By default, the export name of a story is mapped to the test name. To create a more descriptive test description, you can provide a `name` property for the story. This allows you to include spaces, brackets, or other special characters. + +```js +export const Story = { + name: 'custom, descriptive name' +}; +``` + ## API ### Exports