Skip to content

Commit

Permalink
Portable stories: Add tests for React and Vue renderers
Browse files Browse the repository at this point in the history
  • Loading branch information
yannbf committed Feb 14, 2024
1 parent e1feeca commit 29ff689
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function composeStory<TRenderer extends Renderer = Renderer, TArgs extend
const composedStory: ComposedStoryFn<TRenderer, Partial<TArgs>> = Object.assign(
function storyFn(extraArgs?: Partial<TArgs>) {
context.args = {
...context.args,
...context.initialArgs,
...extraArgs,
};

Expand Down
33 changes: 31 additions & 2 deletions code/renderers/react/src/__test__/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 (
<div>
<div data-testid="loaded-data">{loaded.value}</div>
<div data-testid="spy-data">{String(data)}</div>
</div>
);
},
loaders: [
async () => {
spyFn.mockReturnValueOnce('baz');
return {
value: 'bar',
};
},
],
play: async () => {
expect(spyFn).toHaveBeenCalledWith('foo');
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,22 @@ exports[`Renders CSF3Primary story 1`] = `
</div>
</body>
`;

exports[`Renders LoaderStory story 1`] = `
<body>
<div>
<div>
<div
data-testid="loaded-data"
>
bar
</div>
<div
data-testid="spy-data"
>
baz
</div>
</div>
</div>
</body>
`;
21 changes: 17 additions & 4 deletions code/renderers/react/src/__test__/portable-stories.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(<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', () => {
Expand Down Expand Up @@ -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(<Story />);
expect(tree.baseElement).toMatchSnapshot();
await Story.load();
const { container, baseElement } = await render(<Story />);
await Story.play?.({ canvasElement: container });
expect(baseElement).toMatchSnapshot();
});
36 changes: 34 additions & 2 deletions code/renderers/vue3/src/__tests__/composeStories/Button.stories.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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: `
<div>
<div data-testid="loaded-data">{{loaded}}</div>
<div data-testid="spy-data">{{data}}</div>
</div>
`,
}),
loaders: [
async () => {
spyFn.mockReturnValueOnce('baz');
return {
value: 'bar',
};
},
],
play: async () => {
expect(spyFn).toHaveBeenCalledWith('foo');
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,22 @@ exports[`Renders CSF3Primary story 1`] = `
</div>
</body>
`;

exports[`Renders LoaderStory story 1`] = `
<body>
<div>
<div>
<div
data-testid="loaded-data"
>
bar
</div>
<div
data-testid="spy-data"
>
baz
</div>
</div>
</div>
</body>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 (
<div>
<div data-testid="loaded-data">{loaded.value}</div>
<div data-testid="spy-data">{String(data)}</div>
</div>
);
},
loaders: [
async () => {
spyFn.mockReturnValueOnce('mocked');
return {
value: 'bar',
};
},
],
play: async () => {
expect(spyFn).toHaveBeenCalledWith('foo');
},
};

0 comments on commit 29ff689

Please sign in to comment.