diff --git a/.gitignore b/.gitignore index 9bf3c7eb5332..19177a9e9365 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,6 @@ tsconfig.vitest-temp.json # test-storybooks test-storybooks/ember-cli/ember-output -test-storybooks/angular-cli/addon-jest.testresults.json npm-shrinkwrap.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c452195b433..f2c023da7dc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.1.13 + +- CSF: Fix `play-fn` tag for methods - [#32695](https://github.com/storybookjs/storybook/pull/32695), thanks @shilman! + ## 9.1.12 - Maintenance: Hotfix for missing nextjs dts files, thanks @ndelangen! diff --git a/README.md b/README.md index 1bf638d98ba1..9eebc6f6b6f0 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ For additional help, share your issue in [the repo's GitHub Discussions](https:/ | [events](https://github.com/storybookjs/addon-events) | Interactively fire events to components that respond to EventEmitter | | [google-analytics](https://github.com/storybookjs/addon-google-analytics) | Reports google analytics on stories | | [graphql](https://github.com/storybookjs/addon-graphql) | Query a GraphQL server within Storybook stories | -| [jest](code/addons/jest/) | View the results of components' unit tests in Storybook | +| [jest](https://github.com/storybookjs/addon-jest) | View the results of components' unit tests in Storybook | | [links](code/addons/links/) | Create links between stories | | [measure](code/core/src/measure/) | Visually inspect the layout and box model within the Storybook UI | | [outline](code/core/src/outline/) | Visually debug the CSS layout and alignment within the Storybook UI | diff --git a/code/addons/jest/README.md b/code/addons/jest/README.md deleted file mode 100644 index cb0d42202525..000000000000 --- a/code/addons/jest/README.md +++ /dev/null @@ -1,261 +0,0 @@ -# Storybook addon Jest - -Storybook addon for inspecting Jest unit test results. - -[Framework Support](https://storybook.js.org/docs/configure/integration/frameworks-feature-support?ref=readme) - -[![Storybook Jest Addon Demo](https://raw.githubusercontent.com/storybookjs/storybook/next/code/addons/jest/docs/storybook-addon-jest.gif)](http://storybooks-official.netlify.com/?selectedKind=Addons%7Cjest&selectedStory=withTests&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybook%2Ftests%2Fpanel) - -> Check out the above [Live Storybook](http://storybooks-official.netlify.com/?selectedKind=Addons%7Cjest&selectedStory=withTests&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybook%2Ftests%2Fpanel). - -## Installation - -Install this addon by adding the `@storybook/addon-jest` as a development dependency with: - -`npm install --save-dev @storybook/addon-jest` - -Or if you're using yarn as a package manager: - -`yarn add --dev @storybook/addon-jest` - -## Configuration - -Register the addon in your [`.storybook/main.js`](https://storybook.js.org/docs/configure#configure-your-storybook-project?ref=readme): - -```js -export default { - addons: ['@storybook/addon-jest'], -}; -``` - -## Jest Configuration - -When running **Jest**, be sure to save the results in a JSON file: - -```js -"scripts": { - "test:generate-output": "jest --json --outputFile=.jest-test-results.json" -} -``` - -You may want to add the result file to `.gitignore`, since it's a generated file: - -``` -.jest-test-results.json -``` - -But much like lockfiles and snapshots, checking-in generated files can have certain advantages as well. It's up to you. -We recommend to **do** check in the test results file so starting Storybook from a clean git clone doesn't require running all tests first, -but this can mean you'll encounter merge conflicts on this file in the future (_re-generating this file is very similar to re-generating lockfiles and snapshots_). - -### Generating the test results - -Ensure the generated test-results file exists before you start Storybook. During development, you will likely start Jest in watch-mode and so the JSON file will be re-generated every time code or tests change. - -```sh -npm run test:generate-output -- --watch -``` - -And in the jest config, add `jest-test-results.json` to `modulePathIgnorePatterns` to avoid an infinite loop. - -```js -modulePathIgnorePatterns: ['node_modules', 'jest-test-results.json'], -``` - -This change will then be HMR (hot module reloaded) using webpack and displayed by this addon. - -If you want to pre-run Jest automatically during development or a static build, you may need to consider that if your tests fail, the script receives a non-0 exit code and will exit. -You could create a `prebuild:storybook` npm script, which will never fail by appending `|| true`: - -```json -"scripts": { - "test:generate-output": "jest --json --outputFile=.jest-test-results.json || true", - "test": "jest", - "prebuild:storybook": "npm run test:generate-output", - "build:storybook": "build-storybook -c .storybook -o build/", - "predeploy": "npm run build:storybook", - "deploy": "gh-pages -d build/", -} -``` - -## Usage - -Assuming that you have already created a test file for your component (e.g., `MyComponent.test.js`). - -### Story-level - -In your story file, add a [decorator](https://storybook.js.org/docs/writing-stories/decorators?ref=readme) to your story's default export to display the results: - -```js -// MyComponent.stories.js|jsx -import { withTests } from '@storybook/addon-jest'; -import results from '../.jest-test-results.json'; -import MyComponent from './MyComponent'; - -export default { - component: MyComponent, - title: 'MyComponent', - decorators: [withTests({ results })], -}; -``` - -You can also add multiple tests results within your story by including the `jest` [parameter](https://storybook.js.org/docs/writing-stories/parameters?ref=readme), for example: - -```js -// MyComponent.stories.js|jsx - -import MyComponent from './MyComponent'; - -import results from '../.jest-test-results.json'; - -import { withTests } from '@storybook/addon-jest'; - -export default { - component: MyComponent, - title: 'MyComponent', - decorators: [withTests({ results })], -}; - -const Template = (args) => ; - -export const Default = Template.bind({}); -Default.args = { - text: 'Jest results in Storybook', -}; -Default.parameters = { - jest: ['MyComponent.test.js', 'MyOtherComponent.test.js'] -}; -``` - -### Global level - -To avoid importing the results of the tests in each story, you can update -your [`.storybook/preview.js`](https://storybook.js.org/docs/configure#configure-story-rendering?ref=readme) and include a decorator allowing you to display the results only for the stories that have the `jest` parameter defined: - -```js -// .storybook/preview.js -import { withTests } from '@storybook/addon-jest'; -import results from '../.jest-test-results.json'; - -export const decorators = [ - withTests({ - results, - }), -]; -``` - -Then in your story file: - -```js -// MyComponent.stories.js|jsx - -import MyComponent from './MyComponent'; - -export default { - component: MyComponent, - title: 'MyComponent', -}; - -const Template = (args) => ; - -export const Default = Template.bind({}); -Default.args={ - text: 'Jest results in Storybook', -}; -Default.parameters = { - jest: 'MyComponent.test.js', -}; -``` - -The `jest` parameter will default to inferring from your story file name if not provided. For example, if your story file is `MyComponent.stories.js`, -then "MyComponent" will be used to find your test file results. It currently doesn't work in production environments. - -### Disabling - -You can disable the addon for a single story by setting the `jest` parameter to `{disable: true}`: - -```js -// MyComponent.stories.js|jsx -import MyComponent from './MyComponent'; - -export default { - component: MyComponent, - title: 'MyComponent', -}; - -const Template = (args) => ; - -export const Default = Template.bind({}); - -Default.args = { - text: 'Jest results in Storybook', -}; -Default.parameters = { - jest: { disable: true }, -}; -``` - -## Usage with Angular - -Using this addon with Angular will require some additional configuration. You'll need to install and configure Jest with [jest-preset-angular](https://www.npmjs.com/package/jest-preset-angular). - -Then, in your `.storybook/preview.js`, you'll need to add a decorator with the following: - -```js -// .storybook/preview.js -import { withTests } from '@storybook/addon-jest'; -import results from '../.jest-test-results.json'; - -export const decorators = [ - withTests({ - results, - filesExt: '((\\.specs?)|(\\.tests?))?(\\.ts)?$', - }), -]; -``` - -Finally, in your story, you'll need to include the following: - -```ts -// MyComponent.stories.ts -import type { Meta, StoryFn } from '@storybook/angular'; -import MyComponent from './MyComponent.component'; - -export default { - component: MyComponent, - title: 'MyComponent', -} as Meta; - -const Template: StoryFn = (args: MyComponent) => ({ - props: args, -}); - -export const Default = Template.bind({}); -Default.parameters = { - jest: 'MyComponent.component', -}; -``` - -## Available options - -- **options.results**: OBJECT jest output results. _mandatory_ -- **filesExt**: STRING test file extension. _optional_. This allows you to write "MyComponent" and not "MyComponent.test.js". It will be used as regex to find your file results. Default value is `((\\.specs?)|(\\.tests?))?(\\.js)?$`. That means it will match: MyComponent.js, MyComponent.test.js, MyComponent.tests.js, MyComponent.spec.js, MyComponent.specs.js... - -## TODO - -- Add coverage -- Display nested test better (describe) -- Display the date of the test -- Add unit tests -- Add linting -- Split - -## Contributing - -All ideas and contributions are welcome. - -## Licence - -MIT © 2017-present Renaud Tertrais - -Learn more about Storybook at [storybook.js.org](https://storybook.js.org/?ref=readme). diff --git a/code/addons/jest/docs/storybook-addon-jest.gif b/code/addons/jest/docs/storybook-addon-jest.gif deleted file mode 100644 index e5ad98b44599..000000000000 Binary files a/code/addons/jest/docs/storybook-addon-jest.gif and /dev/null differ diff --git a/code/addons/jest/manager.js b/code/addons/jest/manager.js deleted file mode 100644 index 8a2eae4ffce1..000000000000 --- a/code/addons/jest/manager.js +++ /dev/null @@ -1 +0,0 @@ -import './dist/manager'; diff --git a/code/addons/jest/package.json b/code/addons/jest/package.json deleted file mode 100644 index 1497a03ffcc9..000000000000 --- a/code/addons/jest/package.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "name": "@storybook/addon-jest", - "version": "9.1.12", - "description": "Storybook Jest addon: Show Jest report in Storybook's addon panel", - "keywords": [ - "jest", - "react", - "report", - "results", - "storybook", - "storybook-addon", - "unit-testing", - "test", - "component", - "components" - ], - "homepage": "https://github.com/storybookjs/storybook/tree/next/code/addons/jest", - "bugs": { - "url": "https://github.com/storybookjs/storybook/issues" - }, - "repository": { - "type": "git", - "url": "https://github.com/storybookjs/storybook.git", - "directory": "code/addons/jest" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "license": "MIT", - "author": "Renaud Tertrais (https://github.com/renaudtertrais)", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - }, - "./manager": "./dist/manager.js", - "./register": "./dist/manager.js", - "./package.json": "./package.json" - }, - "main": "dist/index.js", - "module": "dist/index.mjs", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*", - "README.md", - "*.js", - "*.d.ts", - "!src/**/*" - ], - "scripts": { - "check": "jiti ../../../scripts/prepare/check.ts", - "prep": "jiti ../../../scripts/prepare/addon-bundle.ts" - }, - "dependencies": { - "tiny-invariant": "^1.3.1", - "upath": "^2.0.1" - }, - "devDependencies": { - "@storybook/icons": "^1.4.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-resize-detector": "^7.1.2", - "typescript": "^5.8.3" - }, - "peerDependencies": { - "storybook": "workspace:^" - }, - "publishConfig": { - "access": "public" - }, - "bundler": { - "exportEntries": [ - "./src/index.ts" - ], - "managerEntries": [ - "./src/manager.tsx" - ] - }, - "gitHead": "ce6a1e4a8d5ad69c699021a0b183df89cfc7b684", - "storybook": { - "displayName": "Jest", - "icon": "https://pbs.twimg.com/profile_images/821713465245102080/mMtKIMax_400x400.jpg", - "unsupportedFrameworks": [ - "react-native" - ] - } -} diff --git a/code/addons/jest/project.json b/code/addons/jest/project.json deleted file mode 100644 index 226ea5597330..000000000000 --- a/code/addons/jest/project.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "addon-jest", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "projectType": "library", - "targets": { - "build": {} - } -} diff --git a/code/addons/jest/src/components/Message.tsx b/code/addons/jest/src/components/Message.tsx deleted file mode 100644 index 5937fcbc83ff..000000000000 --- a/code/addons/jest/src/components/Message.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import type { FC, ReactElement } from 'react'; -import React, { Fragment } from 'react'; - -import { styled } from 'storybook/theming'; - -const positiveConsoleRegex = /\[32m(.*?)\[39m/; -const negativeConsoleRegex = /\[31m(.*?)\[39m/; -const positiveType = 'positive'; -const negativeType = 'negative'; -const endToken = '[39m'; -const failStartToken = '[31m'; -const passStartToken = '[32m'; -const stackTraceStartToken = 'at'; -const titleEndToken = ':'; - -type MsgElement = string | ReactElement; - -class TestDetail { - description!: MsgElement[]; - - result!: MsgElement[]; - - stackTrace!: string; -} -const StackTrace = styled.pre(({ theme }) => ({ - background: theme.color.lighter, - paddingTop: 4, - paddingBottom: 4, - paddingLeft: 6, - borderRadius: 2, - overflow: 'auto', - margin: '10px 30px 10px 30px', - whiteSpace: 'pre', -})); - -const Results = styled.div({ - paddingTop: 10, - marginLeft: 31, - marginRight: 30, -}); - -const Description = styled.div(({ theme }) => ({ - paddingBottom: 10, - paddingTop: 10, - borderBottom: theme.appBorderColor, - marginLeft: 31, - marginRight: 30, - overflowWrap: 'break-word', -})); - -const StatusColor = styled.strong<{ status: string }>(({ status, theme }) => ({ - color: status === positiveType ? theme.color.positive : theme.color.negative, - fontWeight: 500, -})); - -const colorizeText: (msg: string, type: string) => MsgElement[] = (msg: string, type: string) => { - if (type) { - return msg - .split(type === positiveType ? positiveConsoleRegex : negativeConsoleRegex) - .map((i, index) => - index % 2 ? ( - - {i} - - ) : ( - i - ) - ); - } - return [msg]; -}; - -const getConvertedText: (msg: string) => MsgElement[] = (msg: string) => { - let elementArray: MsgElement[] = []; - - if (!msg) { - return elementArray; - } - - const splitText = msg.split(/\[2m/).join('').split(/\[22m/); - - splitText.forEach((element) => { - if (element && element.trim()) { - if ( - element.indexOf(failStartToken) > -1 && - element.indexOf(failStartToken) < element.indexOf(endToken) - ) { - elementArray = elementArray.concat(colorizeText(element, negativeType)); - } else if ( - element.indexOf(passStartToken) > -1 && - element.indexOf(passStartToken) < element.indexOf(endToken) - ) { - elementArray = elementArray.concat(colorizeText(element, positiveType)); - } else { - elementArray = elementArray.concat(element); - } - } - }); - return elementArray; -}; - -const getTestDetail: (msg: string) => TestDetail = (msg: string) => { - const lines = msg.split('\n').filter(Boolean); - - const testDetail: TestDetail = new TestDetail(); - testDetail.description = getConvertedText(lines[0]); - testDetail.stackTrace = ''; - testDetail.result = []; - - for (let index = 1; index < lines.length; index += 1) { - const current = lines[index]; - const next = lines[index + 1]; - - if (current.trim().toLowerCase().indexOf(stackTraceStartToken) === 0) { - testDetail.stackTrace += `${current.trim()}\n`; - } else if (current.trim().indexOf(titleEndToken) > -1) { - let title; - let value = null; - if (current.trim().indexOf(titleEndToken) === current.length - 1) { - // there are breaks in the middle of result - title = current.trim(); - value = getConvertedText(next); - index += 1; - } else { - // results come in a single line - title = current.substring(0, current.indexOf(titleEndToken)).trim(); - value = getConvertedText(current.substring(current.indexOf(titleEndToken), current.length)); - } - testDetail.result = [...testDetail.result, title, ' ', ...value,
]; - } else { - // results come in an unexpected format - testDetail.result = [...testDetail.result, ' ', ...getConvertedText(current)]; - } - } - - return testDetail; -}; - -interface MessageProps { - msg: string; -} - -export const Message: FC = (props) => { - const { msg } = props; - - const detail: TestDetail = getTestDetail(msg); - return ( - - {detail.description ? {detail.description} : null} - {detail.result ? {detail.result} : null} - {detail.stackTrace ? {detail.stackTrace} : null} - - ); -}; - -export default Message; diff --git a/code/addons/jest/src/components/Panel.tsx b/code/addons/jest/src/components/Panel.tsx deleted file mode 100644 index b6b053cda1f0..000000000000 --- a/code/addons/jest/src/components/Panel.tsx +++ /dev/null @@ -1,296 +0,0 @@ -import type { FC } from 'react'; -import React, { Fragment } from 'react'; - -import { Link, Placeholder, ScrollArea, TabsState } from 'storybook/internal/components'; - -import { useResizeDetector } from 'react-resize-detector'; -import { convert, styled, themes } from 'storybook/theming'; - -import type { Test } from '../hoc/provideJestResult'; -import { provideTests as provideJestResult } from '../hoc/provideJestResult'; -import { Result } from './Result'; - -const StatusTypes = { - PASSED_TYPE: 'passed', - FAILED_TYPE: 'failed', - PENDING_TYPE: 'pending', - TODO_TYPE: 'todo', -}; - -const List = styled.ul({ - listStyle: 'none', - fontSize: 14, - padding: 0, - margin: 0, -}); - -const Item = styled.li({ - display: 'block', - padding: 0, -}); - -const ProgressWrapper = styled.div({ - position: 'relative', - height: 10, - width: 30, - display: 'flex', - top: -2, -}); - -const SuiteHead = styled.div({ - display: 'flex', - alignItems: 'baseline', - position: 'absolute', - zIndex: 2, - right: 20, - marginTop: 15, -}); - -const UnstyledSuiteTotals: FC<{ - result: Test['result']; - className?: string; - width: number; -}> = ({ result, className, width }) => ( -
- - {width > 325 && result.assertionResults ? ( -
- {result.assertionResults.length} {result.assertionResults.length > 1 ? `tests` : `test`} -
- ) : null} - {width > 280 && result.endTime && result.startTime ? ( -
- {result.endTime - result.startTime} - ms -
- ) : null} -
-
-); -const SuiteTotals = styled(UnstyledSuiteTotals)(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - color: theme.color.dark, - fontSize: '14px', - marginTop: -5, - '& > *': { - marginRight: 10, - }, -})); - -const SuiteProgressPortion = styled.div<{ color?: string; progressPercent: number }>( - ({ color, progressPercent }) => ({ - height: 6, - top: 3, - width: `${progressPercent}%`, - backgroundColor: color, - }) -); - -interface ContentProps { - tests: Test[]; - className?: string; -} - -const getTestsByTypeMap = (result: any) => { - const testsByType: Map = new Map(); - result.assertionResults.forEach((assertion: any) => { - testsByType.set( - assertion.status, - testsByType.get(assertion.status) - ? testsByType.get(assertion.status).concat(assertion) - : [assertion] - ); - }); - return testsByType; -}; - -const getColorByType = (type: string) => { - // using switch to allow for new types to be added - switch (type) { - case StatusTypes.PASSED_TYPE: - return convert(themes.light).color.positive; - case StatusTypes.FAILED_TYPE: - return convert(themes.light).color.negative; - case StatusTypes.PENDING_TYPE: - return convert(themes.light).color.warning; - case StatusTypes.TODO_TYPE: - return convert(themes.light).color.purple; - default: - return undefined; - } -}; - -const TestPanel: FC<{ test: Test }> = ({ test }) => { - const { ref, width } = useResizeDetector(); - const { result } = test; - if (!result || !result.assertionResults) { - return This story has tests configured, but no file was found; - } - - const testsByType: Map = getTestsByTypeMap(result); - const entries: any = testsByType.entries(); - const sortedTestsByCount = [...entries].sort((a, b) => a[1].length - b[1].length); - - return ( -
- - - {width != null && width > 240 ? ( - - {sortedTestsByCount.map((entry: any) => { - return ( - - ); - })} - - ) : null} - - -
- - {testsByType.get(StatusTypes.FAILED_TYPE) ? ( - testsByType.get(StatusTypes.FAILED_TYPE).map((res: any) => ( - - - - )) - ) : ( - - This story has no failing tests. - - )} - -
-
- - {testsByType.get(StatusTypes.PASSED_TYPE) ? ( - testsByType.get(StatusTypes.PASSED_TYPE).map((res: any) => ( - - - - )) - ) : ( - - This story has no passing tests. - - )} - -
-
- - {testsByType.get(StatusTypes.PENDING_TYPE) ? ( - testsByType.get(StatusTypes.PENDING_TYPE).map((res: any) => ( - - - - )) - ) : ( - - This story has no pending tests. - - )} - -
-
- - {testsByType.get(StatusTypes.TODO_TYPE) ? ( - testsByType.get(StatusTypes.TODO_TYPE).map((res: any) => ( - - - - )) - ) : ( - - This story has no tests todo. - - )} - -
-
-
- ); -}; - -const Content = styled(({ tests, className }: ContentProps) => ( -
- {tests.map((test) => ( - - ))} -
-))({ - flex: '1 1 0%', -}); - -interface PanelProps { - tests?: Test[]; -} - -const Panel = ({ tests }: PanelProps) => ( - - {tests ? ( - - ) : ( - - No tests found - - Learn how to  - - add Jest test results to your story - - - - )} - -); - -Panel.defaultProps = { - tests: undefined, -}; - -export default provideJestResult(Panel); diff --git a/code/addons/jest/src/components/Result.tsx b/code/addons/jest/src/components/Result.tsx deleted file mode 100644 index 25b9281dd717..000000000000 --- a/code/addons/jest/src/components/Result.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React, { Fragment, useState } from 'react'; - -import { ChevronSmallDownIcon } from '@storybook/icons'; - -import { convert, styled, themes } from 'storybook/theming'; - -import Message from './Message'; - -const Wrapper = styled.div<{ status: string }>(({ theme, status }) => ({ - display: 'flex', - width: '100%', - borderTop: `1px solid ${theme.appBorderColor}`, - '&:hover': { - background: status === `failed` ? theme.background.hoverable : undefined, - }, -})); - -const HeaderBar = styled.div<{ status: string }>(({ theme, status }) => ({ - padding: theme.layoutMargin, - paddingLeft: theme.layoutMargin - 3, - background: 'none', - color: 'inherit', - textAlign: 'left', - cursor: status === `failed` ? 'pointer' : undefined, - borderLeft: '3px solid transparent', - width: '100%', - display: 'flex', - - '&:focus': { - outline: '0 none', - borderLeft: `3px solid ${theme.color.secondary}`, - }, -})); - -const Icon = styled(ChevronSmallDownIcon)(({ theme }) => ({ - color: theme.textMutedColor, - marginRight: 10, - transition: 'transform 0.1s ease-in-out', - alignSelf: 'center', - display: 'inline-flex', -})); - -const capitalizeFirstLetter = (text: string) => { - return text.charAt(0).toUpperCase().concat(text.slice(1)); -}; - -interface ResultProps { - fullName?: string; - title?: string; - failureMessages: any; - status: string; -} - -export function Result(props: ResultProps) { - const [isOpen, setIsOpen] = useState(false); - - const onToggle = () => { - setIsOpen(!isOpen); - }; - - const { fullName, title, failureMessages, status } = props; - return ( - - - - {status === `failed` ? ( - - ) : null} -
{capitalizeFirstLetter(fullName ?? '') || capitalizeFirstLetter(title ?? '')}
-
-
- {isOpen ? ( - - {failureMessages.map((msg: string, i: number) => ( - - ))} - - ) : null} -
- ); -} diff --git a/code/addons/jest/src/hoc/provideJestResult.tsx b/code/addons/jest/src/hoc/provideJestResult.tsx deleted file mode 100644 index 1e2b61bb7c65..000000000000 --- a/code/addons/jest/src/hoc/provideJestResult.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import type { ComponentType } from 'react'; -import React, { Component as ReactComponent } from 'react'; - -import { STORY_CHANGED } from 'storybook/internal/core-events'; - -import type { API } from 'storybook/manager-api'; - -import { ADD_TESTS } from '../shared'; - -// TODO: import type from @types/jest -interface AssertionResult { - status: string; - fullName: string; - title: string; - failureMessages: string[]; -} - -export interface Test { - name: string; - result: { - status: string; - startTime?: number; - endTime?: number; - assertionResults: AssertionResult[]; - }; -} - -interface InjectedProps { - tests?: Test[]; -} - -export interface HocProps { - api: API; - active?: boolean; -} - -export interface HocState { - kind?: string; - storyName?: string; - tests?: Test[]; -} - -export const provideTests = (Component: ComponentType) => - class TestProvider extends ReactComponent { - state: HocState = {}; - - static defaultProps = { - active: false, - }; - - componentDidMount() { - this.mounted = true; - const { api } = this.props; - - this.stopListeningOnStory = api.on(STORY_CHANGED, () => { - const { kind, storyName, tests } = this.state; - if (this.mounted && (kind || storyName || tests)) { - this.onAddTests({}); - } - }); - - api.on(ADD_TESTS, this.onAddTests); - } - - componentWillUnmount() { - this.mounted = false; - const { api } = this.props; - - this.stopListeningOnStory(); - api.off(ADD_TESTS, this.onAddTests); - } - - onAddTests = ({ kind, storyName, tests }: HocState) => { - this.setState({ kind, storyName, tests }); - }; - - mounted!: boolean; - - stopListeningOnStory!: () => void; - - render() { - const { active } = this.props; - const { tests } = this.state; - - return active ? : null; - } - }; diff --git a/code/addons/jest/src/index.ts b/code/addons/jest/src/index.ts deleted file mode 100644 index a812f8daf551..000000000000 --- a/code/addons/jest/src/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { addons } from 'storybook/preview-api'; -import { normalize, sep } from 'upath'; - -import { ADD_TESTS, defineJestParameter } from './shared'; - -const findTestResults = ( - testFiles: string[], - jestTestResults: { testResults: { name: string }[] }, - jestTestFilesExt: string -) => - Object.values(testFiles).map((name) => { - const fileName = `${sep}${name}${jestTestFilesExt}`; - - if (jestTestResults && jestTestResults.testResults) { - const fileNamePattern = new RegExp(fileName); - - return { - fileName, - name, - result: jestTestResults.testResults.find((test) => - Boolean(normalize(test.name).match(fileNamePattern)) - ), - }; - } - - return { fileName, name }; - }); - -interface EmitAddTestsArg { - kind: string; - story: () => void; - testFiles: string[]; - options: { - results: { testResults: { name: string }[] }; - filesExt: string; - }; -} - -const emitAddTests = ({ kind, story, testFiles, options }: EmitAddTestsArg) => { - addons.getChannel().emit(ADD_TESTS, { - kind, - storyName: story, - tests: findTestResults(testFiles, options.results, options.filesExt), - }); -}; - -export const withTests = (userOptions: { results: any; filesExt?: string }) => { - const defaultOptions = { - filesExt: '((\\.specs?)|(\\.tests?))?(\\.[jt]sx?)?$', - }; - const options = { ...defaultOptions, ...userOptions }; - - return (...args: any[]) => { - const [storyFn, { kind, parameters = {} }] = args; - const testFiles = defineJestParameter(parameters); - - if (testFiles !== null) { - emitAddTests({ kind, story: storyFn, testFiles, options }); - } - - return storyFn(); - }; -}; diff --git a/code/addons/jest/src/manager.tsx b/code/addons/jest/src/manager.tsx deleted file mode 100644 index 06dc73dd0d5a..000000000000 --- a/code/addons/jest/src/manager.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; - -import { addons, types } from 'storybook/manager-api'; - -import Panel from './components/Panel'; -import { ADDON_ID, PANEL_ID, PARAM_KEY } from './shared'; - -addons.register(ADDON_ID, (api) => { - addons.add(PANEL_ID, { - title: 'Tests', - type: types.PANEL, - render: ({ active }) => , - paramKey: PARAM_KEY, - }); -}); diff --git a/code/addons/jest/src/shared.test.ts b/code/addons/jest/src/shared.test.ts deleted file mode 100644 index 6ef232fe5a32..000000000000 --- a/code/addons/jest/src/shared.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { defineJestParameter } from './shared'; - -describe('defineJestParameter', () => { - it('infers from story file name if jest parameter is not provided', () => { - expect(defineJestParameter({ fileName: './stories/addon-jest.stories.js' })).toEqual([ - 'addon-jest', - ]); - }); - - it('wraps string jest parameter with an array', () => { - expect(defineJestParameter({ jest: 'addon-jest' })).toEqual(['addon-jest']); - }); - - it('returns as is if jest parameter is an array', () => { - expect(defineJestParameter({ jest: ['addon-jest', 'something-else'] })).toEqual([ - 'addon-jest', - 'something-else', - ]); - }); - - it('returns null if disabled option is passed to jest parameter', () => { - expect(defineJestParameter({ jest: { disabled: true } })).toBeNull(); - }); - - it('returns null if no filename to infer from', () => { - expect(defineJestParameter({})).toBeNull(); - }); - - it('returns null if filename is a module ID that cannot be inferred from', () => { - // @ts-expect-error Storybook's fileName type is string, but according to this test it could be number in case it is a module id. - expect(defineJestParameter({ fileName: 1234 })).toBeNull(); - }); -}); diff --git a/code/addons/jest/src/shared.ts b/code/addons/jest/src/shared.ts deleted file mode 100644 index 24a705e39724..000000000000 --- a/code/addons/jest/src/shared.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { StorybookInternalParameters } from 'storybook/internal/types'; - -import invariant from 'tiny-invariant'; - -import type { JestParameters } from './types'; - -// addons, panels and events get unique names using a prefix -export const PARAM_KEY = 'test'; -export const ADDON_ID = 'storybookjs/test'; -export const PANEL_ID = `${ADDON_ID}/panel`; - -export const ADD_TESTS = `${ADDON_ID}/add_tests`; - -export function defineJestParameter( - parameters: JestParameters & StorybookInternalParameters -): string[] | null { - const { jest, fileName: filePath } = parameters; - - if (typeof jest === 'string') { - return [jest]; - } - - if (jest && Array.isArray(jest)) { - return jest; - } - - if (jest === undefined && typeof filePath === 'string') { - const lastPath = filePath.split('/').pop(); - invariant(lastPath != null, 'split should always return at least one value'); - const fileName = lastPath.split('.')[0]; - return [fileName]; - } - - return null; -} diff --git a/code/addons/jest/src/types.ts b/code/addons/jest/src/types.ts deleted file mode 100644 index 998c0254d83f..000000000000 --- a/code/addons/jest/src/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface JestParameters { - /** - * Jest configuration - * - * @see https://github.com/storybookjs/storybook/blob/next/code/addons/jest/README.md#usage - */ - jest?: string | string[] | { disabled: true }; -} diff --git a/code/addons/jest/tsconfig.json b/code/addons/jest/tsconfig.json deleted file mode 100644 index 73a65ef2ef6e..000000000000 --- a/code/addons/jest/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": {}, - "include": ["src/**/*"] -} diff --git a/code/addons/jest/vitest.config.ts b/code/addons/jest/vitest.config.ts deleted file mode 100644 index 7420176b2e46..000000000000 --- a/code/addons/jest/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig, mergeConfig } from 'vitest/config'; - -import { vitestCommonConfig } from '../../vitest.workspace'; - -export default mergeConfig( - vitestCommonConfig, - defineConfig({ - // Add custom config here - }) -); diff --git a/code/core/src/common/versions.ts b/code/core/src/common/versions.ts index 55a387689a2f..1b3bfeac1cb8 100644 --- a/code/core/src/common/versions.ts +++ b/code/core/src/common/versions.ts @@ -2,7 +2,6 @@ export default { '@storybook/addon-a11y': '9.1.12', '@storybook/addon-docs': '9.1.12', - '@storybook/addon-jest': '9.1.12', '@storybook/addon-links': '9.1.12', '@storybook/addon-onboarding': '9.1.12', 'storybook-addon-pseudo-states': '9.1.12', diff --git a/code/core/src/core-server/__for-testing__/main.ts b/code/core/src/core-server/__for-testing__/main.ts index f736e59cf225..ca0837f2adfe 100644 --- a/code/core/src/core-server/__for-testing__/main.ts +++ b/code/core/src/core-server/__for-testing__/main.ts @@ -18,7 +18,6 @@ const config: StorybookConfig = { }, }, '@storybook/addon-essentials', - '@storybook/addon-jest', '@storybook/addon-a11y', ], core: { diff --git a/code/core/src/csf-tools/CsfFile.test.ts b/code/core/src/csf-tools/CsfFile.test.ts index a3ff6cee8cee..a0238be4da04 100644 --- a/code/core/src/csf-tools/CsfFile.test.ts +++ b/code/core/src/csf-tools/CsfFile.test.ts @@ -1422,6 +1422,40 @@ describe('CsfFile', () => { `); }); + it('Object export with args render method', () => { + expect( + parse( + dedent` + export default { title: 'foo/bar' }; + export const A = { + render(args) {} + } + `, + true + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: A + parameters: + __isArgsStory: true + __id: foo-bar--a + __stats: + factory: false + play: false + render: true + loaders: false + beforeEach: false + globals: false + tags: false + storyFn: false + mount: false + moduleMock: false + `); + }); + it('Object export with default render', () => { expect( parse( @@ -1787,6 +1821,67 @@ describe('CsfFile', () => { `); }); + it('play method', () => { + expect( + parse( + dedent` + export default { title: 'foo/bar' }; + export const A = { + play({ context }) {}, + }; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: A + __stats: + factory: false + play: true + render: false + loaders: false + beforeEach: false + globals: false + tags: false + storyFn: false + mount: false + moduleMock: false + tags: + - play-fn + `); + }); + + it('meta play method', () => { + expect( + parse( + dedent` + export default { title: 'foo/bar', play({ context }) {} }; + export const A = {};` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + tags: + - play-fn + stories: + - id: foo-bar--a + name: A + __stats: + factory: false + play: true + render: false + loaders: false + beforeEach: false + globals: false + tags: false + storyFn: false + mount: false + moduleMock: false + `); + }); + it('mount', () => { expect( parse( @@ -1851,6 +1946,38 @@ describe('CsfFile', () => { `); }); + it('mount in method', () => { + expect( + parse( + dedent` + export default { title: 'foo/bar' }; + export const A = { + play({ mount, context }) {}, + }; + ` + ) + ).toMatchInlineSnapshot(` + meta: + title: foo/bar + stories: + - id: foo-bar--a + name: A + __stats: + factory: false + play: true + render: false + loaders: false + beforeEach: false + globals: false + tags: false + storyFn: false + mount: true + moduleMock: false + tags: + - play-fn + `); + }); + it('mount meta', () => { expect( parse( diff --git a/code/core/src/csf-tools/CsfFile.ts b/code/core/src/csf-tools/CsfFile.ts index ab23eb33045a..16d268d49022 100644 --- a/code/core/src/csf-tools/CsfFile.ts +++ b/code/core/src/csf-tools/CsfFile.ts @@ -147,7 +147,11 @@ const sortExports = (exportByName: Record, order: string[]) => { }; const hasMount = (play: t.Node | undefined) => { - if (t.isArrowFunctionExpression(play) || t.isFunctionDeclaration(play)) { + if ( + t.isArrowFunctionExpression(play) || + t.isFunctionDeclaration(play) || + t.isObjectMethod(play) + ) { const params = play.params; if (params.length >= 1) { const [arg] = params; @@ -309,7 +313,8 @@ export class CsfFile { const meta: StaticMeta = {}; (declaration.properties as t.ObjectProperty[]).forEach((p) => { if (t.isIdentifier(p.key)) { - this._metaAnnotations[p.key.name] = p.value; + const value = t.isObjectMethod(p) ? p : p.value; + this._metaAnnotations[p.key.name] = value; if (p.key.name === 'title') { meta.title = this._parseTitle(p.value); @@ -559,30 +564,35 @@ export class CsfFile { // CSF3 object export (storyNode.properties as t.ObjectProperty[]).forEach((p) => { if (t.isIdentifier(p.key)) { - if (p.key.name === 'render') { - parameters.__isArgsStory = isArgsStory( - p.value as t.Expression, - parent, - self - ); - } else if (p.key.name === 'name' && t.isStringLiteral(p.value)) { - name = p.value.value; - } else if (p.key.name === 'storyName' && t.isStringLiteral(p.value)) { - logger.warn( - `Unexpected usage of "storyName" in "${exportName}". Please use "name" instead.` - ); - } else if (p.key.name === 'parameters' && t.isObjectExpression(p.value)) { - const idProperty = p.value.properties.find( - (property) => - t.isObjectProperty(property) && - t.isIdentifier(property.key) && - property.key.name === '__id' - ) as t.ObjectProperty | undefined; - if (idProperty) { - parameters.__id = (idProperty.value as t.StringLiteral).value; + const key = p.key.name; + if (t.isObjectMethod(p)) { + self._storyAnnotations[exportName][key] = p; + } else { + if (p.key.name === 'render') { + parameters.__isArgsStory = isArgsStory( + p.value as t.Expression, + parent, + self + ); + } else if (p.key.name === 'name' && t.isStringLiteral(p.value)) { + name = p.value.value; + } else if (p.key.name === 'storyName' && t.isStringLiteral(p.value)) { + logger.warn( + `Unexpected usage of "storyName" in "${exportName}". Please use "name" instead.` + ); + } else if (p.key.name === 'parameters' && t.isObjectExpression(p.value)) { + const idProperty = p.value.properties.find( + (property) => + t.isObjectProperty(property) && + t.isIdentifier(property.key) && + property.key.name === '__id' + ) as t.ObjectProperty | undefined; + if (idProperty) { + parameters.__id = (idProperty.value as t.StringLiteral).value; + } } + self._storyAnnotations[exportName][p.key.name] = p.value; } - self._storyAnnotations[exportName][p.key.name] = p.value; } }); } else { diff --git a/code/package.json b/code/package.json index feec6602a39f..aa5395f44bee 100644 --- a/code/package.json +++ b/code/package.json @@ -110,7 +110,6 @@ "@storybook/addon-a11y": "workspace:*", "@storybook/addon-designs": "9.0.0-next.1", "@storybook/addon-docs": "workspace:*", - "@storybook/addon-jest": "workspace:*", "@storybook/addon-links": "workspace:*", "@storybook/addon-onboarding": "workspace:*", "@storybook/addon-themes": "workspace:*", @@ -282,5 +281,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "9.1.13" } diff --git a/code/yarn.lock b/code/yarn.lock index 8533a3ddaa2a..6eb7bd0bddd3 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6010,22 +6010,6 @@ __metadata: languageName: unknown linkType: soft -"@storybook/addon-jest@workspace:*, @storybook/addon-jest@workspace:addons/jest": - version: 0.0.0-use.local - resolution: "@storybook/addon-jest@workspace:addons/jest" - dependencies: - "@storybook/icons": "npm:^1.4.0" - react: "npm:^18.2.0" - react-dom: "npm:^18.2.0" - react-resize-detector: "npm:^7.1.2" - tiny-invariant: "npm:^1.3.1" - typescript: "npm:^5.8.3" - upath: "npm:^2.0.1" - peerDependencies: - storybook: "workspace:^" - languageName: unknown - linkType: soft - "@storybook/addon-links@workspace:*, @storybook/addon-links@workspace:addons/links": version: 0.0.0-use.local resolution: "@storybook/addon-links@workspace:addons/links" @@ -6720,7 +6704,6 @@ __metadata: "@storybook/addon-a11y": "workspace:*" "@storybook/addon-designs": "npm:9.0.0-next.1" "@storybook/addon-docs": "workspace:*" - "@storybook/addon-jest": "workspace:*" "@storybook/addon-links": "workspace:*" "@storybook/addon-onboarding": "workspace:*" "@storybook/addon-themes": "workspace:*" @@ -26210,13 +26193,6 @@ __metadata: languageName: node linkType: hard -"upath@npm:^2.0.1": - version: 2.0.1 - resolution: "upath@npm:2.0.1" - checksum: 10c0/79e8e1296b00e24a093b077cfd7a238712d09290c850ce59a7a01458ec78c8d26dcc2ab50b1b9d6a84dabf6511fb4969afeb8a5c9a001aa7272b9cc74c34670f - languageName: node - linkType: hard - "update-browserslist-db@npm:^1.1.1": version: 1.1.3 resolution: "update-browserslist-db@npm:1.1.3" diff --git a/docs/_snippets/addon-a11y-config-in-preview.md b/docs/_snippets/addon-a11y-config-in-preview.md index 4dfbf5e8bd55..65c957f01a63 100644 --- a/docs/_snippets/addon-a11y-config-in-preview.md +++ b/docs/_snippets/addon-a11y-config-in-preview.md @@ -22,7 +22,7 @@ export default { options: {}, }, }, - globals: { + initialGlobals: { a11y: { // Optional flag to prevent the automatic check manual: true, @@ -58,7 +58,7 @@ const preview: Preview = { options: {}, }, }, - globals: { + initialGlobals: { a11y: { // Optional flag to prevent the automatic check manual: true, diff --git a/docs/configure/integration/frameworks-feature-support.mdx b/docs/configure/integration/frameworks-feature-support.mdx index 7cb43797d5ca..1d0644497b2b 100644 --- a/docs/configure/integration/frameworks-feature-support.mdx +++ b/docs/configure/integration/frameworks-feature-support.mdx @@ -34,7 +34,7 @@ Core frameworks have dedicated maintainers or contributors who are responsible f | [Events](https://github.com/storybookjs/addon-events) | ✅ | ✅ | ✅ | ✅ | | [Google analytics](https://github.com/storybookjs/addon-google-analytics) | ✅ | ✅ | ✅ | ✅ | | [GraphQL](https://github.com/storybookjs/addon-graphql) | ✅ | | ✅ | | -| [Jest](https://github.com/storybookjs/storybook/tree/next/code/addons/jest) | ✅ | ✅ | ✅ | ✅ | +| [Jest](https://github.com/storybookjs/addon-jest) | ✅ | ✅ | ✅ | ✅ | | [Links](https://github.com/storybookjs/storybook/tree/next/code/addons/links) | ✅ | ✅ | ✅ | ✅ | | [Queryparams](https://github.com/storybookjs/addon-queryparams) | ✅ | ✅ | ✅ | ✅ | | **Docs** | | | | | @@ -83,7 +83,7 @@ Community frameworks have fewer contributors which means they may not be as up t | [Events](https://github.com/storybookjs/addon-events) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [Google analytics](https://github.com/storybookjs/addon-google-analytics) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [GraphQL](https://github.com/storybookjs/addon-graphql) | | | | | | | -| [Jest](https://github.com/storybookjs/storybook/tree/next/code/addons/jest) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| [Jest](https://github.com/storybookjs/addon-jest) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [Links](https://github.com/storybookjs/storybook/tree/next/code/addons/links) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | [Queryparams](https://github.com/storybookjs/addon-queryparams) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | **Docs** | | | | | | | diff --git a/docs/versions/latest.json b/docs/versions/latest.json index 742e9709584b..9ad5a6d3ee45 100644 --- a/docs/versions/latest.json +++ b/docs/versions/latest.json @@ -1 +1 @@ -{"version":"9.1.11","info":{"plain":"- Automigration: Improve the viewport/backgrounds automigration - [#32619](https://github.com/storybookjs/storybook/pull/32619), thanks @valentinpalkovic!\n- Mocking: Fix `sb.mock` usage in Storybook's deployed in subpaths - [#32678](https://github.com/storybookjs/storybook/pull/32678), thanks @valentinpalkovic!\n- NextJS-Vite: Automatically fix bad PostCSS configuration - [#32691](https://github.com/storybookjs/storybook/pull/32691), thanks @ndelangen!\n- React Native Web: Fix REACT_NATIVE_AND_RNW should detect vite builder - [#32718](https://github.com/storybookjs/storybook/pull/32718), thanks @dannyhw!\n- Telemetry: Add metadata for react routers - [#32615](https://github.com/storybookjs/storybook/pull/32615), thanks @shilman!"}} +{"version":"9.1.13","info":{"plain":"- CSF: Fix `play-fn` tag for methods - [#32695](https://github.com/storybookjs/storybook/pull/32695), thanks @shilman!"}} diff --git a/docs/versions/next.json b/docs/versions/next.json index f9deff565439..4cb7c3383fae 100644 --- a/docs/versions/next.json +++ b/docs/versions/next.json @@ -1 +1 @@ -{"version":"10.0.0-beta.13","info":{"plain":"- CLI: CSF factories codemod - support annotations in npx context - [#32741](https://github.com/storybookjs/storybook/pull/32741), thanks @yannbf!\n- Move: Addon jest into it's own repository - [#32646](https://github.com/storybookjs/storybook/pull/32646), thanks @ndelangen!\n- Upgrade: Enhance ESM compatibility checks and banner generation - [#32694](https://github.com/storybookjs/storybook/pull/32694), thanks @ndelangen!"}} \ No newline at end of file +{"version":"10.0.0-rc.0","info":{"plain":"- A11Y: Bugfix missing `manager.js` entry-file - [#32780](https://github.com/storybookjs/storybook/pull/32780), thanks @ndelangen!\n- Addon Vitest: Support modifying mergeConfig on addon setup - [#32753](https://github.com/storybookjs/storybook/pull/32753), thanks @yannbf!\n- CLI: Change message in downgrade-blocker - [#32745](https://github.com/storybookjs/storybook/pull/32745), thanks @ndelangen!"}} \ No newline at end of file diff --git a/scripts/verdaccio.yaml b/scripts/verdaccio.yaml index 1854b60bfc41..96ad355659e8 100644 --- a/scripts/verdaccio.yaml +++ b/scripts/verdaccio.yaml @@ -125,6 +125,10 @@ packages: access: $all publish: $all proxy: npmjs + '@storybook/addon-jest': + access: $all + publish: $all + proxy: npmjs '@storybook/addon-coverage': access: $all publish: $all