diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts new file mode 100644 index 000000000000..0531a5acdc18 --- /dev/null +++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, vi, it } from 'vitest'; +import { composeStory, composeStories } from './portable-stories'; + +// Most integration tests for this functionality are located under renderers/react +describe('composeStory', () => { + const meta = { + title: 'Button', + parameters: { + firstAddon: true, + }, + args: { + label: 'Hello World', + primary: true, + }, + }; + + it('should return story with composed args and parameters', () => { + const Story = () => {}; + Story.args = { primary: true }; + Story.parameters = { + parameters: { + secondAddon: true, + }, + }; + + const composedStory = composeStory(Story, meta); + expect(composedStory.args).toEqual({ ...Story.args, ...meta.args }); + expect(composedStory.parameters).toEqual( + expect.objectContaining({ ...Story.parameters, ...meta.parameters }) + ); + }); + + it('should throw an error if Story is undefined', () => { + expect(() => { + // @ts-expect-error (invalid input) + composeStory(undefined, meta); + }).toThrow(); + }); + + describe('Id of the story', () => { + it('is exposed correctly when composeStories is used', () => { + const module = { + default: { + title: 'Example/Button', + }, + CSF3Primary: () => {}, + }; + const Primary = composeStory(module.CSF3Primary, module.default, {}); + expect(Primary.id).toBe('example-button--csf-3-primary'); + }); + it('is exposed correctly when composeStory is used and exportsName is passed', () => { + const module = { + default: { + title: 'Example/Button', + }, + CSF3Primary: () => {}, + }; + const Primary = composeStory(module.CSF3Primary, module.default, {}, {}, 'overwritten'); + expect(Primary.id).toBe('example-button--overwritten'); + }); + it("is not unique when composeStory is used and exportsName isn't passed", () => { + const Primary = composeStory({ render: () => {} }, {}); + expect(Primary.id).toContain('unknown'); + }); + }); +}); + +describe('composeStories', () => { + const composeStoryFn = vi.fn((v) => v); + const defaultAnnotations = { render: () => '' }; + it('should call composeStoryFn with stories', () => { + const composeStorySpy = vi.fn((v) => v); + const module = { + default: { + title: 'Button', + }, + StoryOne: () => {}, + StoryTwo: () => {}, + }; + const globalConfig = {}; + composeStories(module, globalConfig, composeStorySpy); + expect(composeStorySpy).toHaveBeenCalledWith( + module.StoryOne, + module.default, + globalConfig, + 'StoryOne' + ); + expect(composeStorySpy).toHaveBeenCalledWith( + module.StoryTwo, + module.default, + globalConfig, + 'StoryTwo' + ); + }); + + it('should not call composeStoryFn for non-story exports', () => { + const composeStorySpy = vi.fn((v) => v); + const module = { + default: { + title: 'Button', + excludeStories: /Data/, + }, + mockData: {}, + }; + composeStories(module, defaultAnnotations, composeStoryFn); + expect(composeStorySpy).not.toHaveBeenCalled(); + }); + + describe('non-story exports', () => { + it('should filter non-story exports with excludeStories', () => { + const StoryModuleWithNonStoryExports = { + default: { + title: 'Some/Component', + excludeStories: /.*Data/, + }, + LegitimateStory: () => 'hello world', + mockData: {}, + }; + + const result = composeStories( + StoryModuleWithNonStoryExports, + defaultAnnotations, + composeStoryFn + ); + expect(Object.keys(result)).not.toContain('mockData'); + }); + + it('should filter non-story exports with includeStories', () => { + const StoryModuleWithNonStoryExports = { + default: { + title: 'Some/Component', + includeStories: /.*Story/, + }, + LegitimateStory: () => 'hello world', + mockData: {}, + }; + + const result = composeStories( + StoryModuleWithNonStoryExports, + defaultAnnotations, + composeStoryFn + ); + expect(Object.keys(result)).not.toContain('mockData'); + }); + }); +}); diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories/index.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts similarity index 90% rename from code/lib/preview-api/src/modules/store/csf/portable-stories/index.ts rename to code/lib/preview-api/src/modules/store/csf/portable-stories.ts index b15a3345ed0c..b9d7abc31213 100644 --- a/code/lib/preview-api/src/modules/store/csf/portable-stories/index.ts +++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts @@ -15,12 +15,12 @@ import type { } from '@storybook/types'; import { HooksContext } from '../../../addons'; -import { composeConfigs } from '../composeConfigs'; -import { prepareContext, prepareStory } from '../prepareStory'; -import { normalizeStory } from '../normalizeStory'; -import { normalizeComponentAnnotations } from '../normalizeComponentAnnotations'; -import { getValuesFromArgTypes } from '../getValuesFromArgTypes'; -import { normalizeProjectAnnotations } from '../normalizeProjectAnnotations'; +import { composeConfigs } from './composeConfigs'; +import { prepareContext, prepareStory } from './prepareStory'; +import { normalizeStory } from './normalizeStory'; +import { normalizeComponentAnnotations } from './normalizeComponentAnnotations'; +import { getValuesFromArgTypes } from './getValuesFromArgTypes'; +import { normalizeProjectAnnotations } from './normalizeProjectAnnotations'; let GLOBAL_STORYBOOK_PROJECT_ANNOTATIONS = composeConfigs([]); diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories/index.test.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories/index.test.ts deleted file mode 100644 index f0c811aa5abc..000000000000 --- a/code/lib/preview-api/src/modules/store/csf/portable-stories/index.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { describe, expect, vi, it } from 'vitest'; -import { composeStory, composeStories } from './index'; - -// Most integration tests for this functionality are located under renderers/react -describe('composeStory', () => { - const meta = { - title: 'Button', - parameters: { - firstAddon: true, - }, - args: { - label: 'Hello World', - primary: true, - }, - }; - - it('should return story with composed args and parameters', () => { - const Story = () => {}; - Story.args = { primary: true }; - Story.parameters = { - parameters: { - secondAddon: true, - }, - }; - - const composedStory = composeStory(Story, meta); - expect(composedStory.args).toEqual({ ...Story.args, ...meta.args }); - expect(composedStory.parameters).toEqual( - expect.objectContaining({ ...Story.parameters, ...meta.parameters }) - ); - }); - - it('should throw an error if Story is undefined', () => { - expect(() => { - // @ts-expect-error (invalid input) - composeStory(undefined, meta); - }).toThrow(); - }); -}); - -describe('composeStories', () => { - it('should call composeStoryFn with stories', () => { - const composeConfigFn = vi.fn((v) => v); - const module = { - default: { - title: 'Button', - }, - StoryOne: () => {}, - StoryTwo: () => {}, - }; - const globalConfig = {}; - composeStories(module, globalConfig, composeConfigFn); - expect(composeConfigFn).toHaveBeenCalledWith( - module.StoryOne, - module.default, - globalConfig, - 'StoryOne' - ); - expect(composeConfigFn).toHaveBeenCalledWith( - module.StoryTwo, - module.default, - globalConfig, - 'StoryTwo' - ); - }); - - it('should not call composeStoryFn for non-story exports', () => { - const composeConfigFn = vi.fn((v) => v); - const module = { - default: { - title: 'Button', - excludeStories: /Data/, - }, - mockData: {}, - }; - composeStories(module, {}, composeConfigFn); - expect(composeConfigFn).not.toHaveBeenCalled(); - }); -}); diff --git a/code/renderers/react/src/__test__/__snapshots__/internals.test.tsx.snap b/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap similarity index 100% rename from code/renderers/react/src/__test__/__snapshots__/internals.test.tsx.snap rename to code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap diff --git a/code/renderers/react/src/__test__/internals.test.tsx b/code/renderers/react/src/__test__/internals.test.tsx deleted file mode 100644 index 8bed037619be..000000000000 --- a/code/renderers/react/src/__test__/internals.test.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import React from 'react'; -import { addons } from '@storybook/preview-api'; -import { cleanup, render, screen } from '@testing-library/react'; - -import { composeStories, composeStory } from '..'; - -import * as stories from './Button.stories'; - -const { CSF2StoryWithParamsAndDecorator } = composeStories(stories); - -it('returns composed args including default values from argtypes', () => { - expect(CSF2StoryWithParamsAndDecorator.args).toEqual({ - ...stories.CSF2StoryWithParamsAndDecorator.args, - }); -}); - -it('returns composed parameters from story', () => { - expect(CSF2StoryWithParamsAndDecorator.parameters).toEqual( - expect.objectContaining({ - ...stories.CSF2StoryWithParamsAndDecorator.parameters, - }) - ); -}); - -describe('Id of the story', () => { - it('is exposed correctly when composeStories is used', () => { - expect(CSF2StoryWithParamsAndDecorator.id).toBe( - 'example-button--csf-2-story-with-params-and-decorator' - ); - }); - it('is exposed correctly when composeStory is used and exportsName is passed', () => { - const exportName = Object.entries(stories).filter( - ([_, story]) => story === stories.CSF3Primary - )[0][0]; - const Primary = composeStory(stories.CSF3Primary, stories.default, {}, exportName); - expect(Primary.id).toBe('example-button--csf-3-primary'); - }); - it("is not unique when composeStory is used and exportsName isn't passed", () => { - const Primary = composeStory(stories.CSF3Primary, stories.default); - expect(Primary.id).toContain('unknown'); - }); -}); - -// common in addons that need to communicate between manager and preview -it('should pass with decorators that need addons channel', () => { - const PrimaryWithChannels = composeStory(stories.CSF3Primary, stories.default, { - decorators: [ - (StoryFn: any) => { - addons.getChannel(); - return ; - }, - ], - }); - render(Hello world); - const buttonElement = screen.getByText(/Hello world/i); - expect(buttonElement).not.toBeNull(); - cleanup(); -}); - -describe('Unsupported formats', () => { - it('should throw error if story is undefined', () => { - const UnsupportedStory = () =>
hello world
; - UnsupportedStory.story = { parameters: {} }; - - const UnsupportedStoryModule: any = { - default: {}, - UnsupportedStory: undefined, - }; - - expect(() => { - composeStories(UnsupportedStoryModule); - }).toThrow(); - }); -}); - -describe('non-story exports', () => { - it('should filter non-story exports with excludeStories', () => { - const StoryModuleWithNonStoryExports = { - default: { - title: 'Some/Component', - excludeStories: /.*Data/, - }, - LegitimateStory: () =>
hello world
, - mockData: {}, - }; - - const result = composeStories(StoryModuleWithNonStoryExports); - expect(Object.keys(result)).not.toContain('mockData'); - }); - - it('should filter non-story exports with includeStories', () => { - const StoryModuleWithNonStoryExports = { - default: { - title: 'Some/Component', - includeStories: /.*Story/, - }, - LegitimateStory: () =>
hello world
, - mockData: {}, - }; - - const result = composeStories(StoryModuleWithNonStoryExports); - expect(Object.keys(result)).not.toContain('mockData'); - }); -}); - -// Batch snapshot testing -const testCases = Object.values(composeStories(stories)).map((Story) => [ - // The ! is necessary in Typescript only, as the property is part of a partial type - Story.storyName!, - Story, -]); -it.each(testCases)('Renders %s story', async (_storyName, Story) => { - cleanup(); - const tree = await render(); - expect(tree.baseElement).toMatchSnapshot(); -}); diff --git a/code/renderers/react/src/__test__/composeStories.test.tsx b/code/renderers/react/src/__test__/portable-stories.test.tsx similarity index 83% rename from code/renderers/react/src/__test__/composeStories.test.tsx rename to code/renderers/react/src/__test__/portable-stories.test.tsx index a8cc360e751b..221404d5ee42 100644 --- a/code/renderers/react/src/__test__/composeStories.test.tsx +++ b/code/renderers/react/src/__test__/portable-stories.test.tsx @@ -1,11 +1,11 @@ import { vi, it, expect, afterEach, describe } from 'vitest'; import React from 'react'; import { render, screen, cleanup } from '@testing-library/react'; - +import { addons } from '@storybook/preview-api'; import type { Meta } from '@storybook/react'; import { expectTypeOf } from 'expect-type'; -import { setProjectAnnotations, composeStories, composeStory } from '..'; +import { setProjectAnnotations, composeStories, composeStory } from '..'; import type { Button } from './Button'; import * as stories from './Button.stories'; @@ -106,6 +106,21 @@ describe('CSF3', () => { }); }); +// common in addons that need to communicate between manager and preview +it('should pass with decorators that need addons channel', () => { + const PrimaryWithChannels = composeStory(stories.CSF3Primary, stories.default, { + decorators: [ + (StoryFn: any) => { + addons.getChannel(); + return StoryFn(); + }, + ], + }); + render(Hello world); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).not.toBeNull(); +}); + describe('ComposeStories types', () => { // this file tests Typescript types that's why there are no assertions it('Should support typescript operators', () => { @@ -122,3 +137,11 @@ describe('ComposeStories types', () => { }).toMatchTypeOf(); }); }); + +// Batch snapshot testing +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName, Story]); +it.each(testCases)('Renders %s story', async (_storyName, Story) => { + cleanup(); + const tree = await render(); + expect(tree.baseElement).toMatchSnapshot(); +}); diff --git a/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts b/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts index 1b14978a9976..239416df5c35 100644 --- a/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts +++ b/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts @@ -53,7 +53,6 @@ const getCaptionForLocale = (locale: string) => { export const CSF2StoryWithLocale: CSF2Story = (args, { globals }) => ({ components: { Button }, setup() { - console.log({ globals }); const label = getCaptionForLocale(globals.locale); return { args: { ...args, label } }; }, diff --git a/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/internals.test.tsx.snap b/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap similarity index 100% rename from code/renderers/vue3/src/__tests__/composeStories/__snapshots__/internals.test.tsx.snap rename to code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap diff --git a/code/renderers/vue3/src/__tests__/composeStories/internals.test.tsx b/code/renderers/vue3/src/__tests__/composeStories/internals.test.tsx deleted file mode 100644 index db3765c6bf08..000000000000 --- a/code/renderers/vue3/src/__tests__/composeStories/internals.test.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React from 'react'; -import { addons } from '@storybook/preview-api'; -import { render, screen } from '@testing-library/vue'; -import { describe, it, expect } from 'vitest'; - -import { composeStories, composeStory } from '../../portable-stories'; - -import * as stories from './Button.stories'; - -const { CSF2StoryWithParamsAndDecorator } = composeStories(stories); - -it('returns composed args including default values from argtypes', () => { - expect({ - ...stories.default.args, - ...CSF2StoryWithParamsAndDecorator.args, - }).toEqual(expect.objectContaining(CSF2StoryWithParamsAndDecorator.args)); -}); - -it('returns composed parameters from story', () => { - expect(CSF2StoryWithParamsAndDecorator.parameters).toEqual( - expect.objectContaining({ - ...stories.CSF2StoryWithParamsAndDecorator.parameters, - }) - ); -}); - -describe('Id of the story', () => { - it('is exposed correctly when composeStories is used', () => { - expect(CSF2StoryWithParamsAndDecorator.id).toBe( - 'example-button--csf-2-story-with-params-and-decorator' - ); - }); - it('is exposed correctly when composeStory is used and exportsName is passed', () => { - const exportName = Object.entries(stories).filter( - ([_, story]) => story === stories.CSF3Primary - )[0][0]; - const Primary = composeStory(stories.CSF3Primary, stories.default, {}, exportName); - expect(Primary.id).toBe('example-button--csf-3-primary'); - }); - it("is not unique when composeStory is used and exportsName isn't passed", () => { - const Primary = composeStory(stories.CSF3Primary, stories.default); - expect(Primary.id).toContain('unknown'); - }); -}); - -// common in addons that need to communicate between manager and preview -it('should pass with decorators that need addons channel', () => { - const PrimaryWithChannels = composeStory(stories.CSF3Primary, stories.default, { - decorators: [ - (StoryFn: any) => { - addons.getChannel(); - return StoryFn(); - }, - ], - }); - render(PrimaryWithChannels({ label: 'Hello world' })); - const buttonElement = screen.getByText(/Hello world/i); - expect(buttonElement).not.toBeNull(); -}); - -describe('Unsupported formats', () => { - it('should throw error if story is undefined', () => { - const UnsupportedStory = () =>
hello world
; - UnsupportedStory.story = { parameters: {} }; - - const UnsupportedStoryModule: any = { - default: {}, - UnsupportedStory: undefined, - }; - - expect(() => { - composeStories(UnsupportedStoryModule); - }).toThrow(); - }); -}); - -describe('non-story exports', () => { - it('should filter non-story exports with excludeStories', () => { - const StoryModuleWithNonStoryExports = { - default: { - title: 'Some/Component', - excludeStories: /.*Data/, - }, - LegitimateStory: () =>
hello world
, - mockData: {}, - }; - - const result = composeStories(StoryModuleWithNonStoryExports); - expect(Object.keys(result)).not.toContain('mockData'); - }); - - it('should filter non-story exports with includeStories', () => { - const StoryModuleWithNonStoryExports = { - default: { - title: 'Some/Component', - includeStories: /.*Story/, - }, - LegitimateStory: () =>
hello world
, - mockData: {}, - }; - - const result = composeStories(StoryModuleWithNonStoryExports); - expect(Object.keys(result)).not.toContain('mockData'); - }); -}); - -// Batch snapshot testing -const testCases = Object.values(composeStories(stories)).map((Story) => [ - // The ! is necessary in Typescript only, as the property is part of a partial type - Story.storyName!, - Story, -]); -it.each(testCases)('Renders %s story', async (_storyName, Story) => { - if (typeof Story === 'string' || _storyName === 'CSF2StoryWithParamsAndDecorator') { - return; - } - - await new Promise((resolve) => setTimeout(resolve, 0)); - - const tree = await render(Story()); - expect(tree.baseElement).toMatchSnapshot(); -}); diff --git a/code/renderers/vue3/src/__tests__/composeStories/composeStories.test.ts b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts similarity index 80% rename from code/renderers/vue3/src/__tests__/composeStories/composeStories.test.ts rename to code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts index 73d415ee029c..a56866fd6227 100644 --- a/code/renderers/vue3/src/__tests__/composeStories/composeStories.test.ts +++ b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts @@ -1,8 +1,10 @@ /// ; import { it, expect, vi, describe } from 'vitest'; import { render, screen } from '@testing-library/vue'; +import { addons } from '@storybook/preview-api'; import { expectTypeOf } from 'expect-type'; import type { Meta } from '@storybook/vue3'; + import * as stories from './Button.stories'; import type Button from './Button.vue'; import { composeStories, composeStory, setProjectAnnotations } from '../../portable-stories'; @@ -91,6 +93,21 @@ describe('CSF3', () => { }); }); +// common in addons that need to communicate between manager and preview +it('should pass with decorators that need addons channel', () => { + const PrimaryWithChannels = composeStory(stories.CSF3Primary, stories.default, { + decorators: [ + (StoryFn: any) => { + addons.getChannel(); + return StoryFn(); + }, + ], + }); + render(PrimaryWithChannels({ label: 'Hello world' })); + const buttonElement = screen.getByText(/Hello world/i); + expect(buttonElement).not.toBeNull(); +}); + describe('ComposeStories types', () => { it('Should support typescript operators', () => { type ComposeStoriesParam = Parameters[0]; @@ -106,3 +123,16 @@ describe('ComposeStories types', () => { }).toMatchTypeOf(); }); }); + +// Batch snapshot testing +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName, Story]); +it.each(testCases)('Renders %s story', async (_storyName, Story) => { + if (typeof Story === 'string' || _storyName === 'CSF2StoryWithParamsAndDecorator') { + return; + } + + await new Promise((resolve) => setTimeout(resolve, 0)); + + const tree = await render(Story()); + expect(tree.baseElement).toMatchSnapshot(); +});