Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script module lang="ts">
import { expect } from 'storybook/test';

import preview from './preview.ts';
import Button from '../Button.svelte';

const meta = preview.meta({ component: Button, args: { disabled: false } });
const { Story } = meta;
// `icon` isn't a prop of the button: the template renders it as the label.
const { Story: IconStory } = meta.type<{ args: { icon: string } }>();
</script>

<Story
name="Primary"
args={{ label: 'Primary' }}
play={async ({ canvas }) => {
await expect(canvas.getByRole('button')).toHaveTextContent('Primary');
}}
/>

<IconStory
name="Icon"
args={{ label: 'Icon', icon: '★' }}
play={async ({ args, canvas }) => {
await expect(args.icon).toBe('★');
await expect(canvas.getByRole('button')).toHaveTextContent('★');
}}
>
{#snippet template({ icon, ...args })}
<Button {...args} label={icon} />
{/snippet}
</IconStory>
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { isStory } from 'storybook/internal/csf';
import { composeStories } from '../../portable-stories.ts';
import * as ButtonStories from './button.stories.svelte';
import * as InteractionsStories from './interactions.stories.svelte';
import * as MetaTypeStories from './meta-type.stories.svelte';
import * as TemplatingStories from './templating.stories.svelte';

afterEach(() => {
cleanup();
});

const files = { ButtonStories, InteractionsStories, TemplatingStories };
const files = { ButtonStories, InteractionsStories, MetaTypeStories, TemplatingStories };

describe.each(Object.entries(files))('%s', (_name, file) => {
const stories = Object.entries(file).filter(([, story]) => isStory(story));
Expand All @@ -26,6 +27,13 @@ describe.each(Object.entries(files))('%s', (_name, file) => {
});
});

it('exports the stories of the Story components from meta.type<>()', () => {
expect(Object.keys(MetaTypeStories).filter((key) => key !== '__namedExportsOrder')).toEqual([
'Primary',
'Icon',
]);
});

it('composes the stories with composeStories', async () => {
// The types of composeStories need a default export, which a CSF factories file doesn't have.
const { Primary } = composeStories(ButtonStories as never) as Record<string, any>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<script module lang="ts">
import { expectTypeOf } from 'vitest';

import type { Args, Decorator } from '../../public-types.ts';

import preview from './preview.ts';
import Button from '../Button.svelte';

const meta = preview.meta({ component: Button, args: { disabled: false } });
const { Story } = meta;
const { Story: IconStory } = meta.type<{ args: { icon: 'star' | 'heart' } }>();
const { Story: RequiredIconStory } = meta.type<{ args: { icon: string } }>();
const { Story: OptionalIconStory } = meta.type<{ args: { icon?: string } }>();
const { Story: DisabledStory } = meta.type<{ args: { disabled: true } }>();

const complete = preview.meta({ component: Button, args: { label: 'Hi', disabled: false } });
const { Story: CompleteOptionalStory } = complete.type<{ args: { icon?: string } }>();
const { Story: CompleteRequiredStory } = complete.type<{ args: { icon: string } }>();

const withTheme: Decorator<{ theme: 'light' | 'dark' }> = (Story) => Story();
const typedMeta = preview.type<{ args: { locale: 'en' | 'nl' } }>().meta({
component: Button,
decorators: [withTheme],
args: { locale: 'nl', disabled: false },
});
const { Story: ComposedStory } = typedMeta
.type<{ args: { icon: string } }>()
.type<{ args: { size: number } }>();
</script>

<!-- adds an arg to the args and the template of that story only -->

<IconStory
name="Icon"
args={{ label: 'Hi', icon: 'star' }}
play={async ({ args }) => {
expectTypeOf(args.icon).toEqualTypeOf<'star' | 'heart'>();
}}
>
{#snippet template(args)}
{expectTypeOf(args.icon).toEqualTypeOf<'star' | 'heart'>()}
{expectTypeOf(args.label).toEqualTypeOf<string>()}
<Button {...args} />
{/snippet}
</IconStory>

<Story name="No icon" args={{ label: 'Hi' }}>
{#snippet template(args)}
{expectTypeOf(args).not.toHaveProperty('icon')}
<Button {...args} />
{/snippet}
</Story>

<IconStory
name="Wrong icon"
args={{
label: 'Hi',
// @ts-expect-error icon must be 'star' | 'heart'
icon: 'x',
}}
/>

{#snippet iconTemplate(args: Args<typeof IconStory>)}
{expectTypeOf(args.icon).toEqualTypeOf<'star' | 'heart'>()}
<Button {...args} />
{/snippet}

<IconStory name="Shared icon template" template={iconTemplate} args={{ label: 'Hi', icon: 'heart' }} />

<!-- a required key is required on that component only -->

{/* @ts-expect-error icon is required */ null}
<RequiredIconStory name="Icon missing" args={{ label: 'Hi' }} />

<OptionalIconStory name="Optional icon" args={{ label: 'Hi' }} />

<Story name="Other story" args={{ label: 'Hi' }} />

<!-- args set in meta stay optional and the others stay required -->

<RequiredIconStory name="Meta arg not set" args={{ label: 'Hi', icon: 'star' }} />

<RequiredIconStory name="Meta arg set" args={{ label: 'Hi', icon: 'star', disabled: true }} />

{/* @ts-expect-error label is required */ null}
<RequiredIconStory name="Label missing" args={{ icon: 'star' }} />

<!-- an arg of the meta that type<>() declares again must be set again -->

{/* @ts-expect-error disabled is required, the meta sets it to false */ null}
<DisabledStory name="Redeclared arg missing" args={{ label: 'Hi' }} />

<DisabledStory name="Redeclared arg set" args={{ label: 'Hi', disabled: true }} />

<!-- no args are needed when no required arg is left or the content takes no args -->

<CompleteOptionalStory name="Complete" />

{/* @ts-expect-error icon is required */ null}
<CompleteRequiredStory name="Complete icon missing" />

<CompleteRequiredStory name="Complete as child" asChild>
<p>Static content</p>
</CompleteRequiredStory>

<CompleteRequiredStory name="Complete template without parameters">
{#snippet template()}
<p>Static content</p>
{/snippet}
</CompleteRequiredStory>

<!-- composes with preview.type<>(), decorators and itself -->

<ComposedStory
name="Composed"
args={{ label: 'Hi', theme: 'dark', icon: 'star', size: 1 }}
play={async ({ args }) => {
expectTypeOf(args.locale).toEqualTypeOf<'en' | 'nl'>();
expectTypeOf(args.theme).toEqualTypeOf<'light' | 'dark'>();
expectTypeOf(args.icon).toEqualTypeOf<string>();
expectTypeOf(args.size).toEqualTypeOf<number>();
}}
/>

{/* @ts-expect-error size is required */ null}
<ComposedStory name="Composed size missing" args={{ label: 'Hi', theme: 'dark', icon: 'star' }} />
Original file line number Diff line number Diff line change
Expand Up @@ -486,11 +486,187 @@ describe('CSF factories', () => {
<Story name="Primary" args={{ label: 'Primary', theme: 'dark' }} />
`);

expect(output).toContain(dedent`
expect(output).toMatchInlineSnapshot(`
"import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';

Button_stories[$.FILENAME] = 'Button.stories.svelte';

import preview from '#.storybook/preview';
import Button from './Button.svelte';
import * as $ from 'svelte/internal/client';

const $__meta = preview.type().meta({ component: Button });
const { Story } = $__meta;

function Button_stories($$anchor, $$props) {
$.check_target(new.target);
$.push($$props, false, Button_stories);

var $$exports = { ...$.legacy_api() };

$.init();

$.add_svelte_meta(
() => Story($$anchor, {
name: 'Primary',
args: { label: 'Primary', theme: 'dark' },
parameters: {
__svelteCsf: { rawCode: "<Button {...args} />" }
}
}),
'component',
Button_stories,
8,
0,
{ componentTag: 'Story' }
);

return $.pop($$exports);
}
import { createRuntimeStories } from "@storybook/svelte/internal/svelte-csf/create-runtime-stories";

const $__stories = createRuntimeStories(Button_stories, $__meta.input);

export const __namedExportsOrder = ["Primary"];

const $__Primary = $__meta.story({
...$__stories["Primary"],
tags: ["svelte-csf-v5"]
});

export { $__Primary as Primary };"
`);
});

it('creates the stories of the Story components from meta.type<>()', async ({ expect }) => {
const output = await transform(dedent`
<script module lang="ts">
import preview from '#.storybook/preview';
import Button from './Button.svelte';

const meta = preview.meta({ component: Button });
const { Story } = meta;
const { Story: IconStory } = meta.type<{ args: { icon: string } }>();
</script>

<Story name="Primary" args={{ label: 'Primary' }} />

<IconStory name="Icon" args={{ label: 'Icon', icon: 'star' }}>
{#snippet template({ icon, ...args })}
<Button {...args} label={icon} />
{/snippet}
</IconStory>
`);

expect(output).toMatchInlineSnapshot(`
"import 'svelte/internal/disclose-version';
import 'svelte/internal/flags/legacy';

Button_stories[$.FILENAME] = 'Button.stories.svelte';

import preview from '#.storybook/preview';
import Button from './Button.svelte';
import * as $ from 'svelte/internal/client';

const meta = preview.meta({ component: Button });
const { Story } = meta;
const { Story: IconStory } = meta.type();
var root = $.add_locations($.from_html(\`<!> <!>\`, 1), Button_stories[$.FILENAME], []);

function Button_stories($$anchor, $$props) {
$.check_target(new.target);
$.push($$props, false, Button_stories);

var $$exports = { ...$.legacy_api() };

$.init();

var fragment = root();
var node = $.first_child(fragment);

$.add_svelte_meta(
() => Story(node, {
name: 'Primary',
args: { label: 'Primary' },
parameters: {
__svelteCsf: { rawCode: "<Button {...args} />" }
}
}),
'component',
Button_stories,
10,
0,
{ componentTag: 'Story' }
);

var node_1 = $.sibling(node, 2);

{
const template = $.wrap_snippet(Button_stories, function ($$anchor, $$arg0) {
$.validate_snippet_args(...arguments);

let icon = () => $$arg0?.().icon;

icon();

let args = () => $.exclude_from_object($$arg0?.(), ['icon']);

args();

$.add_svelte_meta(
() => Button($$anchor, $.spread_props(args, {
get label() {
return icon();
}
})),
'component',
Button_stories,
14,
4,
{ componentTag: 'Button' }
);
});

$.add_svelte_meta(
() => IconStory(node_1, {
name: 'Icon',
args: { label: 'Icon', icon: 'star' },
template,
$$slots: { template: true },
parameters: {
__svelteCsf: { rawCode: "<Button {...args} label={icon} />" }
}
}),
'component',
Button_stories,
12,
0,
{ componentTag: 'IconStory' }
);
}

$.append($$anchor, fragment);
return $.pop($$exports);
}
import { createRuntimeStories } from "@storybook/svelte/internal/svelte-csf/create-runtime-stories";

const $__stories = createRuntimeStories(Button_stories, meta.input);

export const __namedExportsOrder = ["Primary", "Icon"];

const $__Primary = meta.story({
...$__stories["Primary"],
tags: ["svelte-csf-v5"]
});

const $__Icon = meta.story({
...$__stories["Icon"],
tags: ["svelte-csf-v5"]
});

export { $__Primary as Primary, $__Icon as Icon };"
`);
expect(output).toContain('const $__Primary = $__meta.story({');
});

it('uses the meta variable of the file', async ({ expect }) => {
Expand Down
Loading
Loading