Skip to content

Commit

Permalink
add composeStories api for svelte
Browse files Browse the repository at this point in the history
  • Loading branch information
yannbf committed Feb 5, 2024
1 parent ad8a3a9 commit a67f7fb
Show file tree
Hide file tree
Showing 5 changed files with 505 additions and 0 deletions.
121 changes: 121 additions & 0 deletions code/renderers/svelte/src/__test__/composeStories/Button.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { userEvent, within } from '@storybook/testing-library';
import type { Meta, StoryFn as CSF2Story, StoryObj } from '../..';

import Button from './Button.svelte';

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<typeof Button>;

export default meta;
type CSF3Story = StoryObj<typeof meta>;

// For testing purposes. Should be ignored in ComposeStories
export const ImNotAStory = 123;

const Template: CSF2Story = (args) => ({
components: { Button },
setup() {
return { args };
},
template: '<Button v-bind="args" />',
});

export const CSF2Secondary = Template.bind({});
CSF2Secondary.args = {
label: 'label coming from story args!',
primary: false,
};

const getCaptionForLocale = (locale: string) => {
switch (locale) {
case 'es':
return 'Hola!';
case 'fr':
return 'Bonjour!';
case 'kr':
return '안녕하세요!';
case 'pt':
return 'Olá!';
default:
return 'Hello!';
}
};

export const CSF2StoryWithLocale: CSF2Story = (args, { globals }) => ({
components: { Button },
setup() {
console.log({ globals });
const label = getCaptionForLocale(globals.locale);
return { args: { ...args, label } };
},
template: `<div>
<p>locale: ${globals.locale}</p>
<Button v-bind="args" />
</div>`,
});
CSF2StoryWithLocale.storyName = 'WithLocale';

export const CSF2StoryWithParamsAndDecorator = Template.bind({});
CSF2StoryWithParamsAndDecorator.args = {
label: 'foo',
};
CSF2StoryWithParamsAndDecorator.parameters = {
layout: 'centered',
};
CSF2StoryWithParamsAndDecorator.decorators = [
() => ({ template: '<div style="margin: 3em;"><story/></div>' }),
];

export const CSF3Primary: CSF3Story = {
args: {
label: 'foo',
size: 'large',
primary: true,
},
};

export const CSF3Button: CSF3Story = {
args: { label: 'foo' },
};

export const CSF3ButtonWithRender: CSF3Story = {
...CSF3Button,
render: (args) => ({
components: { Button },
setup() {
return { args };
},
template: `
<div>
<p data-testid="custom-render">I am a custom render function</p>
<Button v-bind="args" />
</div>
`,
}),
};

export const CSF3InputFieldFilled: CSF3Story = {
...CSF3Button,
render: (args) => ({
components: { Button },
setup() {
return { args };
},
template: '<input data-testid="input" />',
}),
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await step('Step label', async () => {
await userEvent.type(canvas.getByTestId('input'), 'Hello world!');
});
},
};
32 changes: 32 additions & 0 deletions code/renderers/svelte/src/__test__/composeStories/Button.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
/**
* Is this the principal call to action on the page?
*/
export let primary = false;
/**
* What background color to use
*/
export let backgroundColor: string | undefined = undefined;
/**
* How large should the button be?
*/
export let size: 'small' | 'medium' | 'large' = 'medium';
/**
* Button contents
*/
export let label: string = '';
$: mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
$: style = backgroundColor ? `background-color: ${backgroundColor}` : '';
</script>

<button
type="button"
class={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
{style}
on:click
>
{label}
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/// <reference types="@testing-library/jest-dom" />;
import { it, expect, vi, describe } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import '@testing-library/svelte/vitest';
import { expectTypeOf } from 'expect-type';
import type { Meta } from '../../';
import * as stories from './Button.stories';
// import type Button from './Button.svelte';
import Button from './Button.svelte';
import { composeStories, composeStory, setProjectAnnotations } from '../../portable-stories';
import { SvelteComponent } from 'svelte';

// 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);
// 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<typeof composeStories>[0];

// expectTypeOf({
// ...stories,
// default: stories.default as Meta<typeof Button>,
// }).toMatchTypeOf<ComposeStoriesParam>();

// expectTypeOf({
// ...stories,
// default: stories.default satisfies Meta<typeof Button>,
// }).toMatchTypeOf<ComposeStoriesParam>();
// });
// });
123 changes: 123 additions & 0 deletions code/renderers/svelte/src/__test__/composeStories/internals.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import React from 'react';
// import { addons } from '@storybook/preview-api';
// import { render, screen } from '@testing-library/svelte';
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');
});
});

// TODO: Bring this back
// 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 = () => <div>hello world</div>;
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: () => <div>hello world</div>,
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: () => <div>hello world</div>,
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();
// });
Loading

0 comments on commit a67f7fb

Please sign in to comment.