diff --git a/code/renderers/vue3/package.json b/code/renderers/vue3/package.json index af026e8e3326..71a3ba246e7a 100644 --- a/code/renderers/vue3/package.json +++ b/code/renderers/vue3/package.json @@ -59,6 +59,7 @@ }, "devDependencies": { "@digitak/esrun": "^3.2.2", + "@testing-library/vue": "^8.0.0", "@types/prettier": "^3.0.0", "@vitejs/plugin-vue": "^4.4.0", "typescript": "^5.3.2", diff --git a/code/renderers/vue3/src/__tests__/button.css b/code/renderers/vue3/src/__tests__/button.css new file mode 100644 index 000000000000..dc91dc76370b --- /dev/null +++ b/code/renderers/vue3/src/__tests__/button.css @@ -0,0 +1,30 @@ +.storybook-button { + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-weight: 700; + border: 0; + border-radius: 3em; + cursor: pointer; + display: inline-block; + line-height: 1; +} +.storybook-button--primary { + color: white; + background-color: #1ea7fd; +} +.storybook-button--secondary { + color: #333; + background-color: transparent; + box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; +} +.storybook-button--small { + font-size: 12px; + padding: 10px 16px; +} +.storybook-button--medium { + font-size: 14px; + padding: 11px 20px; +} +.storybook-button--large { + font-size: 16px; + padding: 12px 24px; +} diff --git a/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts b/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts new file mode 100644 index 000000000000..1b14978a9976 --- /dev/null +++ b/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts @@ -0,0 +1,121 @@ +import { userEvent, within } from '@storybook/testing-library'; +import type { Meta, StoryFn as CSF2Story, StoryObj } from '../..'; + +import Button from './Button.vue'; + +const meta = { + title: 'Example/Button', + component: Button, + argTypes: { + size: { control: 'select', options: ['small', 'medium', 'large'] }, + backgroundColor: { control: 'color' }, + onClick: { action: 'clicked' }, + }, + args: { primary: false }, + excludeStories: /.*ImNotAStory$/, +} as Meta; + +export default meta; +type CSF3Story = StoryObj; + +// For testing purposes. Should be ignored in ComposeStories +export const ImNotAStory = 123; + +const Template: CSF2Story = (args) => ({ + components: { Button }, + setup() { + return { args }; + }, + template: ' + + +`; + +exports[`Renders CSF2StoryWithLocale story 1`] = ` + +
+
+

+ locale: undefined +

+ +
+
+ +`; + +exports[`Renders CSF3Button story 1`] = ` + +
+ +
+ +`; + +exports[`Renders CSF3ButtonWithRender story 1`] = ` + +
+
+

+ I am a custom render function +

+ +
+
+ +`; + +exports[`Renders CSF3InputFieldFilled story 1`] = ` + +
+ +
+ +`; + +exports[`Renders CSF3Primary story 1`] = ` + +
+ +
+ +`; diff --git a/code/renderers/vue3/src/__tests__/composeStories/composeStories.test.ts b/code/renderers/vue3/src/__tests__/composeStories/composeStories.test.ts new file mode 100644 index 000000000000..cb2372a4c4e9 --- /dev/null +++ b/code/renderers/vue3/src/__tests__/composeStories/composeStories.test.ts @@ -0,0 +1,108 @@ +/// ; +import { it, expect, vi, describe } from 'vitest'; +import { render, screen } from '@testing-library/vue'; +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'; + +// example with composeStories, returns an object with all stories composed with args/decorators +const { CSF3Primary } = composeStories(stories); + +// example with composeStory, returns a single story composed with args/decorators +const Secondary = composeStory(stories.CSF2Secondary, stories.default); + +it('renders primary button', () => { + render(CSF3Primary({ label: 'Hello world' })); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).toBeInTheDocument(); +}); + +it('reuses args from composed story', () => { + render(Secondary()); + const buttonElement = screen.getByRole('button'); + expect(buttonElement.textContent).toEqual(Secondary.args.label); +}); + +it('myClickEvent handler is called', async () => { + const myClickEventSpy = vi.fn(); + render(Secondary({ onMyClickEvent: myClickEventSpy })); + const buttonElement = screen.getByRole('button'); + buttonElement.click(); + expect(myClickEventSpy).toHaveBeenCalled(); +}); + +it('reuses args from composeStories', () => { + const { getByText } = render(CSF3Primary()); + const buttonElement = getByText(/foo/i); + expect(buttonElement).toBeInTheDocument(); +}); + +describe('projectAnnotations', () => { + it('renders with default projectAnnotations', () => { + const WithEnglishText = composeStory(stories.CSF2StoryWithLocale, stories.default); + const { getByText } = render(WithEnglishText()); + const buttonElement = getByText('Hello!'); + expect(buttonElement).toBeInTheDocument(); + }); + + it('renders with custom projectAnnotations via composeStory params', () => { + const WithPortugueseText = composeStory(stories.CSF2StoryWithLocale, stories.default, { + globalTypes: { locale: { defaultValue: 'pt' } } as any, + }); + const { getByText } = render(WithPortugueseText()); + const buttonElement = getByText('Olá!'); + expect(buttonElement).toBeInTheDocument(); + }); + + it('renders with custom projectAnnotations via setProjectAnnotations', () => { + setProjectAnnotations([{ parameters: { injected: true } }]); + const Story = composeStory(stories.CSF2StoryWithLocale, stories.default); + expect(Story.parameters?.injected).toBe(true); + }); +}); + +describe('CSF3', () => { + it('renders with inferred globalRender', () => { + const Primary = composeStory(stories.CSF3Button, stories.default); + + render(Primary({ label: 'Hello world' })); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).toBeInTheDocument(); + }); + + it('renders with custom render function', () => { + const Primary = composeStory(stories.CSF3ButtonWithRender, stories.default); + + render(Primary()); + expect(screen.getByTestId('custom-render')).toBeInTheDocument(); + }); + + it('renders with play function', async () => { + const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default); + + const { container } = render(CSF3InputFieldFilled()); + + await CSF3InputFieldFilled.play({ canvasElement: container as HTMLElement }); + + const input = screen.getByTestId('input') as HTMLInputElement; + expect(input.value).toEqual('Hello world!'); + }); +}); + +describe('ComposeStories types', () => { + it('Should support typescript operators', () => { + type ComposeStoriesParam = Parameters[0]; + + expectTypeOf({ + ...stories, + default: stories.default as Meta, + }).toMatchTypeOf(); + + expectTypeOf({ + ...stories, + default: stories.default satisfies Meta, + }).toMatchTypeOf(); + }); +}); diff --git a/code/renderers/vue3/src/__tests__/composeStories/internals.test.tsx b/code/renderers/vue3/src/__tests__/composeStories/internals.test.tsx new file mode 100644 index 000000000000..3be07251c9c0 --- /dev/null +++ b/code/renderers/vue3/src/__tests__/composeStories/internals.test.tsx @@ -0,0 +1,122 @@ +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/index.ts b/code/renderers/vue3/src/index.ts index 83d5e6cdd65b..d3c7431a00e1 100644 --- a/code/renderers/vue3/src/index.ts +++ b/code/renderers/vue3/src/index.ts @@ -2,8 +2,9 @@ import './globals'; -export * from './public-types'; export { setup } from './render'; +export * from './public-types'; +export * from './testing-api'; // optimization: stop HMR propagation in webpack try { diff --git a/code/renderers/vue3/src/testing-api.ts b/code/renderers/vue3/src/testing-api.ts new file mode 100644 index 000000000000..4e009b25d672 --- /dev/null +++ b/code/renderers/vue3/src/testing-api.ts @@ -0,0 +1,117 @@ +import { + composeStory as originalComposeStory, + composeStories as originalComposeStories, + setProjectAnnotations as originalSetProjectAnnotations, +} from '@storybook/preview-api'; +import type { + Args, + ProjectAnnotations, + StoryAnnotationsOrFn, + Store_CSFExports, + StoriesWithPartialProps, +} from '@storybook/types'; + +import * as defaultProjectAnnotations from './render'; +import type { Meta } from './public-types'; +import type { VueRenderer } from './types'; + +/** Function that sets the globalConfig of your Storybook. The global config is the preview module of your .storybook folder. + * + * It should be run a single time, so that your global config (e.g. decorators) is applied to your stories when using `composeStories` or `composeStory`. + * + * Example: + *```jsx + * // setup.js (for jest) + * import { setProjectAnnotations } from '@storybook/vue3'; + * import projectAnnotations from './.storybook/preview'; + * + * setProjectAnnotations(projectAnnotations); + *``` + * + * @param projectAnnotations - e.g. (import projectAnnotations from '../.storybook/preview') + */ +export function setProjectAnnotations( + projectAnnotations: ProjectAnnotations | ProjectAnnotations[] +) { + originalSetProjectAnnotations(projectAnnotations); +} + +/** + * Function that will receive a story along with meta (e.g. a default export from a .stories file) + * and optionally projectAnnotations e.g. (import * from '../.storybook/preview) + * and will return a composed component that has all args/parameters/decorators/etc combined and applied to it. + * + * + * It's very useful for reusing a story in scenarios outside of Storybook like unit testing. + * + * Example: + *```jsx + * import { render } from '@testing-library/vue'; + * import { composeStory } from '@storybook/vue3'; + * import Meta, { Primary as PrimaryStory } from './Button.stories'; + * + * const Primary = composeStory(PrimaryStory, Meta); + * + * test('renders primary button with Hello World', () => { + * const { getByText } = render(Primary({label: "Hello world"})); + * expect(getByText(/Hello world/i)).not.toBeNull(); + * }); + *``` + * + * @param story + * @param componentAnnotations - e.g. (import Meta from './Button.stories') + * @param [projectAnnotations] - e.g. (import * as projectAnnotations from '../.storybook/preview') this can be applied automatically if you use `setProjectAnnotations` in your setup files. + * @param [exportsName] - in case your story does not contain a name and you want it to have a name. + */ +export function composeStory( + story: StoryAnnotationsOrFn, + componentAnnotations: Meta, + projectAnnotations?: ProjectAnnotations, + exportsName?: string +) { + return originalComposeStory( + story as StoryAnnotationsOrFn, + componentAnnotations, + projectAnnotations, + defaultProjectAnnotations, + exportsName + ); +} + +/** + * Function that will receive a stories import (e.g. `import * as stories from './Button.stories'`) + * and optionally projectAnnotations (e.g. `import * from '../.storybook/preview`) + * and will return an object containing all the stories passed, but now as a composed component that has all args/parameters/decorators/etc combined and applied to it. + * + * + * It's very useful for reusing stories in scenarios outside of Storybook like unit testing. + * + * Example: + *```jsx + * import { render } from '@testing-library/vue'; + * import { composeStories } from '@storybook/vue3'; + * import * as stories from './Button.stories'; + * + * const { Primary, Secondary } = composeStories(stories); + * + * test('renders primary button with Hello World', () => { + * const { getByText } = render(Primary({label: "Hello world"})); + * expect(getByText(/Hello world/i)).not.toBeNull(); + * }); + *``` + * + * @param csfExports - e.g. (import * as stories from './Button.stories') + * @param [projectAnnotations] - e.g. (import * as projectAnnotations from '../.storybook/preview') this can be applied automatically if you use `setProjectAnnotations` in your setup files. + */ +export function composeStories>( + csfExports: TModule, + projectAnnotations?: ProjectAnnotations +) { + // @ts-expect-error Deep down TRenderer['canvasElement'] resolves to canvasElement: unknown but VueRenderer uses WebRenderer where canvasElement is HTMLElement, so the types clash + const composedStories = originalComposeStories(csfExports, projectAnnotations, composeStory); + + return composedStories as unknown as Omit< + StoriesWithPartialProps, + keyof Store_CSFExports + >; +} diff --git a/code/vitest-setup.ts b/code/vitest-setup.ts index 4c1d82c78407..431af5b3c14d 100644 --- a/code/vitest-setup.ts +++ b/code/vitest-setup.ts @@ -1,5 +1,5 @@ import '@testing-library/jest-dom/vitest'; -import { vi } from 'vitest'; +import { vi, expect } from 'vitest'; import { dedent } from 'ts-dedent'; diff --git a/code/yarn.lock b/code/yarn.lock index 658e3dd2ca3c..7ba452bc433f 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -421,7 +421,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:7.23.2": +"@babel/core@npm:7.23.2, @babel/core@npm:^7.23.2": version: 7.23.2 resolution: "@babel/core@npm:7.23.2" dependencies: @@ -444,7 +444,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.18.9, @babel/core@npm:^7.20.12, @babel/core@npm:^7.23.0, @babel/core@npm:^7.23.2, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.0, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.18.9, @babel/core@npm:^7.20.12, @babel/core@npm:^7.23.0, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5": version: 7.23.7 resolution: "@babel/core@npm:7.23.7" dependencies: @@ -522,26 +522,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.22.15": - version: 7.23.7 - resolution: "@babel/helper-create-class-features-plugin@npm:7.23.7" - dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.22.5" - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-member-expression-to-functions": "npm:^7.23.0" - "@babel/helper-optimise-call-expression": "npm:^7.22.5" - "@babel/helper-replace-supers": "npm:^7.22.20" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.22.5" - "@babel/helper-split-export-declaration": "npm:^7.22.6" - semver: "npm:^6.3.1" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: f594e99f97211bda5530756712751c1c4ce6063bb376f1f38cc540309a086bd0f4b62aff969ddb29e7310e936c2d3745934a2b292c4710be8112e57fbe3f3381 - languageName: node - linkType: hard - -"@babel/helper-create-class-features-plugin@npm:^7.23.5": +"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.22.15, @babel/helper-create-class-features-plugin@npm:^7.23.5": version: 7.23.5 resolution: "@babel/helper-create-class-features-plugin@npm:7.23.5" dependencies: @@ -1181,21 +1162,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.23.2": - version: 7.23.7 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.7" - dependencies: - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/helper-remap-async-to-generator": "npm:^7.22.20" - "@babel/plugin-syntax-async-generators": "npm:^7.8.4" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 63d314edc9fbeaf2700745ca0e19bf9840e87f2d7d1f6c5638e06d2aec3e7418d0d7493ed09087e2fe369cc15e9d96c113fb2cd367cb5e3ff922e3712c27b7d4 - languageName: node - linkType: hard - -"@babel/plugin-transform-async-generator-functions@npm:^7.23.4": +"@babel/plugin-transform-async-generator-functions@npm:^7.23.2, @babel/plugin-transform-async-generator-functions@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.4" dependencies: @@ -1282,25 +1249,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.22.15": - version: 7.23.8 - resolution: "@babel/plugin-transform-classes@npm:7.23.8" - dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.22.5" - "@babel/helper-compilation-targets": "npm:^7.23.6" - "@babel/helper-environment-visitor": "npm:^7.22.20" - "@babel/helper-function-name": "npm:^7.23.0" - "@babel/helper-plugin-utils": "npm:^7.22.5" - "@babel/helper-replace-supers": "npm:^7.22.20" - "@babel/helper-split-export-declaration": "npm:^7.22.6" - globals: "npm:^11.1.0" - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 227ac5166501e04d9e7fbd5eda6869b084ffa4af6830ac12544ac6ea14953ca00eb1762b0df9349c0f6c8d2a799385910f558066cd0fb85b9ca437b1131a6043 - languageName: node - linkType: hard - -"@babel/plugin-transform-classes@npm:^7.23.5": +"@babel/plugin-transform-classes@npm:^7.22.15, @babel/plugin-transform-classes@npm:^7.23.5": version: 7.23.5 resolution: "@babel/plugin-transform-classes@npm:7.23.5" dependencies: @@ -4293,6 +4242,13 @@ __metadata: languageName: node linkType: hard +"@one-ini/wasm@npm:0.1.1": + version: 0.1.1 + resolution: "@one-ini/wasm@npm:0.1.1" + checksum: 54700e055037f1a63bfcc86d24822203b25759598c2c3e295d1435130a449108aebc119c9c2e467744767dbe0b6ab47a182c61aa1071ba7368f5e20ab197ba65 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -6813,6 +6769,7 @@ __metadata: "@storybook/global": "npm:^5.0.0" "@storybook/preview-api": "workspace:*" "@storybook/types": "workspace:*" + "@testing-library/vue": "npm:^8.0.0" "@types/prettier": "npm:^3.0.0" "@vitejs/plugin-vue": "npm:^4.4.0" "@vue/compiler-core": "npm:^3.0.0" @@ -6952,7 +6909,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:^9.0.0, @testing-library/dom@npm:^9.3.1": +"@testing-library/dom@npm:^9.0.0, @testing-library/dom@npm:^9.3.1, @testing-library/dom@npm:^9.3.3": version: 9.3.3 resolution: "@testing-library/dom@npm:9.3.3" dependencies: @@ -7043,6 +7000,20 @@ __metadata: languageName: node linkType: hard +"@testing-library/vue@npm:^8.0.0": + version: 8.0.1 + resolution: "@testing-library/vue@npm:8.0.1" + dependencies: + "@babel/runtime": "npm:^7.23.2" + "@testing-library/dom": "npm:^9.3.3" + "@vue/test-utils": "npm:^2.4.1" + peerDependencies: + "@vue/compiler-sfc": ">= 3" + vue: ">= 3" + checksum: 1a65435fed6b020de1b704eec02cebfacf5bc8052c23bbcb19eea1300f5e60e9d112c4c99bd02aaabbeee2f7da9431dff4f61a38c0e2ced968fa865d2b65b68e + languageName: node + linkType: hard + "@tootallnate/once@npm:2": version: 2.0.0 resolution: "@tootallnate/once@npm:2.0.0" @@ -8923,6 +8894,22 @@ __metadata: languageName: node linkType: hard +"@vue/test-utils@npm:^2.4.1": + version: 2.4.1 + resolution: "@vue/test-utils@npm:2.4.1" + dependencies: + js-beautify: "npm:1.14.9" + vue-component-type-helpers: "npm:1.8.4" + peerDependencies: + "@vue/server-renderer": ^3.0.1 + vue: ^3.0.1 + peerDependenciesMeta: + "@vue/server-renderer": + optional: true + checksum: b3e88b84468c610a62dcea51d5ae089ea299192dd1fc787d69e78c76316b8425cbf1f035a05ab9043cad043c800823df7d76652224bfa713db8d0cf9ec8abb9c + languageName: node + linkType: hard + "@vue/typescript@npm:1.8.15": version: 1.8.15 resolution: "@vue/typescript@npm:1.8.15" @@ -11754,6 +11741,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^10.0.0": + version: 10.0.1 + resolution: "commander@npm:10.0.1" + checksum: 53f33d8927758a911094adadda4b2cbac111a5b377d8706700587650fd8f45b0bbe336de4b5c3fe47fd61f420a3d9bd452b6e0e6e5600a7e74d7bf0174f6efe3 + languageName: node + linkType: hard + "commander@npm:^2.18.0, commander@npm:^2.19.0, commander@npm:^2.2.0, commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -11879,6 +11873,16 @@ __metadata: languageName: node linkType: hard +"config-chain@npm:^1.1.13": + version: 1.1.13 + resolution: "config-chain@npm:1.1.13" + dependencies: + ini: "npm:^1.3.4" + proto-list: "npm:~1.2.1" + checksum: 39d1df18739d7088736cc75695e98d7087aea43646351b028dfabd5508d79cf6ef4c5bcd90471f52cd87ae470d1c5490c0a8c1a292fbe6ee9ff688061ea0963e + languageName: node + linkType: hard + "confusing-browser-globals@npm:^1.0.10": version: 1.0.11 resolution: "confusing-browser-globals@npm:1.0.11" @@ -13215,6 +13219,20 @@ __metadata: languageName: node linkType: hard +"editorconfig@npm:^1.0.3": + version: 1.0.4 + resolution: "editorconfig@npm:1.0.4" + dependencies: + "@one-ini/wasm": "npm:0.1.1" + commander: "npm:^10.0.0" + minimatch: "npm:9.0.1" + semver: "npm:^7.5.3" + bin: + editorconfig: bin/editorconfig + checksum: ed6985959d7b34a56e1c09bef118758c81c969489b768d152c93689fce8403b0452462e934f665febaba3478eebc0fd41c0a36100783eaadf6d926c4abc87a3d + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -16024,7 +16042,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.1": +"glob@npm:^8.0.1, glob@npm:^8.1.0": version: 8.1.0 resolution: "glob@npm:8.1.0" dependencies: @@ -17208,7 +17226,7 @@ __metadata: languageName: node linkType: hard -"ini@npm:^1.3.5, ini@npm:~1.3.0": +"ini@npm:^1.3.4, ini@npm:^1.3.5, ini@npm:~1.3.0": version: 1.3.8 resolution: "ini@npm:1.3.8" checksum: ec93838d2328b619532e4f1ff05df7909760b6f66d9c9e2ded11e5c1897d6f2f9980c54dd638f88654b00919ce31e827040631eab0a3969e4d1abefa0719516a @@ -18300,6 +18318,22 @@ __metadata: languageName: node linkType: hard +"js-beautify@npm:1.14.9": + version: 1.14.9 + resolution: "js-beautify@npm:1.14.9" + dependencies: + config-chain: "npm:^1.1.13" + editorconfig: "npm:^1.0.3" + glob: "npm:^8.1.0" + nopt: "npm:^6.0.0" + bin: + css-beautify: js/bin/css-beautify.js + html-beautify: js/bin/html-beautify.js + js-beautify: js/bin/js-beautify.js + checksum: be7b968a15fef3b3f906b3f043538aebbe5ce0db60b3b44b9532c93d05790078011b8cf66c9b00b28fdd49ba3b15b098babb22fb4ae75fcf2b7814a6ad4ce12f + languageName: node + linkType: hard + "js-stringify@npm:^1.0.2": version: 1.0.2 resolution: "js-stringify@npm:1.0.2" @@ -20885,6 +20919,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:9.0.1": + version: 9.0.1 + resolution: "minimatch@npm:9.0.1" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: aa043eb8822210b39888a5d0d28df0017b365af5add9bd522f180d2a6962de1cbbf1bdeacdb1b17f410dc3336bc8d76fb1d3e814cdc65d00c2f68e01f0010096 + languageName: node + linkType: hard + "minimatch@npm:9.0.3, minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:^9.0.3": version: 9.0.3 resolution: "minimatch@npm:9.0.3" @@ -23481,6 +23524,13 @@ __metadata: languageName: node linkType: hard +"proto-list@npm:~1.2.1": + version: 1.2.4 + resolution: "proto-list@npm:1.2.4" + checksum: b9179f99394ec8a68b8afc817690185f3b03933f7b46ce2e22c1930dc84b60d09f5ad222beab4e59e58c6c039c7f7fcf620397235ef441a356f31f9744010e12 + languageName: node + linkType: hard + "proxy-addr@npm:~2.0.7": version: 2.0.7 resolution: "proxy-addr@npm:2.0.7" @@ -29077,6 +29127,13 @@ __metadata: languageName: node linkType: hard +"vue-component-type-helpers@npm:1.8.4": + version: 1.8.4 + resolution: "vue-component-type-helpers@npm:1.8.4" + checksum: b18ffe06e4834e6df2ff08ec1ddff19eb730b6b68a40727f937eb80fcb20bf523c1f8f0884ac17e5d72f4612f34da9dbd4aba9659a34f89e70c73e7e5f818de9 + languageName: node + linkType: hard + "vue-component-type-helpers@npm:latest": version: 1.8.15 resolution: "vue-component-type-helpers@npm:1.8.15" diff --git a/docs/snippets/vue/component-test-with-testing-library.3.js.mdx b/docs/snippets/vue/component-test-with-testing-library.3.js.mdx index d726c9337a7d..ce1ae6871a11 100644 --- a/docs/snippets/vue/component-test-with-testing-library.3.js.mdx +++ b/docs/snippets/vue/component-test-with-testing-library.3.js.mdx @@ -3,7 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/vue'; -import { composeStory } from '@storybook/testing-vue3'; +import { composeStory } from '@storybook/vue3'; import Meta, { InvalidForm as InvalidFormStory } from './LoginForm.stories'; //👈 Our stories imported here. diff --git a/docs/snippets/vue/component-test-with-testing-library.3.ts.mdx b/docs/snippets/vue/component-test-with-testing-library.3.ts.mdx index e89b5673b0c1..dfab5b1b1345 100644 --- a/docs/snippets/vue/component-test-with-testing-library.3.ts.mdx +++ b/docs/snippets/vue/component-test-with-testing-library.3.ts.mdx @@ -3,7 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/vue'; -import { composeStory } from '@storybook/testing-vue3'; +import { composeStory } from '@storybook/vue3'; import Meta, { InvalidForm as InvalidFormStory } from './LoginForm.stories'; //👈 Our stories imported here. diff --git a/docs/snippets/vue/multiple-stories-test.3.js.mdx b/docs/snippets/vue/multiple-stories-test.3.js.mdx index 3d5e38c3e014..cedc62296bbd 100644 --- a/docs/snippets/vue/multiple-stories-test.3.js.mdx +++ b/docs/snippets/vue/multiple-stories-test.3.js.mdx @@ -3,7 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/vue'; -import { composeStories } from '@storybook/testing-vue3'; +import { composeStories } from '@storybook/vue3'; import * as FormStories from './LoginForm.stories'; diff --git a/docs/snippets/vue/multiple-stories-test.3.ts.mdx b/docs/snippets/vue/multiple-stories-test.3.ts.mdx index 16d0b1603eb9..de66f556a819 100644 --- a/docs/snippets/vue/multiple-stories-test.3.ts.mdx +++ b/docs/snippets/vue/multiple-stories-test.3.ts.mdx @@ -3,7 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/vue'; -import { composeStories } from '@storybook/testing-vue3'; +import { composeStories } from '@storybook/vue3'; import * as FormStories from './LoginForm.stories'; diff --git a/docs/snippets/vue/override-compose-story-test.compose-stories.3.js.mdx b/docs/snippets/vue/override-compose-story-test.compose-stories.3.js.mdx index d6f7801907be..21ff3a8c5e29 100644 --- a/docs/snippets/vue/override-compose-story-test.compose-stories.3.js.mdx +++ b/docs/snippets/vue/override-compose-story-test.compose-stories.3.js.mdx @@ -1,7 +1,7 @@ ```js // tests/Form.test.js -import { composeStory } from '@storybook/testing-vue3'; +import { composeStory } from '@storybook/vue3'; import * as FormStories from './LoginForm.stories'; diff --git a/docs/snippets/vue/override-compose-story-test.compose-stories.3.ts.mdx b/docs/snippets/vue/override-compose-story-test.compose-stories.3.ts.mdx index d8bace872090..545f2756540f 100644 --- a/docs/snippets/vue/override-compose-story-test.compose-stories.3.ts.mdx +++ b/docs/snippets/vue/override-compose-story-test.compose-stories.3.ts.mdx @@ -1,7 +1,7 @@ ```ts // tests/Form.test.ts -import { composeStory } from '@storybook/testing-vue3'; +import { composeStory } from '@storybook/vue3'; import * as FormStories from './LoginForm.stories'; diff --git a/docs/snippets/vue/override-compose-story-test.compose-story.3.js.mdx b/docs/snippets/vue/override-compose-story-test.compose-story.3.js.mdx index b377e1fa713e..9da0bb9572c3 100644 --- a/docs/snippets/vue/override-compose-story-test.compose-story.3.js.mdx +++ b/docs/snippets/vue/override-compose-story-test.compose-story.3.js.mdx @@ -1,7 +1,7 @@ ```js // tests/Form.test.js -import { composeStory } from '@storybook/testing-vue3'; +import { composeStory } from '@storybook/vue3'; import Meta, { ValidForm as ValidFormStory } from './LoginForm.stories'; diff --git a/docs/snippets/vue/override-compose-story-test.compose-story.3.ts.mdx b/docs/snippets/vue/override-compose-story-test.compose-story.3.ts.mdx index 987e05cc7905..f8dbedd15c8e 100644 --- a/docs/snippets/vue/override-compose-story-test.compose-story.3.ts.mdx +++ b/docs/snippets/vue/override-compose-story-test.compose-story.3.ts.mdx @@ -1,7 +1,7 @@ ```ts // tests/Form.test.ts -import { composeStory } from '@storybook/testing-vue3'; +import { composeStory } from '@storybook/vue3'; import Meta, { ValidForm as ValidFormStory } from './LoginForm.stories'; diff --git a/docs/snippets/vue/reuse-args-test.3.js.mdx b/docs/snippets/vue/reuse-args-test.3.js.mdx index 359039264697..fdd957980e29 100644 --- a/docs/snippets/vue/reuse-args-test.3.js.mdx +++ b/docs/snippets/vue/reuse-args-test.3.js.mdx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/vue'; -import { composeStories } from '@storybook/testing-vue3'; +import { composeStories } from '@storybook/vue3'; import * as stories from './Button.stories'; diff --git a/docs/snippets/vue/reuse-args-test.3.ts.mdx b/docs/snippets/vue/reuse-args-test.3.ts.mdx index bfc8a8f69f33..fc750f1c742f 100644 --- a/docs/snippets/vue/reuse-args-test.3.ts.mdx +++ b/docs/snippets/vue/reuse-args-test.3.ts.mdx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/vue'; -import { composeStories } from '@storybook/testing-vue3'; +import { composeStories } from '@storybook/vue3'; import * as stories from './Button.stories'; diff --git a/docs/snippets/vue/single-story-test.3.js.mdx b/docs/snippets/vue/single-story-test.3.js.mdx index 5e75d53fb765..6e05fcc32ae9 100644 --- a/docs/snippets/vue/single-story-test.3.js.mdx +++ b/docs/snippets/vue/single-story-test.3.js.mdx @@ -3,7 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/vue'; -import { composeStory } from '@storybook/testing-vue3'; +import { composeStory } from '@storybook/vue3'; import Meta, { ValidForm as ValidFormStory } from './LoginForm.stories'; diff --git a/docs/snippets/vue/single-story-test.3.ts.mdx b/docs/snippets/vue/single-story-test.3.ts.mdx index a9a03b4108aa..ca9208d9a86e 100644 --- a/docs/snippets/vue/single-story-test.3.ts.mdx +++ b/docs/snippets/vue/single-story-test.3.ts.mdx @@ -3,7 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/vue'; -import { composeStory } from '@storybook/testing-vue3'; +import { composeStory } from '@storybook/vue3'; import Meta, { ValidForm as ValidFormStory } from './LoginForm.stories'; diff --git a/docs/snippets/vue/storybook-testing-addon-optional-config.js.mdx b/docs/snippets/vue/storybook-testing-addon-optional-config.js.mdx index c2f2b12d1a5f..d50a45ba4c7a 100644 --- a/docs/snippets/vue/storybook-testing-addon-optional-config.js.mdx +++ b/docs/snippets/vue/storybook-testing-addon-optional-config.js.mdx @@ -4,7 +4,7 @@ // Storybook's preview file location import * as globalStorybookConfig from './.storybook/preview'; -import { setProjectAnnotations } from '@storybook/testing-vue3'; +import { setProjectAnnotations } from '@storybook/vue3'; setProjectAnnotations(globalStorybookConfig); ``` diff --git a/docs/snippets/vue/storybook-testing-library-install.npm.js.mdx b/docs/snippets/vue/storybook-testing-library-install.npm.js.mdx deleted file mode 100644 index c71c6a486789..000000000000 --- a/docs/snippets/vue/storybook-testing-library-install.npm.js.mdx +++ /dev/null @@ -1,3 +0,0 @@ -```sh -npm install --save-dev @storybook/testing-vue3 -``` diff --git a/docs/snippets/vue/storybook-testing-library-install.pnpm.js.mdx b/docs/snippets/vue/storybook-testing-library-install.pnpm.js.mdx deleted file mode 100644 index 8d079ae0b22f..000000000000 --- a/docs/snippets/vue/storybook-testing-library-install.pnpm.js.mdx +++ /dev/null @@ -1,3 +0,0 @@ -```shell -pnpm add --save-dev @storybook/testing-vue3 -``` diff --git a/docs/snippets/vue/storybook-testing-library-install.yarn.js.mdx b/docs/snippets/vue/storybook-testing-library-install.yarn.js.mdx deleted file mode 100644 index ee260ff86a91..000000000000 --- a/docs/snippets/vue/storybook-testing-library-install.yarn.js.mdx +++ /dev/null @@ -1,3 +0,0 @@ -```sh -yarn add --dev @storybook/testing-vue3 -``` diff --git a/docs/writing-tests/stories-in-unit-tests.md b/docs/writing-tests/stories-in-unit-tests.md index e21c90708528..146bae494833 100644 --- a/docs/writing-tests/stories-in-unit-tests.md +++ b/docs/writing-tests/stories-in-unit-tests.md @@ -2,40 +2,12 @@ title: 'Unit tests' --- -Teams test a variety of UI characteristics using different tools. Each tool requires you to replicate the same component state over and over. That’s a maintenance headache. Ideally, you’d set up your tests in the same way and reuse that across tools. +Teams test a variety of UI characteristics using different tools. Each tool requires you to replicate the same component state over and over. That’s a maintenance headache. Ideally, you’d set up your tests similarly and reuse that across tools. -Storybook enables you to isolate a component and capture its use cases in a `*.stories.js|ts` file. Stories are standard JavaScript modules cross-compatible with the whole JavaScript ecosystem. +Storybook enables you to isolate a component and capture its use cases in a `*.stories.js|ts` file. Stories are standard JavaScript modules that are cross-compatible with the whole JavaScript ecosystem. Stories are a practical starting point for UI testing. Import stories into tools like [Jest](https://jestjs.io/), [Testing Library](https://testing-library.com/), [Vitest](https://vitest.dev/) and [Playwright](https://playwright.dev/), to save time and maintenance work. - - -## Set up the testing addon - -Storybook's [`@storybook/testing-vue3`](https://storybook.js.org/addons/@storybook/testing-vue3/) addon is a powerful tool that simplifies the testing process by allowing you to reuse your stories inside alongside their associated mocks, dependencies, and context, saving time and ensuring consistency and accuracy in the testing process. - -Run the following command to install the addon. - - - - - - - - - -If you're using Storybook 7 or higher, the `@storybook/testing-vue3` addon is the only one we support. For Vue 2 users, refer to the [troubleshooting section](#troubleshooting) for additional guidance. - - - - - ## Write a test with Testing Library [Testing Library](https://testing-library.com/) is a suite of helper libraries for browser-based interaction tests. With [Component Story Format](../api/csf.md), your stories are reusable with Testing Library. Each named export (story) is renderable within your testing setup. For example, if you were working on a login component and wanted to test the invalid credentials scenario, here's how you could write your test: @@ -94,7 +66,7 @@ Update your test script to include the configuration file: -Update your test configuration file (e.g., `vite.config.js|ts`) if you're using [Vitest](https://vitest.dev/), or your test script if you're using [Jest](https://jestjs.io/): +Update your test configuration file (e.g., `vite.config.js|ts`) if you're using [Vitest](https://vitest.dev/) or your test script if you're using [Jest](https://jestjs.io/):