From 29ff689e8e3f07e967aacfa9156cc3873d729e7d Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 14 Feb 2024 12:08:36 +0100 Subject: [PATCH] Portable stories: Add tests for React and Vue renderers --- .../src/modules/store/csf/portable-stories.ts | 2 +- .../react/src/__test__/Button.stories.tsx | 33 +++++++++- .../portable-stories.test.tsx.snap | 19 ++++++ .../src/__test__/portable-stories.test.tsx | 21 +++++-- .../composeStories/Button.stories.ts | 36 ++++++++++- .../portable-stories.test.ts.snap | 19 ++++++ .../composeStories/portable-stories.test.ts | 61 +++++++++++-------- .../src/stories/Button.stories.tsx | 29 ++++++++- 8 files changed, 186 insertions(+), 34 deletions(-) diff --git a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts index bd2dd421ccfe..de45ab6af59b 100644 --- a/code/lib/preview-api/src/modules/store/csf/portable-stories.ts +++ b/code/lib/preview-api/src/modules/store/csf/portable-stories.ts @@ -89,7 +89,7 @@ export function composeStory> = Object.assign( function storyFn(extraArgs?: Partial) { context.args = { - ...context.args, + ...context.initialArgs, ...extraArgs, }; diff --git a/code/renderers/react/src/__test__/Button.stories.tsx b/code/renderers/react/src/__test__/Button.stories.tsx index 6882b957b136..412f55048c2f 100644 --- a/code/renderers/react/src/__test__/Button.stories.tsx +++ b/code/renderers/react/src/__test__/Button.stories.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { within, userEvent } from '@storybook/testing-library'; +import { within, userEvent, fn, expect } from '@storybook/test'; import type { StoryFn as CSF2Story, StoryObj as CSF3Story, Meta } from '..'; import type { ButtonProps } from './Button'; @@ -84,7 +84,36 @@ export const CSF3InputFieldFilled: CSF3Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Step label', async () => { - await userEvent.type(canvas.getByTestId('input'), 'Hello world!'); + const inputEl = canvas.getByTestId('input'); + await userEvent.type(inputEl, 'Hello world!'); + await expect(inputEl).toHaveValue('Hello world!'); }); }, }; + +const spyFn = fn(); +export const LoaderStory: CSF3Story<{ spyFn: (val: string) => string }> = { + args: { + spyFn, + }, + render: (args, { loaded }) => { + const data = args.spyFn('foo'); + return ( +
+
{loaded.value}
+
{String(data)}
+
+ ); + }, + loaders: [ + async () => { + spyFn.mockReturnValueOnce('baz'); + return { + value: 'bar', + }; + }, + ], + play: async () => { + expect(spyFn).toHaveBeenCalledWith('foo'); + }, +}; diff --git a/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap b/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap index 2b92b1d68424..018406c3581d 100644 --- a/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap +++ b/code/renderers/react/src/__test__/__snapshots__/portable-stories.test.tsx.snap @@ -94,3 +94,22 @@ exports[`Renders CSF3Primary story 1`] = ` `; + +exports[`Renders LoaderStory story 1`] = ` + +
+
+
+ bar +
+
+ baz +
+
+
+ +`; diff --git a/code/renderers/react/src/__test__/portable-stories.test.tsx b/code/renderers/react/src/__test__/portable-stories.test.tsx index afa0b70142e4..bcac8173dc78 100644 --- a/code/renderers/react/src/__test__/portable-stories.test.tsx +++ b/code/renderers/react/src/__test__/portable-stories.test.tsx @@ -10,7 +10,7 @@ import type { Button } from './Button'; import * as stories from './Button.stories'; // example with composeStories, returns an object with all stories composed with args/decorators -const { CSF3Primary } = composeStories(stories); +const { CSF3Primary, LoaderStory } = composeStories(stories); // example with composeStory, returns a single story composed with args/decorators const Secondary = composeStory(stories.CSF2Secondary, stories.default); @@ -44,6 +44,15 @@ describe('renders', () => { const buttonElement = getByText(/foo/i); expect(buttonElement).not.toBeNull(); }); + + it('should call and compose loaders data', async () => { + await LoaderStory.load(); + const { getByTestId, container } = render(); + expect(getByTestId('spy-data').textContent).toEqual('baz'); + expect(getByTestId('loaded-data').textContent).toEqual('bar'); + // spy assertions happen in the play function and should work + await LoaderStory.play!({ canvasElement: container as HTMLElement }); + }); }); describe('projectAnnotations', () => { @@ -139,9 +148,13 @@ describe('ComposeStories types', () => { }); // Batch snapshot testing -const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName, Story]); +const testCases = Object.values(composeStories(stories)).map( + (Story) => [Story.storyName, Story] as [string, typeof Story] +); it.each(testCases)('Renders %s story', async (_storyName, Story) => { cleanup(); - const tree = await render(); - expect(tree.baseElement).toMatchSnapshot(); + await Story.load(); + const { container, baseElement } = await render(); + await Story.play?.({ canvasElement: container }); + expect(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 239416df5c35..5865565de219 100644 --- a/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts +++ b/code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts @@ -1,4 +1,4 @@ -import { userEvent, within } from '@storybook/testing-library'; +import { userEvent, within, expect, fn } from '@storybook/test'; import type { Meta, StoryFn as CSF2Story, StoryObj } from '../..'; import Button from './Button.vue'; @@ -114,7 +114,39 @@ export const CSF3InputFieldFilled: CSF3Story = { play: async ({ canvasElement, step }) => { const canvas = within(canvasElement); await step('Step label', async () => { - await userEvent.type(canvas.getByTestId('input'), 'Hello world!'); + const inputEl = canvas.getByTestId('input'); + await userEvent.type(inputEl, 'Hello world!'); + await expect(inputEl).toHaveValue('Hello world!'); }); }, }; + +const spyFn = fn(); +export const LoaderStory: StoryObj<{ spyFn: (val: string) => string }> = { + args: { + spyFn, + }, + render: (args, { loaded }) => ({ + components: { Button }, + setup() { + return { args, data: args.spyFn('foo'), loaded: loaded.value }; + }, + template: ` +
+
{{loaded}}
+
{{data}}
+
+ `, + }), + loaders: [ + async () => { + spyFn.mockReturnValueOnce('baz'); + return { + value: 'bar', + }; + }, + ], + play: async () => { + expect(spyFn).toHaveBeenCalledWith('foo'); + }, +}; diff --git a/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap b/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap index 75eab08758cf..5ef40a0d82f3 100644 --- a/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap +++ b/code/renderers/vue3/src/__tests__/composeStories/__snapshots__/portable-stories.test.ts.snap @@ -86,3 +86,22 @@ exports[`Renders CSF3Primary story 1`] = ` `; + +exports[`Renders LoaderStory story 1`] = ` + +
+
+
+ bar +
+
+ baz +
+
+
+ +`; diff --git a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts index 4c541e1c4536..9c698a4d8824 100644 --- a/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts +++ b/code/renderers/vue3/src/__tests__/composeStories/portable-stories.test.ts @@ -10,35 +10,46 @@ import type Button from './Button.vue'; import { composeStories, composeStory, setProjectAnnotations } from '../../portable-stories'; // example with composeStories, returns an object with all stories composed with args/decorators -const { CSF3Primary } = composeStories(stories); +const { CSF3Primary, LoaderStory } = 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(); -}); +describe('renders', () => { + 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('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('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(); + }); -it('reuses args from composeStories', () => { - const { getByText } = render(CSF3Primary()); - const buttonElement = getByText(/foo/i); - expect(buttonElement).toBeInTheDocument(); + it('should call and compose loaders data', async () => { + await LoaderStory.load(); + const { getByTestId, container } = render(LoaderStory()); + expect(getByTestId('spy-data').textContent).toEqual('baz'); + expect(getByTestId('loaded-data').textContent).toEqual('bar'); + // spy assertions happen in the play function and should work + await LoaderStory.play!({ canvasElement: container as HTMLElement }); + }); }); describe('projectAnnotations', () => { @@ -131,8 +142,10 @@ it.each(testCases)('Renders %s story', async (_storyName, Story) => { return; } + await Story.load(); + const { container, baseElement } = await render(Story()); + await Story.play?.({ canvasElement: container as HTMLElement }); await new Promise((resolve) => setTimeout(resolve, 0)); - const tree = await render(Story()); - expect(tree.baseElement).toMatchSnapshot(); + expect(baseElement).toMatchSnapshot(); }); diff --git a/test-storybooks/portable-stories-react/src/stories/Button.stories.tsx b/test-storybooks/portable-stories-react/src/stories/Button.stories.tsx index 83d21d2702f9..fb3793b815c1 100644 --- a/test-storybooks/portable-stories-react/src/stories/Button.stories.tsx +++ b/test-storybooks/portable-stories-react/src/stories/Button.stories.tsx @@ -1,4 +1,4 @@ -import { expect, within, userEvent } from '@storybook/test'; +import { expect, within, userEvent, fn } from '@storybook/test'; import type { StoryFn as CSF2Story, StoryObj as CSF3Story, Meta } from '@storybook/react'; import type { ButtonProps } from './Button'; @@ -99,3 +99,30 @@ export const CSF3InputFieldFilled: CSF3Story = { console.log('end of play function') }, }; + +const spyFn = fn(); +export const LoaderStory: CSF3Story<{ spyFn: (val: string) => string }> = { + args: { + spyFn, + }, + render: (args, { loaded }) => { + const data = args.spyFn('foo'); + return ( +
+
{loaded.value}
+
{String(data)}
+
+ ); + }, + loaders: [ + async () => { + spyFn.mockReturnValueOnce('mocked'); + return { + value: 'bar', + }; + }, + ], + play: async () => { + expect(spyFn).toHaveBeenCalledWith('foo'); + }, +}; \ No newline at end of file