mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-09 00:19:13 +08:00
Merge branch 'next' into valentin/fix-prod-mode
This commit is contained in:
commit
a61d7563d0
@ -67,7 +67,7 @@
|
||||
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/addon-bundle.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.10",
|
||||
"@storybook/csf": "0.1.11",
|
||||
"@storybook/global": "^5.0.0",
|
||||
"ts-dedent": "^2.0.0"
|
||||
},
|
||||
|
@ -244,7 +244,7 @@
|
||||
"prep": "bun ./scripts/prep.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.10",
|
||||
"@storybook/csf": "0.1.11",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^18.0.0",
|
||||
"browser-assert": "^1.2.1",
|
||||
|
@ -33,6 +33,7 @@ import { StoryStore } from '../../store';
|
||||
import { StoryRender } from './render/StoryRender';
|
||||
import type { CsfDocsRender } from './render/CsfDocsRender';
|
||||
import type { MdxDocsRender } from './render/MdxDocsRender';
|
||||
import { mountDestructured } from './render/mount-utils';
|
||||
import type { Args, Globals, Renderer, StoryId } from '@storybook/core/types';
|
||||
import type {
|
||||
ModuleImportFn,
|
||||
@ -291,7 +292,11 @@ export class Preview<TRenderer extends Renderer> {
|
||||
await Promise.all(
|
||||
this.storyRenders
|
||||
.filter((r) => r.id === storyId && !r.renderOptions.forceInitialArgs)
|
||||
.map((r) => r.rerender())
|
||||
.map((r) =>
|
||||
// We only run the play function, with in a force remount.
|
||||
// But when mount is destructured, the rendering happens inside of the play function.
|
||||
r.story && mountDestructured(r.story.playFunction) ? r.remount() : r.rerender()
|
||||
)
|
||||
);
|
||||
|
||||
this.channel.emit(STORY_ARGS_UPDATED, {
|
||||
|
@ -908,6 +908,41 @@ describe('PreviewWeb', () => {
|
||||
expect(mockChannel.emit).toHaveBeenCalledWith(STORY_RENDERED, 'component-one--a');
|
||||
});
|
||||
|
||||
describe('if play function destructures mount', () => {
|
||||
it('passes forceRemount to renderToCanvas', async () => {
|
||||
document.location.search = '?id=component-one--a';
|
||||
const newImportFn = vi.fn(async (path) => {
|
||||
if (path === './src/ComponentOne.stories.js') {
|
||||
return {
|
||||
...componentOneExports,
|
||||
a: { ...componentOneExports.a, play: ({ mount }: any) => mount() },
|
||||
};
|
||||
}
|
||||
return importFn(path);
|
||||
});
|
||||
await createAndRenderPreview({ importFn: newImportFn });
|
||||
|
||||
mockChannel.emit.mockClear();
|
||||
projectAnnotations.renderToCanvas.mockClear();
|
||||
emitter.emit(UPDATE_STORY_ARGS, {
|
||||
storyId: 'component-one--a',
|
||||
updatedArgs: { new: 'arg' },
|
||||
});
|
||||
await waitForRender();
|
||||
|
||||
expect(projectAnnotations.renderToCanvas).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
forceRemount: true,
|
||||
storyContext: expect.objectContaining({
|
||||
initialArgs: { foo: 'a', one: 1 },
|
||||
args: { foo: 'a', new: 'arg', one: 'mapped-1' },
|
||||
}),
|
||||
}),
|
||||
'story-element'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('while story is still rendering', () => {
|
||||
it('runs loaders again after renderToCanvas is done', async () => {
|
||||
// Arrange - set up a gate to control when the loaders run
|
||||
@ -3626,6 +3661,7 @@ describe('PreviewWeb', () => {
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component One",
|
||||
},
|
||||
"component-one--b": {
|
||||
@ -3674,6 +3710,7 @@ describe('PreviewWeb', () => {
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component One",
|
||||
},
|
||||
"component-one--e": {
|
||||
@ -3700,6 +3737,7 @@ describe('PreviewWeb', () => {
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component One",
|
||||
},
|
||||
"component-two--c": {
|
||||
@ -3736,6 +3774,7 @@ describe('PreviewWeb', () => {
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component Two",
|
||||
},
|
||||
}
|
||||
|
@ -463,17 +463,9 @@ export class PreviewWithSelection<TRenderer extends Renderer> extends Preview<TR
|
||||
this.channel.emit(STORY_THREW_EXCEPTION, { name, message, stack });
|
||||
this.channel.emit(STORY_RENDER_PHASE_CHANGED, { newPhase: 'errored', storyId });
|
||||
|
||||
// Ignored exceptions exist for control flow purposes, and are typically handled elsewhere.
|
||||
//
|
||||
// FIXME: Should be '=== IGNORED_EXCEPTION', but currently the object
|
||||
// is coming from two different bundles (index.js vs index.mjs)
|
||||
//
|
||||
// https://github.com/storybookjs/storybook/issues/19321
|
||||
if (!error.message?.startsWith('ignoredException')) {
|
||||
this.view.showErrorDisplay(error);
|
||||
logger.error(`Error rendering story '${storyId}':`);
|
||||
logger.error(error);
|
||||
}
|
||||
this.view.showErrorDisplay(error);
|
||||
logger.error(`Error rendering story '${storyId}':`);
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
// renderError is used by the various app layers to inform the user they have done something
|
||||
|
@ -1,7 +1,7 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Channel } from '@storybook/core/channels';
|
||||
import type { Renderer, StoryIndexEntry } from '@storybook/core/types';
|
||||
import type { PreparedStory, Renderer, StoryContext, StoryIndexEntry } from '@storybook/core/types';
|
||||
import type { StoryStore } from '../../store';
|
||||
import { PREPARE_ABORTED } from './Render';
|
||||
|
||||
@ -26,29 +26,45 @@ const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
window.location = { reload: vi.fn() } as any;
|
||||
|
||||
const buildStory = (overrides: Partial<PreparedStory> = {}): PreparedStory =>
|
||||
({
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
name: 'name',
|
||||
tags: [],
|
||||
applyLoaders: vi.fn(),
|
||||
applyBeforeEach: vi.fn(),
|
||||
unboundStoryFn: vi.fn(),
|
||||
playFunction: vi.fn(),
|
||||
mount: vi.fn((context: StoryContext) => {
|
||||
return async () => {
|
||||
await context.renderToCanvas();
|
||||
return context.canvas;
|
||||
};
|
||||
}),
|
||||
...overrides,
|
||||
}) as any;
|
||||
|
||||
const buildStore = (overrides: Partial<StoryStore<Renderer>> = {}): StoryStore<Renderer> =>
|
||||
({
|
||||
getStoryContext: () => ({}),
|
||||
addCleanupCallbacks: vi.fn(),
|
||||
cleanupStory: vi.fn(),
|
||||
...overrides,
|
||||
}) as any;
|
||||
|
||||
describe('StoryRender', () => {
|
||||
it('does run play function if passed autoplay=true', async () => {
|
||||
const story = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
name: 'name',
|
||||
tags: [],
|
||||
applyLoaders: vi.fn(),
|
||||
applyBeforeEach: vi.fn(),
|
||||
unboundStoryFn: vi.fn(),
|
||||
playFunction: vi.fn(),
|
||||
prepareContext: vi.fn(),
|
||||
};
|
||||
|
||||
const story = buildStory();
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
{ getStoryContext: () => ({}), addCleanupCallbacks: vi.fn() } as any,
|
||||
buildStore(),
|
||||
vi.fn() as any,
|
||||
{} as any,
|
||||
entry.id,
|
||||
'story',
|
||||
{ autoplay: true },
|
||||
story as any
|
||||
story
|
||||
);
|
||||
|
||||
await render.renderToElement({} as any);
|
||||
@ -56,27 +72,16 @@ describe('StoryRender', () => {
|
||||
});
|
||||
|
||||
it('does not run play function if passed autoplay=false', async () => {
|
||||
const story = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
name: 'name',
|
||||
tags: [],
|
||||
applyLoaders: vi.fn(),
|
||||
applyBeforeEach: vi.fn(),
|
||||
unboundStoryFn: vi.fn(),
|
||||
playFunction: vi.fn(),
|
||||
prepareContext: vi.fn(),
|
||||
};
|
||||
|
||||
const story = buildStory();
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
{ getStoryContext: () => ({}), addCleanupCallbacks: vi.fn() } as any,
|
||||
buildStore(),
|
||||
vi.fn() as any,
|
||||
{} as any,
|
||||
entry.id,
|
||||
'story',
|
||||
{ autoplay: false },
|
||||
story as any
|
||||
story
|
||||
);
|
||||
|
||||
await render.renderToElement({} as any);
|
||||
@ -86,32 +91,20 @@ describe('StoryRender', () => {
|
||||
it('only rerenders once when triggered multiple times while pending', async () => {
|
||||
// Arrange - setup StoryRender and async gate blocking applyLoaders
|
||||
const [loaderGate, openLoaderGate] = createGate();
|
||||
const story = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
name: 'name',
|
||||
tags: [],
|
||||
applyLoaders: vi.fn(() => loaderGate),
|
||||
applyBeforeEach: vi.fn(),
|
||||
unboundStoryFn: vi.fn(),
|
||||
playFunction: vi.fn(),
|
||||
prepareContext: vi.fn(),
|
||||
};
|
||||
const store = {
|
||||
getStoryContext: () => ({}),
|
||||
cleanupStory: vi.fn(),
|
||||
addCleanupCallbacks: vi.fn(),
|
||||
};
|
||||
const renderToScreen = vi.fn();
|
||||
|
||||
const story = buildStory({
|
||||
applyLoaders: vi.fn(() => loaderGate as any),
|
||||
});
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
store as any,
|
||||
buildStore(),
|
||||
renderToScreen,
|
||||
{} as any,
|
||||
entry.id,
|
||||
'story',
|
||||
{ autoplay: true },
|
||||
story as any
|
||||
story
|
||||
);
|
||||
// Arrange - render (blocked by loaders)
|
||||
render.renderToElement({} as any);
|
||||
@ -133,27 +126,129 @@ describe('StoryRender', () => {
|
||||
|
||||
// Assert - loaded and rendered twice, played once
|
||||
await vi.waitFor(async () => {
|
||||
console.log(render.phase);
|
||||
expect(story.applyLoaders).toHaveBeenCalledTimes(2);
|
||||
expect(renderToScreen).toHaveBeenCalledTimes(2);
|
||||
expect(story.playFunction).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls mount if play function does not destructure mount', async () => {
|
||||
const actualMount = vi.fn(async (context) => {
|
||||
await context.renderToCanvas();
|
||||
return {};
|
||||
});
|
||||
const story = buildStory({
|
||||
mount: (context) => () => actualMount(context) as any,
|
||||
playFunction: () => {},
|
||||
});
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
buildStore(),
|
||||
vi.fn() as any,
|
||||
{} as any,
|
||||
entry.id,
|
||||
'story',
|
||||
{ autoplay: true },
|
||||
story
|
||||
);
|
||||
|
||||
await render.renderToElement({} as any);
|
||||
expect(actualMount).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call mount if play function destructures mount', async () => {
|
||||
const actualMount = vi.fn(async (context) => {
|
||||
await context.renderToCanvas();
|
||||
return context.canvas;
|
||||
});
|
||||
const story = buildStory({
|
||||
mount: (context) => () => actualMount(context) as any,
|
||||
playFunction: ({ mount }) => {},
|
||||
});
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
buildStore(),
|
||||
vi.fn() as any,
|
||||
{} as any,
|
||||
entry.id,
|
||||
'story',
|
||||
{ autoplay: true },
|
||||
story
|
||||
);
|
||||
|
||||
await render.renderToElement({} as any);
|
||||
expect(actualMount).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('errors if play function calls mount without destructuring', async () => {
|
||||
const actualMount = vi.fn(async (context) => {
|
||||
await context.renderToCanvas();
|
||||
return {};
|
||||
});
|
||||
const story = buildStory({
|
||||
mount: (context) => () => actualMount(context) as any,
|
||||
playFunction: async (context) => {
|
||||
await context.mount();
|
||||
},
|
||||
});
|
||||
const view = { showException: vi.fn() };
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
buildStore(),
|
||||
vi.fn() as any,
|
||||
view as any,
|
||||
entry.id,
|
||||
'story',
|
||||
{ autoplay: true },
|
||||
story
|
||||
);
|
||||
|
||||
await render.renderToElement({} as any);
|
||||
expect(view.showException).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('enters rendering phase during play if play function calls mount', async () => {
|
||||
const actualMount = vi.fn(async (context) => {
|
||||
await context.renderToCanvas();
|
||||
return {};
|
||||
});
|
||||
const story = buildStory({
|
||||
mount: (context) => () => actualMount(context) as any,
|
||||
playFunction: ({ mount }) => {
|
||||
expect(render.phase).toBe('playing');
|
||||
mount();
|
||||
},
|
||||
});
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
buildStore(),
|
||||
vi.fn(() => {
|
||||
expect(render.phase).toBe('rendering');
|
||||
}) as any,
|
||||
{} as any,
|
||||
entry.id,
|
||||
'story',
|
||||
{ autoplay: true },
|
||||
story
|
||||
);
|
||||
|
||||
await render.renderToElement({} as any);
|
||||
expect(actualMount).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('teardown', () => {
|
||||
it('throws PREPARE_ABORTED if torndown during prepare', async () => {
|
||||
const [importGate, openImportGate] = createGate();
|
||||
const mockStore = {
|
||||
const mockStore = buildStore({
|
||||
loadStory: vi.fn(async () => {
|
||||
await importGate;
|
||||
return {};
|
||||
}),
|
||||
cleanupStory: vi.fn(),
|
||||
};
|
||||
}) as any,
|
||||
});
|
||||
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
mockStore as unknown as StoryStore<Renderer>,
|
||||
mockStore,
|
||||
vi.fn(),
|
||||
{} as any,
|
||||
entry.id,
|
||||
@ -172,31 +267,19 @@ describe('StoryRender', () => {
|
||||
it('reloads the page when tearing down during loading', async () => {
|
||||
// Arrange - setup StoryRender and async gate blocking applyLoaders
|
||||
const [loaderGate] = createGate();
|
||||
const story = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
name: 'name',
|
||||
tags: [],
|
||||
applyLoaders: vi.fn(() => loaderGate),
|
||||
applyBeforeEach: vi.fn(),
|
||||
unboundStoryFn: vi.fn(),
|
||||
playFunction: vi.fn(),
|
||||
prepareContext: vi.fn(),
|
||||
};
|
||||
const store = {
|
||||
getStoryContext: () => ({}),
|
||||
cleanupStory: vi.fn(),
|
||||
addCleanupCallbacks: vi.fn(),
|
||||
};
|
||||
const story = buildStory({
|
||||
applyLoaders: vi.fn(() => loaderGate as any),
|
||||
});
|
||||
const store = buildStore();
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
store as any,
|
||||
store,
|
||||
vi.fn() as any,
|
||||
{} as any,
|
||||
entry.id,
|
||||
'story',
|
||||
{ autoplay: true },
|
||||
story as any
|
||||
story
|
||||
);
|
||||
|
||||
// Act - render (blocked by loaders), teardown
|
||||
@ -215,33 +298,19 @@ describe('StoryRender', () => {
|
||||
it('reloads the page when tearing down during rendering', async () => {
|
||||
// Arrange - setup StoryRender and async gate blocking renderToScreen
|
||||
const [renderGate] = createGate();
|
||||
const story = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
name: 'name',
|
||||
tags: [],
|
||||
applyLoaders: vi.fn(),
|
||||
applyBeforeEach: vi.fn(),
|
||||
unboundStoryFn: vi.fn(),
|
||||
playFunction: vi.fn(),
|
||||
prepareContext: vi.fn(),
|
||||
};
|
||||
const store = {
|
||||
getStoryContext: () => ({}),
|
||||
cleanupStory: vi.fn(),
|
||||
addCleanupCallbacks: vi.fn(),
|
||||
};
|
||||
const story = buildStory();
|
||||
const store = buildStore();
|
||||
const renderToScreen = vi.fn(() => renderGate);
|
||||
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
store as any,
|
||||
store,
|
||||
renderToScreen as any,
|
||||
{} as any,
|
||||
entry.id,
|
||||
'story',
|
||||
{ autoplay: true },
|
||||
story as any
|
||||
story
|
||||
);
|
||||
|
||||
// Act - render (blocked by renderToScreen), teardown
|
||||
@ -261,32 +330,20 @@ describe('StoryRender', () => {
|
||||
it('reloads the page when tearing down during playing', async () => {
|
||||
// Arrange - setup StoryRender and async gate blocking playing
|
||||
const [playGate] = createGate();
|
||||
const story = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
name: 'name',
|
||||
tags: [],
|
||||
applyLoaders: vi.fn(),
|
||||
applyBeforeEach: vi.fn(),
|
||||
unboundStoryFn: vi.fn(),
|
||||
playFunction: vi.fn(() => playGate),
|
||||
prepareContext: vi.fn(),
|
||||
};
|
||||
const store = {
|
||||
getStoryContext: () => ({}),
|
||||
cleanupStory: vi.fn(),
|
||||
addCleanupCallbacks: vi.fn(),
|
||||
};
|
||||
const story = buildStory({
|
||||
playFunction: vi.fn(() => playGate as any),
|
||||
});
|
||||
const store = buildStore();
|
||||
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
store as any,
|
||||
store,
|
||||
vi.fn() as any,
|
||||
{} as any,
|
||||
entry.id,
|
||||
'story',
|
||||
{ autoplay: true },
|
||||
story as any
|
||||
story
|
||||
);
|
||||
|
||||
// Act - render (blocked by playFn), teardown
|
||||
@ -307,31 +364,20 @@ describe('StoryRender', () => {
|
||||
it('reloads the page when remounting during loading', async () => {
|
||||
// Arrange - setup StoryRender and async gate blocking applyLoaders
|
||||
const [loaderGate] = createGate();
|
||||
const story = {
|
||||
id: 'id',
|
||||
title: 'title',
|
||||
name: 'name',
|
||||
tags: [],
|
||||
applyLoaders: vi.fn(() => loaderGate),
|
||||
applyBeforeEach: vi.fn(),
|
||||
unboundStoryFn: vi.fn(),
|
||||
playFunction: vi.fn(),
|
||||
prepareContext: vi.fn(),
|
||||
};
|
||||
const store = {
|
||||
getStoryContext: () => ({}),
|
||||
cleanupStory: vi.fn(),
|
||||
addCleanupCallbacks: vi.fn(),
|
||||
};
|
||||
const story = buildStory({
|
||||
applyLoaders: vi.fn(() => loaderGate as any),
|
||||
});
|
||||
const store = buildStore();
|
||||
|
||||
const render = new StoryRender(
|
||||
new Channel({}),
|
||||
store as any,
|
||||
store,
|
||||
vi.fn() as any,
|
||||
{} as any,
|
||||
entry.id,
|
||||
'story',
|
||||
{ autoplay: true },
|
||||
story as any
|
||||
story
|
||||
);
|
||||
|
||||
// Act - render, blocked by loaders
|
||||
|
@ -8,8 +8,11 @@ import {
|
||||
import type { StoryStore } from '../../store';
|
||||
import type { Render, RenderType } from './Render';
|
||||
import { PREPARE_ABORTED } from './Render';
|
||||
import { mountDestructured } from './mount-utils';
|
||||
import { MountMustBeDestructuredError } from '@storybook/core-events/preview-errors';
|
||||
|
||||
import type {
|
||||
Canvas,
|
||||
PreparedStory,
|
||||
RenderContext,
|
||||
RenderContextCallbacks,
|
||||
@ -179,6 +182,10 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
|
||||
// abort controller may be torn down (above) before we actually check the signal.
|
||||
const abortSignal = (this.abortController as AbortController).signal;
|
||||
|
||||
let mounted = false;
|
||||
|
||||
const isMountDestructured = playFunction && mountDestructured(playFunction);
|
||||
|
||||
try {
|
||||
const context: StoryContext<TRenderer> = {
|
||||
...this.storyContext(),
|
||||
@ -188,9 +195,23 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
|
||||
loaded: {},
|
||||
step: (label, play) => runStep(label, play, context),
|
||||
context: null!,
|
||||
canvas: {},
|
||||
canvas: {} as Canvas,
|
||||
mount: null!,
|
||||
renderToCanvas: async () => {
|
||||
await this.runPhase(abortSignal, 'rendering', async () => {
|
||||
const teardown = await this.renderToScreen(renderContext, canvasElement);
|
||||
this.teardownRender = teardown || (() => {});
|
||||
mounted = true;
|
||||
});
|
||||
|
||||
if (isMountDestructured) {
|
||||
// put the phase back to playing if mount is used inside a play function
|
||||
await this.runPhase(abortSignal, 'playing', async () => {});
|
||||
}
|
||||
},
|
||||
};
|
||||
context.context = context;
|
||||
context.mount = this.story.mount(context);
|
||||
|
||||
const renderContext: RenderContext<TRenderer> = {
|
||||
componentId,
|
||||
@ -227,10 +248,9 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
|
||||
|
||||
if (abortSignal.aborted) return;
|
||||
|
||||
await this.runPhase(abortSignal, 'rendering', async () => {
|
||||
const teardown = await this.renderToScreen(renderContext, canvasElement);
|
||||
this.teardownRender = teardown || (() => {});
|
||||
});
|
||||
if (!mounted && !isMountDestructured) {
|
||||
await context.mount();
|
||||
}
|
||||
|
||||
this.notYetRendered = false;
|
||||
if (abortSignal.aborted) return;
|
||||
@ -248,6 +268,11 @@ export class StoryRender<TRenderer extends Renderer> implements Render<TRenderer
|
||||
window.addEventListener('unhandledrejection', onError);
|
||||
this.disableKeyListeners = true;
|
||||
try {
|
||||
if (!isMountDestructured) {
|
||||
context.mount = async () => {
|
||||
throw new MountMustBeDestructuredError({ playFunction: playFunction.toString() });
|
||||
};
|
||||
}
|
||||
await this.runPhase(abortSignal, 'playing', async () => {
|
||||
await playFunction(context);
|
||||
});
|
||||
|
@ -0,0 +1,57 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { getUsedProps } from './mount-utils';
|
||||
|
||||
const StoryWithContext = {
|
||||
play: async (context: any) => {
|
||||
console.log(context);
|
||||
},
|
||||
};
|
||||
|
||||
const StoryWitCanvasElement = {
|
||||
play: async ({ canvasElement }: any) => {
|
||||
console.log(canvasElement);
|
||||
},
|
||||
};
|
||||
|
||||
const MountStory = {
|
||||
play: async ({ mount }: any) => {
|
||||
await mount();
|
||||
},
|
||||
};
|
||||
|
||||
const LongDefinition = {
|
||||
play: async ({
|
||||
mount,
|
||||
veryLongDefinitionnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn,
|
||||
over,
|
||||
multiple,
|
||||
lines,
|
||||
}: any) => {
|
||||
await mount();
|
||||
},
|
||||
};
|
||||
|
||||
test('Detect destructure', () => {
|
||||
expect(getUsedProps(StoryWithContext.play)).toMatchInlineSnapshot(`[]`);
|
||||
expect(getUsedProps(StoryWitCanvasElement.play)).toMatchInlineSnapshot(`
|
||||
[
|
||||
"canvasElement",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(getUsedProps(MountStory.play)).toMatchInlineSnapshot(`
|
||||
[
|
||||
"mount",
|
||||
]
|
||||
`);
|
||||
|
||||
expect(getUsedProps(LongDefinition.play)).toMatchInlineSnapshot(`
|
||||
[
|
||||
"mount",
|
||||
"veryLongDefinitionnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn",
|
||||
"over",
|
||||
"multiple",
|
||||
"lines",
|
||||
]
|
||||
`);
|
||||
});
|
@ -0,0 +1,45 @@
|
||||
// Inspired by Vitest fixture implementation:
|
||||
// https://github.com/vitest-dev/vitest/blob/200a4349a2f85686bc7005dce686d9d1b48b84d2/packages/runner/src/fixture.ts
|
||||
import { type PreparedStory, type Renderer } from '@storybook/types';
|
||||
|
||||
export function mountDestructured<TRenderer extends Renderer>(
|
||||
playFunction: PreparedStory<TRenderer>['playFunction']
|
||||
) {
|
||||
return playFunction && getUsedProps(playFunction).includes('mount');
|
||||
}
|
||||
export function getUsedProps(fn: Function) {
|
||||
const match = fn.toString().match(/[^(]*\(([^)]*)/);
|
||||
if (!match) return [];
|
||||
|
||||
const args = splitByComma(match[1]);
|
||||
if (!args.length) return [];
|
||||
|
||||
const first = args[0];
|
||||
if (!(first.startsWith('{') && first.endsWith('}'))) return [];
|
||||
|
||||
const props = splitByComma(first.slice(1, -1).replace(/\s/g, '')).map((prop) => {
|
||||
return prop.replace(/:.*|=.*/g, '');
|
||||
});
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
function splitByComma(s: string) {
|
||||
const result = [];
|
||||
const stack = [];
|
||||
let start = 0;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
if (s[i] === '{' || s[i] === '[') {
|
||||
stack.push(s[i] === '{' ? '}' : ']');
|
||||
} else if (s[i] === stack[stack.length - 1]) {
|
||||
stack.pop();
|
||||
} else if (!stack.length && s[i] === ',') {
|
||||
const token = s.substring(start, i).trim();
|
||||
if (token) result.push(token);
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
const lastToken = s.substring(start).trim();
|
||||
if (lastToken) result.push(lastToken);
|
||||
return result;
|
||||
}
|
@ -311,12 +311,14 @@ describe('StoryStore', () => {
|
||||
"fileName": "./src/ComponentOne-new.stories.js",
|
||||
},
|
||||
"playFunction": undefined,
|
||||
"renderToCanvas": undefined,
|
||||
"story": "A",
|
||||
"subcomponents": undefined,
|
||||
"tags": [
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component One",
|
||||
},
|
||||
}
|
||||
@ -477,12 +479,14 @@ describe('StoryStore', () => {
|
||||
"fileName": "./src/ComponentOne.stories.js",
|
||||
},
|
||||
"playFunction": undefined,
|
||||
"renderToCanvas": undefined,
|
||||
"story": "A",
|
||||
"subcomponents": undefined,
|
||||
"tags": [
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component One",
|
||||
},
|
||||
"component-one--b": {
|
||||
@ -516,12 +520,14 @@ describe('StoryStore', () => {
|
||||
"fileName": "./src/ComponentOne.stories.js",
|
||||
},
|
||||
"playFunction": undefined,
|
||||
"renderToCanvas": undefined,
|
||||
"story": "B",
|
||||
"subcomponents": undefined,
|
||||
"tags": [
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component One",
|
||||
},
|
||||
"component-two--c": {
|
||||
@ -555,12 +561,14 @@ describe('StoryStore', () => {
|
||||
"fileName": "./src/ComponentTwo.stories.js",
|
||||
},
|
||||
"playFunction": undefined,
|
||||
"renderToCanvas": undefined,
|
||||
"story": "C",
|
||||
"subcomponents": undefined,
|
||||
"tags": [
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component Two",
|
||||
},
|
||||
}
|
||||
@ -656,6 +664,7 @@ describe('StoryStore', () => {
|
||||
"foo": "a",
|
||||
},
|
||||
},
|
||||
"mount": [Function],
|
||||
"name": "A",
|
||||
"originalStoryFn": [MockFunction spy],
|
||||
"parameters": {
|
||||
@ -663,6 +672,7 @@ describe('StoryStore', () => {
|
||||
"fileName": "./src/ComponentOne.stories.js",
|
||||
},
|
||||
"playFunction": undefined,
|
||||
"renderToCanvas": undefined,
|
||||
"runStep": [Function],
|
||||
"story": "A",
|
||||
"storyFn": [Function],
|
||||
@ -671,6 +681,7 @@ describe('StoryStore', () => {
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component One",
|
||||
"unboundStoryFn": [Function],
|
||||
"undecoratedStoryFn": [Function],
|
||||
@ -704,6 +715,7 @@ describe('StoryStore', () => {
|
||||
"foo": "b",
|
||||
},
|
||||
},
|
||||
"mount": [Function],
|
||||
"name": "B",
|
||||
"originalStoryFn": [MockFunction spy],
|
||||
"parameters": {
|
||||
@ -711,6 +723,7 @@ describe('StoryStore', () => {
|
||||
"fileName": "./src/ComponentOne.stories.js",
|
||||
},
|
||||
"playFunction": undefined,
|
||||
"renderToCanvas": undefined,
|
||||
"runStep": [Function],
|
||||
"story": "B",
|
||||
"storyFn": [Function],
|
||||
@ -719,6 +732,7 @@ describe('StoryStore', () => {
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component One",
|
||||
"unboundStoryFn": [Function],
|
||||
"undecoratedStoryFn": [Function],
|
||||
@ -752,6 +766,7 @@ describe('StoryStore', () => {
|
||||
"foo": "c",
|
||||
},
|
||||
},
|
||||
"mount": [Function],
|
||||
"name": "C",
|
||||
"originalStoryFn": [MockFunction spy],
|
||||
"parameters": {
|
||||
@ -759,6 +774,7 @@ describe('StoryStore', () => {
|
||||
"fileName": "./src/ComponentTwo.stories.js",
|
||||
},
|
||||
"playFunction": undefined,
|
||||
"renderToCanvas": undefined,
|
||||
"runStep": [Function],
|
||||
"story": "C",
|
||||
"storyFn": [Function],
|
||||
@ -767,6 +783,7 @@ describe('StoryStore', () => {
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component Two",
|
||||
"unboundStoryFn": [Function],
|
||||
"undecoratedStoryFn": [Function],
|
||||
@ -823,12 +840,14 @@ describe('StoryStore', () => {
|
||||
"fileName": "./src/ComponentOne.stories.js",
|
||||
},
|
||||
"playFunction": undefined,
|
||||
"renderToCanvas": undefined,
|
||||
"story": "A",
|
||||
"subcomponents": undefined,
|
||||
"tags": [
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component One",
|
||||
},
|
||||
"component-one--b": {
|
||||
@ -862,12 +881,14 @@ describe('StoryStore', () => {
|
||||
"fileName": "./src/ComponentOne.stories.js",
|
||||
},
|
||||
"playFunction": undefined,
|
||||
"renderToCanvas": undefined,
|
||||
"story": "B",
|
||||
"subcomponents": undefined,
|
||||
"tags": [
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component One",
|
||||
},
|
||||
"component-two--c": {
|
||||
@ -901,12 +922,14 @@ describe('StoryStore', () => {
|
||||
"fileName": "./src/ComponentTwo.stories.js",
|
||||
},
|
||||
"playFunction": undefined,
|
||||
"renderToCanvas": undefined,
|
||||
"story": "C",
|
||||
"subcomponents": undefined,
|
||||
"tags": [
|
||||
"dev",
|
||||
"test",
|
||||
],
|
||||
"testingLibraryRender": undefined,
|
||||
"title": "Component Two",
|
||||
},
|
||||
},
|
||||
|
@ -374,6 +374,7 @@ export class StoryStore<TRenderer extends Renderer> {
|
||||
loaded: {},
|
||||
step: (label, play) => story.runStep(label, play, context),
|
||||
context: null!,
|
||||
mount: null!,
|
||||
canvas: {},
|
||||
viewMode: 'story',
|
||||
} as StoryContext<TRenderer>;
|
||||
|
@ -67,5 +67,7 @@ export function composeConfigs<TRenderer extends Renderer>(
|
||||
applyDecorators: getSingletonField(moduleExportList, 'applyDecorators'),
|
||||
runStep: composeStepRunners<TRenderer>(stepRunners),
|
||||
tags: getArrayField(moduleExportList, 'tags'),
|
||||
mount: getSingletonField(moduleExportList, 'mount'),
|
||||
testingLibraryRender: getSingletonField(moduleExportList, 'testingLibraryRender'),
|
||||
};
|
||||
}
|
||||
|
@ -85,8 +85,12 @@ describe('composeStory', () => {
|
||||
spy(context);
|
||||
};
|
||||
|
||||
const composedStory = composeStory(Story, meta);
|
||||
await composedStory.play!({ canvasElement: null });
|
||||
const composedStory = composeStory(Story, meta, {
|
||||
mount: (context) => async () => {
|
||||
return context.canvas;
|
||||
},
|
||||
});
|
||||
await composedStory.play({ canvasElement: null });
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
args: {
|
||||
@ -259,48 +263,6 @@ describe('composeStory', () => {
|
||||
expect(spyFn).toHaveBeenNthCalledWith(2, 'from beforeEach');
|
||||
});
|
||||
|
||||
it('should warn when previous cleanups are still around when rendering a story', async () => {
|
||||
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
const cleanupSpy = vi.fn();
|
||||
const beforeEachSpy = vi.fn(() => {
|
||||
return () => {
|
||||
cleanupSpy();
|
||||
};
|
||||
});
|
||||
|
||||
const PreviousStory: Story = {
|
||||
render: () => 'first',
|
||||
beforeEach: beforeEachSpy,
|
||||
};
|
||||
const CurrentStory: Story = {
|
||||
render: () => 'second',
|
||||
args: {
|
||||
firstArg: false,
|
||||
secondArg: true,
|
||||
},
|
||||
};
|
||||
const firstComposedStory = composeStory(PreviousStory, {});
|
||||
await firstComposedStory.load();
|
||||
firstComposedStory();
|
||||
|
||||
expect(beforeEachSpy).toHaveBeenCalled();
|
||||
expect(cleanupSpy).not.toHaveBeenCalled();
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
|
||||
const secondComposedStory = composeStory(CurrentStory, {});
|
||||
secondComposedStory();
|
||||
|
||||
expect(cleanupSpy).not.toHaveBeenCalled();
|
||||
expect(consoleWarnSpy).toHaveBeenCalledOnce();
|
||||
expect(consoleWarnSpy.mock.calls[0][0]).toMatchInlineSnapshot(
|
||||
`
|
||||
"Some stories were not cleaned up before rendering 'Unnamed Story (firstArg, secondArg)'.
|
||||
|
||||
You should load the story with \`await Story.load()\` before rendering it."
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if Story is undefined', () => {
|
||||
expect(() => {
|
||||
// @ts-expect-error (invalid input)
|
||||
|
@ -3,17 +3,21 @@
|
||||
import { type CleanupCallback, isExportStory } from '@storybook/csf';
|
||||
import { dedent } from 'ts-dedent';
|
||||
import type {
|
||||
Renderer,
|
||||
Args,
|
||||
Canvas,
|
||||
ComponentAnnotations,
|
||||
ComposedStoryFn,
|
||||
ComposeStoryFn,
|
||||
LegacyStoryAnnotationsOrFn,
|
||||
NamedOrDefaultProjectAnnotations,
|
||||
ComposeStoryFn,
|
||||
Parameters,
|
||||
PreparedStory,
|
||||
ProjectAnnotations,
|
||||
RenderContext,
|
||||
Renderer,
|
||||
Store_CSFExports,
|
||||
StoryContext,
|
||||
Parameters,
|
||||
StrictArgTypes,
|
||||
ProjectAnnotations,
|
||||
} from '@storybook/core/types';
|
||||
|
||||
import { HooksContext } from '../../../addons';
|
||||
@ -23,7 +27,8 @@ import { normalizeStory } from './normalizeStory';
|
||||
import { normalizeComponentAnnotations } from './normalizeComponentAnnotations';
|
||||
import { getValuesFromArgTypes } from './getValuesFromArgTypes';
|
||||
import { normalizeProjectAnnotations } from './normalizeProjectAnnotations';
|
||||
import type { ComposedStoryFn } from '@storybook/core/types';
|
||||
import { mountDestructured } from '../../../modules/preview-web/render/mount-utils';
|
||||
import { MountMustBeDestructuredError } from '@storybook/core/preview-errors';
|
||||
|
||||
let globalProjectAnnotations: ProjectAnnotations<any> = {};
|
||||
|
||||
@ -48,7 +53,7 @@ export function setProjectAnnotations<TRenderer extends Renderer = Renderer>(
|
||||
globalProjectAnnotations = composeConfigs(annotations.map(extractAnnotation));
|
||||
}
|
||||
|
||||
const cleanups: { storyName: string; callback: CleanupCallback }[] = [];
|
||||
const cleanups: CleanupCallback[] = [];
|
||||
|
||||
export function composeStory<TRenderer extends Renderer = Renderer, TArgs extends Args = Args>(
|
||||
storyAnnotations: LegacyStoryAnnotationsOrFn<TRenderer>,
|
||||
@ -81,8 +86,25 @@ export function composeStory<TRenderer extends Renderer = Renderer, TArgs extend
|
||||
normalizedComponentAnnotations
|
||||
);
|
||||
|
||||
// TODO: Remove this in 9.0
|
||||
// We can only use the renderToCanvas definition of the default config when testingLibraryRender is set
|
||||
// This makes sure, that when the user doesn't do this, and doesn't provide its own renderToCanvas definition,
|
||||
// we fall back to the < 8.1 behavior of the play function.
|
||||
|
||||
const fallback =
|
||||
defaultConfig &&
|
||||
!globalProjectAnnotations?.testingLibraryRender &&
|
||||
!projectAnnotations?.testingLibraryRender;
|
||||
|
||||
const normalizedProjectAnnotations = normalizeProjectAnnotations<TRenderer>(
|
||||
composeConfigs([defaultConfig ?? {}, globalProjectAnnotations, projectAnnotations ?? {}])
|
||||
composeConfigs([
|
||||
{
|
||||
...defaultConfig,
|
||||
renderToCanvas: fallback ? undefined : defaultConfig?.renderToCanvas,
|
||||
},
|
||||
globalProjectAnnotations,
|
||||
projectAnnotations ?? {},
|
||||
])
|
||||
);
|
||||
|
||||
const story = prepareStory<TRenderer>(
|
||||
@ -93,85 +115,114 @@ export function composeStory<TRenderer extends Renderer = Renderer, TArgs extend
|
||||
|
||||
const globalsFromGlobalTypes = getValuesFromArgTypes(normalizedProjectAnnotations.globalTypes);
|
||||
|
||||
const context: StoryContext<TRenderer> = {
|
||||
hooks: new HooksContext(),
|
||||
globals: {
|
||||
...globalsFromGlobalTypes,
|
||||
...normalizedProjectAnnotations.initialGlobals,
|
||||
},
|
||||
args: { ...story.initialArgs },
|
||||
viewMode: 'story',
|
||||
loaded: {},
|
||||
abortSignal: new AbortController().signal,
|
||||
step: (label, play) => story.runStep(label, play, context),
|
||||
canvasElement: globalThis?.document?.body,
|
||||
context: null!,
|
||||
canvas: {},
|
||||
...story,
|
||||
const initializeContext = () => {
|
||||
const context: StoryContext<TRenderer> = prepareContext({
|
||||
hooks: new HooksContext(),
|
||||
globals: {
|
||||
...globalsFromGlobalTypes,
|
||||
...normalizedProjectAnnotations.initialGlobals,
|
||||
},
|
||||
args: { ...story.initialArgs },
|
||||
viewMode: 'story',
|
||||
loaded: {},
|
||||
abortSignal: new AbortController().signal,
|
||||
step: (label, play) => story.runStep(label, play, context),
|
||||
canvasElement: globalThis?.document?.body,
|
||||
canvas: {} as Canvas,
|
||||
...story,
|
||||
context: null!,
|
||||
mount: null!,
|
||||
});
|
||||
|
||||
context.context = context;
|
||||
|
||||
if (story.renderToCanvas) {
|
||||
context.renderToCanvas = async () => {
|
||||
// Consolidate this renderContext with Context in SB 9.0
|
||||
const unmount = await story.renderToCanvas?.(
|
||||
{
|
||||
componentId: story.componentId,
|
||||
title: story.title,
|
||||
id: story.id,
|
||||
name: story.name,
|
||||
tags: story.tags,
|
||||
showError: (error) => {},
|
||||
showException: (error) => {},
|
||||
forceRemount: true,
|
||||
storyContext: context,
|
||||
storyFn: () => story.unboundStoryFn(context),
|
||||
unboundStoryFn: story.unboundStoryFn,
|
||||
} as RenderContext<TRenderer>,
|
||||
context.canvasElement
|
||||
);
|
||||
if (unmount) {
|
||||
cleanups.push(unmount);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
context.mount = story.mount(context);
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
context.context = context;
|
||||
let loadedContext: StoryContext<TRenderer> | undefined;
|
||||
|
||||
const playFunction = story.playFunction
|
||||
? async (extraContext?: Partial<StoryContext<TRenderer, Partial<TArgs>>>) => {
|
||||
Object.assign(context, extraContext);
|
||||
return story.playFunction!(context);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
let previousCleanupsDone = false;
|
||||
// TODO: Remove in 9.0
|
||||
const backwardsCompatiblePlay = async (
|
||||
extraContext?: Partial<StoryContext<TRenderer, Partial<TArgs>>>
|
||||
) => {
|
||||
const context = initializeContext();
|
||||
if (loadedContext) {
|
||||
context.loaded = loadedContext.loaded;
|
||||
}
|
||||
Object.assign(context, extraContext);
|
||||
return story.playFunction!(context);
|
||||
};
|
||||
const newPlay = (extraContext?: Partial<StoryContext<TRenderer, Partial<TArgs>>>) => {
|
||||
const context = initializeContext();
|
||||
Object.assign(context, extraContext);
|
||||
return playStory(story, context);
|
||||
};
|
||||
const playFunction =
|
||||
!story.renderToCanvas && story.playFunction
|
||||
? backwardsCompatiblePlay
|
||||
: !story.renderToCanvas && !story.playFunction
|
||||
? undefined
|
||||
: newPlay;
|
||||
|
||||
const composedStory: ComposedStoryFn<TRenderer, Partial<TArgs>> = Object.assign(
|
||||
function storyFn(extraArgs?: Partial<TArgs>) {
|
||||
const context = initializeContext();
|
||||
if (loadedContext) {
|
||||
context.loaded = loadedContext.loaded;
|
||||
}
|
||||
context.args = {
|
||||
...context.initialArgs,
|
||||
...extraArgs,
|
||||
};
|
||||
|
||||
if (cleanups.length > 0 && !previousCleanupsDone) {
|
||||
let humanReadableIdentifier = storyName;
|
||||
if (story.title !== DEFAULT_STORY_TITLE) {
|
||||
// prefix with title unless it's the generic ComposedStory title
|
||||
humanReadableIdentifier = `${story.title} - ${humanReadableIdentifier}`;
|
||||
}
|
||||
if (storyName === DEFAULT_STORY_NAME && Object.keys(context.args).length > 0) {
|
||||
// suffix with args if it's an unnamed story and there are args
|
||||
humanReadableIdentifier = `${humanReadableIdentifier} (${Object.keys(context.args).join(
|
||||
', '
|
||||
)})`;
|
||||
}
|
||||
console.warn(
|
||||
dedent`Some stories were not cleaned up before rendering '${humanReadableIdentifier}'.
|
||||
|
||||
You should load the story with \`await Story.load()\` before rendering it.`
|
||||
);
|
||||
// TODO: Add a link to the docs when they are ready
|
||||
// eg. "See https://storybook.js.org/docs/api/portable-stories-${process.env.JEST_WORKER_ID !== undefined ? 'jest' : 'vitest'}#3-load for more information."
|
||||
}
|
||||
return story.unboundStoryFn(prepareContext(context));
|
||||
return story.unboundStoryFn(context);
|
||||
},
|
||||
{
|
||||
id: story.id,
|
||||
storyName,
|
||||
load: async () => {
|
||||
// First run any registered cleanup function
|
||||
for (const { callback } of [...cleanups].reverse()) await callback();
|
||||
for (const callback of [...cleanups].reverse()) await callback();
|
||||
cleanups.length = 0;
|
||||
|
||||
previousCleanupsDone = true;
|
||||
const context = initializeContext();
|
||||
|
||||
context.loaded = await story.applyLoaders(context);
|
||||
|
||||
cleanups.push(
|
||||
...(await story.applyBeforeEach(context))
|
||||
.filter(Boolean)
|
||||
.map((callback) => ({ storyName, callback }))
|
||||
);
|
||||
cleanups.push(...(await story.applyBeforeEach(context)).filter(Boolean));
|
||||
|
||||
loadedContext = context;
|
||||
},
|
||||
args: story.initialArgs as Partial<TArgs>,
|
||||
parameters: story.parameters as Parameters,
|
||||
argTypes: story.argTypes as StrictArgTypes<TArgs>,
|
||||
play: playFunction,
|
||||
play: playFunction!,
|
||||
tags: story.tags,
|
||||
}
|
||||
);
|
||||
@ -207,9 +258,12 @@ export function composeStories<TModule extends Store_CSFExports>(
|
||||
type WrappedStoryRef = { __pw_type: 'jsx' | 'importRef' };
|
||||
type UnwrappedJSXStoryRef = {
|
||||
__pw_type: 'jsx';
|
||||
type: ComposedStoryFn;
|
||||
type: UnwrappedImportStoryRef;
|
||||
};
|
||||
type UnwrappedImportStoryRef = ComposedStoryFn & {
|
||||
playPromise?: Promise<void>;
|
||||
renderingEnded?: PromiseWithResolvers<void>;
|
||||
};
|
||||
type UnwrappedImportStoryRef = ComposedStoryFn;
|
||||
|
||||
declare global {
|
||||
function __pwUnwrapObject(
|
||||
@ -268,3 +322,37 @@ export function createPlaywrightTest<TFixture extends { extend: any }>(
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// TODO At some point this function should live in prepareStory and become the core of StoryRender.render as well.
|
||||
// Will make a follow up PR for that
|
||||
async function playStory<TRenderer extends Renderer>(
|
||||
story: PreparedStory<TRenderer>,
|
||||
context: StoryContext<TRenderer>
|
||||
) {
|
||||
for (const callback of [...cleanups].reverse()) await callback();
|
||||
cleanups.length = 0;
|
||||
|
||||
context.loaded = await story.applyLoaders(context);
|
||||
if (context.abortSignal.aborted) return;
|
||||
|
||||
cleanups.push(...(await story.applyBeforeEach(context)).filter(Boolean));
|
||||
|
||||
const playFunction = story.playFunction;
|
||||
|
||||
const isMountDestructured = playFunction && mountDestructured(playFunction);
|
||||
|
||||
if (!isMountDestructured) {
|
||||
await context.mount();
|
||||
}
|
||||
|
||||
if (context.abortSignal.aborted) return;
|
||||
|
||||
if (playFunction) {
|
||||
if (!isMountDestructured) {
|
||||
context.mount = async () => {
|
||||
throw new MountMustBeDestructuredError({ playFunction: playFunction.toString() });
|
||||
};
|
||||
}
|
||||
await playFunction(context);
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ const addExtraContext = (
|
||||
hooks: new HooksContext(),
|
||||
viewMode: 'story' as const,
|
||||
loaded: {},
|
||||
mount: vi.fn(),
|
||||
abortSignal: new AbortController().signal,
|
||||
canvasElement: {},
|
||||
step: vi.fn(),
|
||||
@ -763,6 +764,9 @@ describe('prepareMeta', () => {
|
||||
undecoratedStoryFn,
|
||||
playFunction,
|
||||
runStep,
|
||||
mount,
|
||||
renderToCanvas,
|
||||
testingLibraryRender,
|
||||
...preparedStory
|
||||
} = prepareStory({ id, name, moduleExport }, meta, { render });
|
||||
|
||||
|
@ -26,6 +26,7 @@ import type {
|
||||
NormalizedProjectAnnotations,
|
||||
NormalizedStoryAnnotations,
|
||||
} from '@storybook/core/types';
|
||||
import { mountDestructured } from '../../preview-web/render/mount-utils';
|
||||
|
||||
// Combine all the metadata about a story (both direct and inherited from the component/global scope)
|
||||
// into a "render-able" story function, with all decorators applied, parameters passed as context etc
|
||||
@ -82,7 +83,7 @@ export function prepareStory<TRenderer extends Renderer>(
|
||||
};
|
||||
|
||||
const undecoratedStoryFn = (context: StoryContext<TRenderer>) =>
|
||||
(render as ArgsStoryFn<TRenderer>)(context.args, context);
|
||||
(context.originalStoryFn as ArgsStoryFn<TRenderer>)(context.args, context);
|
||||
|
||||
// Currently it is only possible to set these globally
|
||||
const { applyDecorators = defaultDecorateStory, runStep } = projectAnnotations;
|
||||
@ -100,26 +101,59 @@ export function prepareStory<TRenderer extends Renderer>(
|
||||
storyAnnotations?.render ||
|
||||
componentAnnotations.render ||
|
||||
projectAnnotations.render;
|
||||
if (!render) throw new Error(`No render function available for storyId '${id}'`);
|
||||
|
||||
const decoratedStoryFn = applyHooks<TRenderer>(applyDecorators)(undecoratedStoryFn, decorators);
|
||||
const unboundStoryFn = (context: StoryContext<TRenderer>) => decoratedStoryFn(context);
|
||||
|
||||
const playFunction = storyAnnotations?.play ?? componentAnnotations?.play;
|
||||
|
||||
const mountUsed = mountDestructured(playFunction);
|
||||
|
||||
if (!render && !mountUsed) {
|
||||
// TODO Make this a named error
|
||||
throw new Error(`No render function available for storyId '${id}'`);
|
||||
}
|
||||
|
||||
let { tags } = partialAnnotations;
|
||||
|
||||
if (mountUsed) {
|
||||
// Don't show stories where mount is used in docs.
|
||||
// As the play function is not running in docs, and when mount is used, the mounting is happening in play itself.
|
||||
tags = tags.filter((tag) => tag !== 'autodocs');
|
||||
}
|
||||
|
||||
const defaultMount = (context: StoryContext) => {
|
||||
return async () => {
|
||||
await context.renderToCanvas();
|
||||
return context.canvas;
|
||||
};
|
||||
};
|
||||
|
||||
const mount =
|
||||
storyAnnotations.mount ??
|
||||
componentAnnotations.mount ??
|
||||
projectAnnotations.mount ??
|
||||
defaultMount;
|
||||
|
||||
const testingLibraryRender = projectAnnotations.testingLibraryRender;
|
||||
|
||||
return {
|
||||
...partialAnnotations,
|
||||
tags,
|
||||
moduleExport,
|
||||
id,
|
||||
name,
|
||||
story: name,
|
||||
originalStoryFn: render,
|
||||
originalStoryFn: render!,
|
||||
undecoratedStoryFn,
|
||||
unboundStoryFn,
|
||||
applyLoaders,
|
||||
applyBeforeEach,
|
||||
playFunction,
|
||||
runStep,
|
||||
mount,
|
||||
testingLibraryRender,
|
||||
renderToCanvas: projectAnnotations.renderToCanvas,
|
||||
};
|
||||
}
|
||||
export function prepareMeta<TRenderer extends Renderer>(
|
||||
|
@ -238,6 +238,63 @@ export class StoryStoreAccessedBeforeInitializationError extends StorybookError
|
||||
}
|
||||
}
|
||||
|
||||
export class MountMustBeDestructuredError extends StorybookError {
|
||||
readonly category = Category.PREVIEW_API;
|
||||
|
||||
readonly code = 12;
|
||||
|
||||
constructor(public data: { playFunction: string }) {
|
||||
super();
|
||||
}
|
||||
|
||||
template() {
|
||||
return dedent`
|
||||
To use mount in the play function, you must use object destructuring, e.g. play: ({ mount }) => {}.
|
||||
|
||||
Instead received:
|
||||
${this.data.playFunction}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export class TestingLibraryMustBeConfiguredError extends StorybookError {
|
||||
readonly category = Category.PREVIEW_API;
|
||||
|
||||
readonly code = 13;
|
||||
|
||||
template() {
|
||||
return dedent`
|
||||
You must configure testingLibraryRender to use play in portable stories.
|
||||
|
||||
import { render } from '@testing-library/[renderer]';
|
||||
|
||||
setProjectAnnotations({
|
||||
testingLibraryRender: render,
|
||||
});
|
||||
|
||||
For other testing renderers, you can configure renderToCanvas:
|
||||
|
||||
import { render } from 'your-renderer';
|
||||
|
||||
setProjectAnnotations({
|
||||
renderToCanvas: ({ storyFn }) => {
|
||||
const Story = storyFn();
|
||||
|
||||
// Svelte
|
||||
render(Story.Component, Story.props);
|
||||
|
||||
// Vue
|
||||
render(Story);
|
||||
|
||||
// or for React
|
||||
render(<Story/>);
|
||||
},
|
||||
});
|
||||
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export class NextJsSharpError extends StorybookError {
|
||||
readonly category = Category.FRAMEWORK_NEXTJS;
|
||||
|
||||
|
@ -44,7 +44,7 @@ export type ComposedStoryFn<
|
||||
> = PartialArgsStoryFn<TRenderer, TArgs> & {
|
||||
args: TArgs;
|
||||
id: StoryId;
|
||||
play?: (context?: Partial<StoryContext<TRenderer, Partial<TArgs>>>) => Promise<void>;
|
||||
play: (context?: Partial<StoryContext<TRenderer, Partial<TArgs>>>) => Promise<void>;
|
||||
load: () => Promise<void>;
|
||||
storyName: string;
|
||||
parameters: Parameters;
|
||||
|
@ -11,6 +11,7 @@ export type {
|
||||
ArgTypes,
|
||||
ArgTypesEnhancer,
|
||||
BaseAnnotations,
|
||||
Canvas,
|
||||
ComponentAnnotations,
|
||||
ComponentId,
|
||||
ComponentTitle,
|
||||
|
@ -5,6 +5,7 @@ import type {
|
||||
LoaderFunction,
|
||||
CleanupCallback,
|
||||
StepRunner,
|
||||
Canvas,
|
||||
} from '@storybook/csf';
|
||||
|
||||
import type {
|
||||
@ -43,6 +44,7 @@ export type RenderToCanvas<TRenderer extends Renderer> = (
|
||||
|
||||
export interface ProjectAnnotations<TRenderer extends Renderer>
|
||||
extends CsfProjectAnnotations<TRenderer> {
|
||||
testingLibraryRender?: (...args: never[]) => { unmount: () => void };
|
||||
renderToCanvas?: RenderToCanvas<TRenderer>;
|
||||
/* @deprecated use renderToCanvas */
|
||||
renderToDOM?: RenderToCanvas<TRenderer>;
|
||||
@ -106,6 +108,9 @@ export type PreparedStory<TRenderer extends Renderer = Renderer> =
|
||||
applyBeforeEach: (context: StoryContext<TRenderer>) => Promise<CleanupCallback[]>;
|
||||
playFunction?: (context: StoryContext<TRenderer>) => Promise<void> | void;
|
||||
runStep: StepRunner<TRenderer>;
|
||||
mount: (context: StoryContext<TRenderer>) => () => Promise<Canvas>;
|
||||
testingLibraryRender?: (...args: never[]) => unknown;
|
||||
renderToCanvas?: ProjectAnnotations<TRenderer>['renderToCanvas'];
|
||||
};
|
||||
|
||||
export type PreparedMeta<TRenderer extends Renderer = Renderer> = Omit<
|
||||
@ -119,6 +124,7 @@ export type BoundStory<TRenderer extends Renderer = Renderer> = PreparedStory<TR
|
||||
storyFn: PartialStoryFn<TRenderer>;
|
||||
};
|
||||
|
||||
// TODO Consolidate this with context for 9.0
|
||||
export declare type RenderContext<TRenderer extends Renderer = Renderer> = StoryIdentifier & {
|
||||
showMain: () => void;
|
||||
showError: (error: { title: string; description: string }) => void;
|
||||
|
@ -27,6 +27,7 @@ const defaultContext: Addon_StoryContext<AngularRenderer> = {
|
||||
step: undefined,
|
||||
context: undefined,
|
||||
canvas: undefined,
|
||||
mount: undefined,
|
||||
};
|
||||
|
||||
defaultContext.context = defaultContext;
|
||||
|
@ -44,7 +44,7 @@
|
||||
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.10",
|
||||
"@storybook/csf": "0.1.11",
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/icons": "^1.2.5",
|
||||
"@types/lodash": "^4.14.167",
|
||||
|
@ -95,7 +95,10 @@ type BaseTemplates = Template & {
|
||||
const baseTemplates = {
|
||||
'cra/default-js': {
|
||||
name: 'Create React App Latest (Webpack | JavaScript)',
|
||||
script: 'npx create-react-app {{beforeDir}}',
|
||||
script: `
|
||||
npx create-react-app {{beforeDir}} && cd {{beforeDir}} && \
|
||||
jq '.browserslist.production[0] = ">0.9%"' package.json > tmp.json && mv tmp.json package.json
|
||||
`,
|
||||
expected: {
|
||||
// TODO: change this to @storybook/cra once that package is created
|
||||
framework: '@storybook/react-webpack5',
|
||||
@ -120,7 +123,10 @@ const baseTemplates = {
|
||||
},
|
||||
'cra/default-ts': {
|
||||
name: 'Create React App Latest (Webpack | TypeScript)',
|
||||
script: 'npx create-react-app {{beforeDir}} --template typescript',
|
||||
script: `
|
||||
npx create-react-app {{beforeDir}} --template typescript && cd {{beforeDir}} && \
|
||||
jq '.browserslist.production[0] = ">0.9%"' package.json > tmp.json && mv tmp.json package.json
|
||||
`,
|
||||
// Re-enable once https://github.com/storybookjs/storybook/issues/19351 is fixed.
|
||||
skipTasks: ['smoke-test', 'bench'],
|
||||
expected: {
|
||||
|
@ -58,7 +58,7 @@
|
||||
"@babel/preset-env": "^7.24.4",
|
||||
"@babel/types": "^7.24.0",
|
||||
"@storybook/core": "workspace:*",
|
||||
"@storybook/csf": "0.1.10",
|
||||
"@storybook/csf": "0.1.11",
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"globby": "^14.0.1",
|
||||
|
@ -45,7 +45,7 @@
|
||||
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.10",
|
||||
"@storybook/csf": "0.1.11",
|
||||
"estraverse": "^5.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prettier": "^3.1.1"
|
||||
|
@ -44,7 +44,7 @@
|
||||
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.10",
|
||||
"@storybook/csf": "0.1.11",
|
||||
"@storybook/instrumenter": "workspace:*",
|
||||
"@testing-library/dom": "10.1.0",
|
||||
"@testing-library/jest-dom": "6.4.5",
|
||||
|
65
code/lib/test/template/stories/mount-in-play.stories.ts
Normal file
65
code/lib/test/template/stories/mount-in-play.stories.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { expect, mocked, getByRole, spyOn, userEvent, fn } from '@storybook/test';
|
||||
|
||||
const meta = {
|
||||
component: globalThis.Components.Button,
|
||||
loaders() {
|
||||
spyOn(console, 'log').mockName('console.log');
|
||||
console.log('1 - [from loaders]');
|
||||
},
|
||||
beforeEach() {
|
||||
console.log('2 - [from meta beforeEach]');
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const MountInPlay = {
|
||||
beforeEach() {
|
||||
console.log('3 - [from story beforeEach]');
|
||||
},
|
||||
decorators: (storyFn) => {
|
||||
console.log('5 - [from decorator]');
|
||||
return storyFn();
|
||||
},
|
||||
args: {
|
||||
label: 'Button',
|
||||
onClick: () => {
|
||||
console.log('7 - [from onClick]');
|
||||
},
|
||||
},
|
||||
async play({ mount, canvasElement }) {
|
||||
console.log('4 - [before mount]');
|
||||
await mount();
|
||||
console.log('6 - [after mount]');
|
||||
await userEvent.click(getByRole(canvasElement, 'button'));
|
||||
await expect(mocked(console.log).mock.calls).toEqual([
|
||||
['1 - [from loaders]'],
|
||||
['2 - [from meta beforeEach]'],
|
||||
['3 - [from story beforeEach]'],
|
||||
['4 - [before mount]'],
|
||||
['5 - [from decorator]'],
|
||||
['6 - [after mount]'],
|
||||
['7 - [from onClick]'],
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const MountShouldBeDestructured = {
|
||||
parameters: { chromatic: { disable: true } },
|
||||
args: {
|
||||
label: 'Button',
|
||||
onClick: fn(),
|
||||
},
|
||||
async play(context) {
|
||||
let error;
|
||||
|
||||
// TODO use expect.toThrow once this issue is fixed
|
||||
// https://github.com/storybookjs/storybook/issues/28406
|
||||
try {
|
||||
await context.mount();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
await expect(error?.name).toContain('MountMustBeDestructuredError');
|
||||
},
|
||||
};
|
@ -118,7 +118,7 @@
|
||||
"@storybook/codemod": "workspace:*",
|
||||
"@storybook/core": "workspace:*",
|
||||
"@storybook/core-webpack": "workspace:*",
|
||||
"@storybook/csf": "0.1.10",
|
||||
"@storybook/csf": "0.1.11",
|
||||
"@storybook/csf-plugin": "workspace:*",
|
||||
"@storybook/ember": "workspace:*",
|
||||
"@storybook/eslint-config-storybook": "^4.0.0",
|
||||
|
@ -6,6 +6,7 @@ import type { ButtonProps } from './Button';
|
||||
import { Button } from './Button';
|
||||
import type { HandlerFunction } from '@storybook/addon-actions';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { mocked } from '@storybook/test';
|
||||
|
||||
const meta = {
|
||||
title: 'Example/Button',
|
||||
@ -127,6 +128,30 @@ export const LoaderStory: CSF3Story<{ mockFn: (val: string) => string }> = {
|
||||
},
|
||||
};
|
||||
|
||||
export const MountInPlayFunction: CSF3Story<{ mockFn: (val: string) => string }> = {
|
||||
args: {
|
||||
mockFn: fn(),
|
||||
},
|
||||
play: async ({ args, mount, context }) => {
|
||||
// equivalent of loaders
|
||||
const loadedData = await Promise.resolve('loaded data');
|
||||
mocked(args.mockFn).mockReturnValueOnce('mockFn return value');
|
||||
// equivalent of render
|
||||
const data = args.mockFn('render');
|
||||
// TODO refactor this in the mount args PR
|
||||
context.originalStoryFn = () => (
|
||||
<div>
|
||||
<div data-testid="loaded-data">{loadedData}</div>
|
||||
<div data-testid="spy-data">{String(data)}</div>
|
||||
</div>
|
||||
);
|
||||
await mount();
|
||||
|
||||
// equivalent of play
|
||||
expect(args.mockFn).toHaveBeenCalledWith('render');
|
||||
},
|
||||
};
|
||||
|
||||
export const WithActionArg: CSF3Story<{ someActionArg: HandlerFunction }> = {
|
||||
args: {
|
||||
someActionArg: action('some-action-arg'),
|
||||
|
@ -28,14 +28,14 @@ export interface ButtonProps {
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export const Button: React.FC<ButtonProps> = (props) => {
|
||||
const { primary = false, size = 'medium', backgroundColor, children, ...otherProps } = props;
|
||||
const { primary = false, size = 'medium', backgroundColor, children, onClick } = props;
|
||||
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
|
||||
style={{ backgroundColor }}
|
||||
{...otherProps}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
@ -0,0 +1,120 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Legacy Portable Stories API > Renders CSF2Secondary story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
type="button"
|
||||
>
|
||||
Children coming from story args!
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Legacy Portable Stories API > Renders CSF2StoryWithParamsAndDecorator story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Legacy Portable Stories API > Renders CSF3Button story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Legacy Portable Stories API > Renders CSF3ButtonWithRender story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div>
|
||||
<p
|
||||
data-testid="custom-render"
|
||||
>
|
||||
I am a custom render function
|
||||
</p>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Legacy Portable Stories API > Renders CSF3InputFieldFilled story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<input
|
||||
data-testid="input"
|
||||
/>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Legacy Portable Stories API > Renders CSF3Primary story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<button
|
||||
class="storybook-button storybook-button--large storybook-button--primary"
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Legacy Portable Stories API > Renders LoaderStory story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
data-testid="loaded-data"
|
||||
>
|
||||
loaded data
|
||||
</div>
|
||||
<div
|
||||
data-testid="spy-data"
|
||||
>
|
||||
mockFn return value
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Legacy Portable Stories API > Renders WithActionArg story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<button />
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Legacy Portable Stories API > Renders WithActionArgType story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div>
|
||||
nothing
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
@ -101,6 +101,25 @@ exports[`Renders LoaderStory story 1`] = `
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders MountInPlayFunction story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
data-testid="loaded-data"
|
||||
>
|
||||
loaded data
|
||||
</div>
|
||||
<div
|
||||
data-testid="spy-data"
|
||||
>
|
||||
mockFn return value
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders WithActionArg story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
|
@ -0,0 +1,209 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
/* eslint-disable import/namespace */
|
||||
import React from 'react';
|
||||
import { vi, it, expect, afterEach, describe } from 'vitest';
|
||||
import { render, screen, cleanup } from '@testing-library/react';
|
||||
import { addons } from '@storybook/preview-api';
|
||||
|
||||
import * as addonActionsPreview from '@storybook/addon-actions/preview';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { expectTypeOf } from 'expect-type';
|
||||
|
||||
import { setProjectAnnotations, composeStories, composeStory } from '..';
|
||||
import type { Button } from './Button';
|
||||
import * as stories from './Button.stories';
|
||||
|
||||
// TODO: Potentially remove this in Storybook 9.0 once we fully move users to the new portable stories API
|
||||
describe('Legacy Portable Stories API', () => {
|
||||
// example with composeStories, returns an object with all stories composed with args/decorators
|
||||
const { CSF3Primary, LoaderStory } = composeStories(stories);
|
||||
|
||||
// example with composeStory, returns a single story composed with args/decorators
|
||||
const Secondary = composeStory(stories.CSF2Secondary, stories.default);
|
||||
describe('renders', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders primary button', () => {
|
||||
render(<CSF3Primary>Hello world</CSF3Primary>);
|
||||
const buttonElement = screen.getByText(/Hello world/i);
|
||||
expect(buttonElement).not.toBeNull();
|
||||
});
|
||||
|
||||
it('reuses args from composed story', () => {
|
||||
render(<Secondary />);
|
||||
const buttonElement = screen.getByRole('button');
|
||||
expect(buttonElement.textContent).toEqual(Secondary.args.children);
|
||||
});
|
||||
|
||||
it('onclick handler is called', async () => {
|
||||
const onClickSpy = vi.fn();
|
||||
render(<Secondary onClick={onClickSpy} />);
|
||||
const buttonElement = screen.getByRole('button');
|
||||
buttonElement.click();
|
||||
expect(onClickSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reuses args from composeStories', () => {
|
||||
const { getByText } = render(<CSF3Primary />);
|
||||
const buttonElement = getByText(/foo/i);
|
||||
expect(buttonElement).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should call and compose loaders data', async () => {
|
||||
await LoaderStory.load();
|
||||
const { getByTestId } = render(<LoaderStory />);
|
||||
expect(getByTestId('spy-data').textContent).toEqual('mockFn return value');
|
||||
expect(getByTestId('loaded-data').textContent).toEqual('loaded data');
|
||||
// spy assertions happen in the play function and should work
|
||||
await LoaderStory.play!();
|
||||
});
|
||||
});
|
||||
|
||||
describe('projectAnnotations', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders with default projectAnnotations', () => {
|
||||
setProjectAnnotations([
|
||||
{
|
||||
parameters: { injected: true },
|
||||
globalTypes: {
|
||||
locale: { defaultValue: 'en' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
const WithEnglishText = composeStory(stories.CSF2StoryWithLocale, stories.default);
|
||||
const { getByText } = render(<WithEnglishText />);
|
||||
const buttonElement = getByText('Hello!');
|
||||
expect(buttonElement).not.toBeNull();
|
||||
expect(WithEnglishText.parameters?.injected).toBe(true);
|
||||
});
|
||||
|
||||
it('renders with custom projectAnnotations via composeStory params', () => {
|
||||
const WithPortugueseText = composeStory(stories.CSF2StoryWithLocale, stories.default, {
|
||||
initialGlobals: { locale: 'pt' },
|
||||
});
|
||||
const { getByText } = render(<WithPortugueseText />);
|
||||
const buttonElement = getByText('Olá!');
|
||||
expect(buttonElement).not.toBeNull();
|
||||
});
|
||||
|
||||
it('explicit action are spies when the test loader is loaded', async () => {
|
||||
const Story = composeStory(stories.WithActionArg, stories.default);
|
||||
await Story.load();
|
||||
expect(vi.mocked(Story.args.someActionArg!).mock).toBeDefined();
|
||||
|
||||
const { container } = render(<Story />);
|
||||
expect(Story.args.someActionArg).toHaveBeenCalledOnce();
|
||||
expect(Story.args.someActionArg).toHaveBeenCalledWith('in render');
|
||||
|
||||
await Story.play!({ canvasElement: container });
|
||||
expect(Story.args.someActionArg).toHaveBeenCalledTimes(2);
|
||||
expect(Story.args.someActionArg).toHaveBeenCalledWith('on click');
|
||||
});
|
||||
|
||||
it('has action arg from argTypes when addon-actions annotations are added', () => {
|
||||
//@ts-expect-error our tsconfig.jsn#moduleResulution is set to 'node', which doesn't support this import
|
||||
const Story = composeStory(stories.WithActionArgType, stories.default, addonActionsPreview);
|
||||
expect(Story.args.someActionArg).toHaveProperty('isAction', true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CSF3', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders with inferred globalRender', () => {
|
||||
const Primary = composeStory(stories.CSF3Button, stories.default);
|
||||
|
||||
render(<Primary>Hello world</Primary>);
|
||||
const buttonElement = screen.getByText(/Hello world/i);
|
||||
expect(buttonElement).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders with custom render function', () => {
|
||||
const Primary = composeStory(stories.CSF3ButtonWithRender, stories.default);
|
||||
|
||||
render(<Primary />);
|
||||
expect(screen.getByTestId('custom-render')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders with play function without canvas element', async () => {
|
||||
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
|
||||
|
||||
render(<CSF3InputFieldFilled />);
|
||||
|
||||
await CSF3InputFieldFilled.play!();
|
||||
|
||||
const input = screen.getByTestId('input') as HTMLInputElement;
|
||||
expect(input.value).toEqual('Hello world!');
|
||||
});
|
||||
|
||||
it('renders with play function with canvas element', async () => {
|
||||
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
|
||||
|
||||
const { container } = render(<CSF3InputFieldFilled />);
|
||||
|
||||
await CSF3InputFieldFilled.play!({ canvasElement: container });
|
||||
|
||||
const input = screen.getByTestId('input') as HTMLInputElement;
|
||||
expect(input.value).toEqual('Hello world!');
|
||||
});
|
||||
});
|
||||
|
||||
// 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>Hello world</PrimaryWithChannels>);
|
||||
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', () => {
|
||||
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>();
|
||||
});
|
||||
});
|
||||
|
||||
// Batch snapshot testing
|
||||
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();
|
||||
|
||||
if (_storyName === 'CSF2StoryWithLocale' || _storyName === 'MountInPlayFunction') {
|
||||
return;
|
||||
}
|
||||
|
||||
await Story.load();
|
||||
|
||||
const { baseElement } = await render(<Story />);
|
||||
|
||||
await Story.play?.();
|
||||
expect(baseElement).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -14,16 +14,18 @@ import { setProjectAnnotations, composeStories, composeStory } from '..';
|
||||
import type { Button } from './Button';
|
||||
import * as stories from './Button.stories';
|
||||
|
||||
setProjectAnnotations([{ testingLibraryRender: render }]);
|
||||
|
||||
// example with composeStories, returns an object with all stories composed with args/decorators
|
||||
const { CSF3Primary, LoaderStory } = composeStories(stories);
|
||||
const { CSF3Primary, LoaderStory, MountInPlayFunction } = composeStories(stories);
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// example with composeStory, returns a single story composed with args/decorators
|
||||
const Secondary = composeStory(stories.CSF2Secondary, stories.default);
|
||||
describe('renders', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders primary button', () => {
|
||||
render(<CSF3Primary>Hello world</CSF3Primary>);
|
||||
const buttonElement = screen.getByText(/Hello world/i);
|
||||
@ -50,6 +52,13 @@ describe('renders', () => {
|
||||
expect(buttonElement).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should render component mounted in play function', async () => {
|
||||
await MountInPlayFunction.play();
|
||||
|
||||
expect(screen.getByTestId('spy-data').textContent).toEqual('mockFn return value');
|
||||
expect(screen.getByTestId('loaded-data').textContent).toEqual('loaded data');
|
||||
});
|
||||
|
||||
it('should call and compose loaders data', async () => {
|
||||
await LoaderStory.load();
|
||||
const { getByTestId } = render(<LoaderStory />);
|
||||
@ -68,6 +77,7 @@ describe('projectAnnotations', () => {
|
||||
it('renders with default projectAnnotations', () => {
|
||||
setProjectAnnotations([
|
||||
{
|
||||
testingLibraryRender: render,
|
||||
parameters: { injected: true },
|
||||
globalTypes: {
|
||||
locale: { defaultValue: 'en' },
|
||||
@ -90,20 +100,6 @@ describe('projectAnnotations', () => {
|
||||
expect(buttonElement).not.toBeNull();
|
||||
});
|
||||
|
||||
it('explicit action are spies when the test loader is loaded', async () => {
|
||||
const Story = composeStory(stories.WithActionArg, stories.default);
|
||||
await Story.load();
|
||||
expect(vi.mocked(Story.args.someActionArg!).mock).toBeDefined();
|
||||
|
||||
const { container } = render(<Story />);
|
||||
expect(Story.args.someActionArg).toHaveBeenCalledOnce();
|
||||
expect(Story.args.someActionArg).toHaveBeenCalledWith('in render');
|
||||
|
||||
await Story.play!({ canvasElement: container });
|
||||
expect(Story.args.someActionArg).toHaveBeenCalledTimes(2);
|
||||
expect(Story.args.someActionArg).toHaveBeenCalledWith('on click');
|
||||
});
|
||||
|
||||
it('has action arg from argTypes when addon-actions annotations are added', () => {
|
||||
//@ts-expect-error our tsconfig.jsn#moduleResulution is set to 'node', which doesn't support this import
|
||||
const Story = composeStory(stories.WithActionArgType, stories.default, addonActionsPreview);
|
||||
@ -112,10 +108,6 @@ describe('projectAnnotations', () => {
|
||||
});
|
||||
|
||||
describe('CSF3', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders with inferred globalRender', () => {
|
||||
const Primary = composeStory(stories.CSF3Button, stories.default);
|
||||
|
||||
@ -133,10 +125,7 @@ describe('CSF3', () => {
|
||||
|
||||
it('renders with play function without canvas element', async () => {
|
||||
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
|
||||
|
||||
render(<CSF3InputFieldFilled />);
|
||||
|
||||
await CSF3InputFieldFilled.play!();
|
||||
await CSF3InputFieldFilled.play();
|
||||
|
||||
const input = screen.getByTestId('input') as HTMLInputElement;
|
||||
expect(input.value).toEqual('Hello world!');
|
||||
@ -145,12 +134,16 @@ describe('CSF3', () => {
|
||||
it('renders with play function with canvas element', async () => {
|
||||
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
|
||||
|
||||
const { container } = render(<CSF3InputFieldFilled />);
|
||||
const div = document.createElement('div');
|
||||
console.log(div.tagName);
|
||||
document.body.appendChild(div);
|
||||
|
||||
await CSF3InputFieldFilled.play!({ canvasElement: container });
|
||||
await CSF3InputFieldFilled.play({ canvasElement: div });
|
||||
|
||||
const input = screen.getByTestId('input') as HTMLInputElement;
|
||||
expect(input.value).toEqual('Hello world!');
|
||||
|
||||
document.body.removeChild(div);
|
||||
});
|
||||
});
|
||||
|
||||
@ -191,16 +184,7 @@ 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();
|
||||
|
||||
if (_storyName === 'CSF2StoryWithLocale') {
|
||||
return;
|
||||
}
|
||||
|
||||
await Story.load();
|
||||
|
||||
const { baseElement } = await render(<Story />);
|
||||
|
||||
await Story.play?.();
|
||||
expect(baseElement).toMatchSnapshot();
|
||||
if (_storyName === 'CSF2StoryWithLocale') return;
|
||||
await Story.play();
|
||||
expect(document.body).toMatchSnapshot();
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
export const parameters: {} = { renderer: 'react' };
|
||||
export { render } from './render';
|
||||
export { renderToCanvas } from './renderToCanvas';
|
||||
export { mount } from './mount';
|
||||
|
11
code/renderers/react/src/mount.ts
Normal file
11
code/renderers/react/src/mount.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { type StoryContext, type ReactRenderer } from './public-types';
|
||||
import type { BaseAnnotations } from '@storybook/types';
|
||||
|
||||
export const mount: BaseAnnotations<ReactRenderer>['mount'] =
|
||||
(context: StoryContext) => async (ui) => {
|
||||
if (ui != null) {
|
||||
context.originalStoryFn = () => ui;
|
||||
}
|
||||
await context.renderToCanvas();
|
||||
return context.canvas;
|
||||
};
|
@ -15,6 +15,8 @@ import type {
|
||||
import * as reactProjectAnnotations from './entry-preview';
|
||||
import type { Meta } from './public-types';
|
||||
import type { ReactRenderer } from './types';
|
||||
import { TestingLibraryMustBeConfiguredError } from 'storybook/internal/preview-errors';
|
||||
import React from 'react';
|
||||
|
||||
/** Function that sets the globalConfig of your storybook. The global config is the preview module of your .storybook folder.
|
||||
*
|
||||
@ -40,8 +42,16 @@ export function setProjectAnnotations(
|
||||
}
|
||||
|
||||
// This will not be necessary once we have auto preset loading
|
||||
export const INTERNAL_DEFAULT_PROJECT_ANNOTATIONS: ProjectAnnotations<ReactRenderer> =
|
||||
reactProjectAnnotations;
|
||||
export const INTERNAL_DEFAULT_PROJECT_ANNOTATIONS: ProjectAnnotations<ReactRenderer> = {
|
||||
...reactProjectAnnotations,
|
||||
renderToCanvas: ({
|
||||
storyContext: { context, unboundStoryFn: Story, testingLibraryRender: render, canvasElement },
|
||||
}) => {
|
||||
if (render == null) throw new TestingLibraryMustBeConfiguredError();
|
||||
const { unmount } = render(<Story {...context} />, { baseElement: context.canvasElement });
|
||||
return unmount;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that will receive a story along with meta (e.g. a default export from a .stories file)
|
@ -15,6 +15,7 @@ import { fn } from '@storybook/test';
|
||||
|
||||
import type { Decorator, Meta, StoryObj } from './public-types';
|
||||
import type { ReactRenderer } from './types';
|
||||
import type { Canvas } from '@storybook/csf';
|
||||
|
||||
type ReactStory<TArgs, TRequiredArgs> = StoryAnnotations<ReactRenderer, TArgs, TRequiredArgs>;
|
||||
|
||||
@ -315,7 +316,9 @@ it('Infer mock function given to args in meta.', () => {
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const Basic: Story = {
|
||||
play: async ({ args }) => {
|
||||
play: async ({ args, mount }) => {
|
||||
const canvas = await mount(<TestButton {...args} />);
|
||||
expectTypeOf(canvas).toEqualTypeOf<Canvas>();
|
||||
expectTypeOf(args.onClick).toEqualTypeOf<Mock<[], void>>();
|
||||
expectTypeOf(args.onRender).toEqualTypeOf<() => JSX.Element>();
|
||||
},
|
||||
|
@ -1,11 +1,12 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import type { WebRenderer } from 'storybook/internal/types';
|
||||
import type { Canvas, WebRenderer } from 'storybook/internal/types';
|
||||
|
||||
export type { RenderContext, StoryContext } from 'storybook/internal/types';
|
||||
|
||||
export interface ReactRenderer extends WebRenderer {
|
||||
component: ComponentType<this['T']>;
|
||||
storyResult: StoryFnReactReturnType;
|
||||
mount: (ui?: JSX.Element) => Promise<Canvas>;
|
||||
}
|
||||
|
||||
export interface ShowErrorArgs {
|
||||
|
@ -0,0 +1,20 @@
|
||||
import type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import type { StoryObj } from '@storybook/react';
|
||||
|
||||
const Button: FC<{ label?: string; disabled?: boolean }> = (props) => {
|
||||
return <button disabled={props.disabled}>{props.label}</button>;
|
||||
};
|
||||
|
||||
export default {
|
||||
component: Button,
|
||||
};
|
||||
|
||||
export const Basic: StoryObj<typeof Button> = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
async play({ mount, args }) {
|
||||
await mount(<Button {...args} label={'set in play'} />);
|
||||
},
|
||||
};
|
@ -46,7 +46,7 @@
|
||||
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/csf": "0.1.10",
|
||||
"@storybook/csf": "0.1.11",
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"fs-extra": "^11.1.0",
|
||||
|
@ -2,54 +2,29 @@
|
||||
|
||||
exports[`Renders CSF2Secondary story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
label coming from story args!
|
||||
<!---->
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
label coming from story args!
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
|
||||
</div>
|
||||
</button>
|
||||
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
data-testid="local-decorator"
|
||||
style="margin: 3em;"
|
||||
>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
<!---->
|
||||
</button>
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF3Button story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
|
||||
|
||||
|
||||
<div
|
||||
data-testid="local-decorator"
|
||||
style="margin: 3em;"
|
||||
>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
@ -60,59 +35,139 @@ exports[`Renders CSF3Button story 1`] = `
|
||||
</button>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF3Button story 1`] = `
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
<!---->
|
||||
</button>
|
||||
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF3ButtonWithRender story 1`] = `
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<p
|
||||
data-testid="custom-render"
|
||||
>
|
||||
I am a custom render function
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
<!---->
|
||||
</button>
|
||||
<p
|
||||
data-testid="custom-render"
|
||||
>
|
||||
I am a custom render function
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
<!---->
|
||||
</div>
|
||||
</button>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF3InputFieldFilled story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<input
|
||||
data-testid="input"
|
||||
formaction="http://localhost:3000/"
|
||||
formmethod=""
|
||||
/>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<input
|
||||
data-testid="input"
|
||||
formaction="http://localhost:3000/"
|
||||
formmethod=""
|
||||
/>
|
||||
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF3Primary story 1`] = `
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<button
|
||||
class="storybook-button storybook-button--large storybook-button--primary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
<!---->
|
||||
</button>
|
||||
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders LoaderStory story 1`] = `
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div>
|
||||
<div
|
||||
data-testid="loaded-data"
|
||||
>
|
||||
loaded data
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="spy-data"
|
||||
>
|
||||
mockFn return value
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders NewStory story 1`] = `
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
|
||||
<div
|
||||
data-testid="local-decorator"
|
||||
style="margin: 3em;"
|
||||
>
|
||||
<button
|
||||
class="storybook-button storybook-button--large storybook-button--primary"
|
||||
style=""
|
||||
@ -123,60 +178,9 @@ exports[`Renders CSF3Primary story 1`] = `
|
||||
</button>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders LoaderStory story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
data-testid="loaded-data"
|
||||
>
|
||||
loaded data
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="spy-data"
|
||||
>
|
||||
mockFn return value
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders NewStory story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
data-testid="local-decorator"
|
||||
style="margin: 3em;"
|
||||
>
|
||||
<button
|
||||
class="storybook-button storybook-button--large storybook-button--primary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
<!---->
|
||||
</button>
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
|
||||
</div>
|
||||
<!---->
|
||||
<!---->
|
||||
|
||||
</body>
|
||||
`;
|
||||
|
@ -11,16 +11,18 @@ import * as stories from './Button.stories';
|
||||
import type Button from './Button.svelte';
|
||||
import { composeStories, composeStory, setProjectAnnotations } from '../../portable-stories';
|
||||
|
||||
setProjectAnnotations({ testingLibraryRender: render });
|
||||
|
||||
// example with composeStories, returns an object with all stories composed with args/decorators
|
||||
const { CSF3Primary, LoaderStory } = composeStories(stories);
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// example with composeStory, returns a single story composed with args/decorators
|
||||
const Secondary = composeStory(stories.CSF2Secondary, stories.default);
|
||||
describe('renders', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders primary button with custom props via composeStory', () => {
|
||||
// We unfortunately can't do the following:
|
||||
// render(CSF3Primary.Component, { ...CSF3Primary.props, label: 'Hello world' });
|
||||
@ -73,10 +75,6 @@ describe('renders', () => {
|
||||
});
|
||||
|
||||
describe('projectAnnotations', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders with default projectAnnotations', () => {
|
||||
setProjectAnnotations([
|
||||
{
|
||||
@ -84,6 +82,7 @@ describe('projectAnnotations', () => {
|
||||
globalTypes: {
|
||||
locale: { defaultValue: 'en' },
|
||||
},
|
||||
testingLibraryRender: render,
|
||||
},
|
||||
]);
|
||||
const WithEnglishText = composeStory(stories.CSF2StoryWithLocale, stories.default);
|
||||
@ -104,10 +103,6 @@ describe('projectAnnotations', () => {
|
||||
});
|
||||
|
||||
describe('CSF3', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders with inferred globalRender', () => {
|
||||
const Primary = composeStory(stories.CSF3Button, stories.default);
|
||||
render(Primary.Component, Primary.props);
|
||||
@ -125,9 +120,7 @@ describe('CSF3', () => {
|
||||
it('renders with play function without canvas element', async () => {
|
||||
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
|
||||
|
||||
render(CSF3InputFieldFilled.Component, CSF3InputFieldFilled.props);
|
||||
|
||||
await CSF3InputFieldFilled.play!();
|
||||
await CSF3InputFieldFilled.play();
|
||||
|
||||
const input = screen.getByTestId('input') as HTMLInputElement;
|
||||
expect(input.value).toEqual('Hello world!');
|
||||
@ -136,12 +129,15 @@ describe('CSF3', () => {
|
||||
it('renders with play function with canvas element', async () => {
|
||||
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
|
||||
|
||||
const { container } = render(CSF3InputFieldFilled.Component, CSF3InputFieldFilled.props);
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
|
||||
await CSF3InputFieldFilled.play!({ canvasElement: container });
|
||||
await CSF3InputFieldFilled.play({ canvasElement: div });
|
||||
|
||||
const input = screen.getByTestId('input') as HTMLInputElement;
|
||||
expect(input.value).toEqual('Hello world!');
|
||||
|
||||
document.body.removeChild(div);
|
||||
});
|
||||
});
|
||||
|
||||
@ -174,16 +170,7 @@ 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();
|
||||
|
||||
if (_storyName === 'CSF2StoryWithLocale') {
|
||||
return;
|
||||
}
|
||||
|
||||
await Story.load();
|
||||
|
||||
const { container } = await render(Story.Component, Story.props);
|
||||
|
||||
await Story.play?.({ canvasElement: container });
|
||||
expect(container).toMatchSnapshot();
|
||||
if (_storyName === 'CSF2StoryWithLocale') return;
|
||||
await Story.play();
|
||||
expect(document.body).toMatchSnapshot();
|
||||
});
|
||||
|
@ -2,7 +2,6 @@
|
||||
import SlotDecorator from './SlotDecorator.svelte';
|
||||
import { dedent } from 'ts-dedent';
|
||||
|
||||
export let svelteVersion;
|
||||
export let name;
|
||||
export let title;
|
||||
export let storyFn;
|
||||
@ -56,4 +55,4 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<SlotDecorator {svelteVersion} {Component} {props} on={{ ...eventsFromArgTypes, ...on }} />
|
||||
<SlotDecorator {Component} {props} on={{ ...eventsFromArgTypes, ...on }} />
|
||||
|
@ -1,15 +1,17 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { VERSION } from 'svelte/compiler';
|
||||
|
||||
export let svelteVersion;
|
||||
export let decorator = undefined;
|
||||
export let Component;
|
||||
export let props = {};
|
||||
export let on = undefined;
|
||||
|
||||
|
||||
let instance;
|
||||
let decoratorInstance;
|
||||
|
||||
|
||||
const svelteVersion = VERSION[0];
|
||||
|
||||
if (on && svelteVersion < 5) {
|
||||
// Attach Svelte event listeners in Svelte v4
|
||||
// In Svelte v5 this is not possible anymore as instances are no longer classes with $on() properties, so it will be a no-op
|
||||
|
@ -2,3 +2,4 @@ export const parameters: {} = { renderer: 'svelte' };
|
||||
|
||||
export { render, renderToCanvas } from './render';
|
||||
export { decorateStory as applyDecorators } from './decorators';
|
||||
export { mount } from './mount';
|
||||
|
15
code/renderers/svelte/src/mount.ts
Normal file
15
code/renderers/svelte/src/mount.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { type StoryContext, type SvelteRenderer } from './public-types';
|
||||
import { type BaseAnnotations } from '@storybook/types';
|
||||
|
||||
export const mount: BaseAnnotations<SvelteRenderer>['mount'] = (context: StoryContext) => {
|
||||
return async (Component, options) => {
|
||||
if (Component) {
|
||||
context.originalStoryFn = () => ({
|
||||
Component,
|
||||
props: options && 'props' in options ? options?.props : options,
|
||||
});
|
||||
}
|
||||
await context.renderToCanvas();
|
||||
return context.canvas;
|
||||
};
|
||||
};
|
@ -19,6 +19,7 @@ import PreviewRender from '@storybook/svelte/internal/PreviewRender.svelte';
|
||||
// @ts-expect-error Don't know why TS doesn't pick up the types export here
|
||||
import { createSvelte5Props } from '@storybook/svelte/internal/createSvelte5Props';
|
||||
import { IS_SVELTE_V4 } from './utils';
|
||||
import { TestingLibraryMustBeConfiguredError } from 'storybook/internal/preview-errors';
|
||||
|
||||
type ComposedStory<TArgs extends Args = any> = ComposedStoryFn<SvelteRenderer, TArgs> & {
|
||||
Component: typeof PreviewRender;
|
||||
@ -57,8 +58,15 @@ export function setProjectAnnotations(
|
||||
}
|
||||
|
||||
// This will not be necessary once we have auto preset loading
|
||||
export const INTERNAL_DEFAULT_PROJECT_ANNOTATIONS: ProjectAnnotations<SvelteRenderer> =
|
||||
svelteProjectAnnotations;
|
||||
export const INTERNAL_DEFAULT_PROJECT_ANNOTATIONS: ProjectAnnotations<SvelteRenderer> = {
|
||||
...svelteProjectAnnotations,
|
||||
renderToCanvas: ({ storyFn, storyContext: { testingLibraryRender: render, canvasElement } }) => {
|
||||
if (render == null) throw new TestingLibraryMustBeConfiguredError();
|
||||
const { Component, props } = storyFn();
|
||||
const { unmount } = render(Component, { props, target: canvasElement });
|
||||
return unmount;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that will receive a story along with meta (e.g. a default export from a .stories file)
|
||||
|
@ -231,3 +231,13 @@ describe('Story args can be inferred', () => {
|
||||
expectTypeOf(Basic).toEqualTypeOf<Expected>();
|
||||
});
|
||||
});
|
||||
|
||||
it('mount accepts a Component and props', () => {
|
||||
const Basic: StoryObj<Button> = {
|
||||
async play({ mount }) {
|
||||
const canvas = await mount(Button, { label: 'label', disabled: true });
|
||||
expectTypeOf(canvas).toEqualTypeOf<Canvas>();
|
||||
},
|
||||
};
|
||||
expectTypeOf(Basic).toEqualTypeOf<StoryObj<Button>>();
|
||||
});
|
||||
|
@ -1,4 +1,8 @@
|
||||
import type { StoryContext as StoryContextBase, WebRenderer } from 'storybook/internal/types';
|
||||
import type {
|
||||
Canvas,
|
||||
StoryContext as StoryContextBase,
|
||||
WebRenderer,
|
||||
} from 'storybook/internal/types';
|
||||
import type { ComponentConstructorOptions, ComponentEvents, SvelteComponent } from 'svelte';
|
||||
|
||||
export type StoryContext = StoryContextBase<SvelteRenderer>;
|
||||
@ -37,6 +41,12 @@ export interface SvelteRenderer<C extends SvelteComponent = SvelteComponent> ext
|
||||
storyResult: this['T'] extends Record<string, any>
|
||||
? SvelteStoryResult<this['T'], ComponentEvents<C>>
|
||||
: SvelteStoryResult;
|
||||
|
||||
mount: (
|
||||
Component?: ComponentType,
|
||||
// TODO add proper typesafety
|
||||
options?: Record<string, any> & { props: Record<string, any> }
|
||||
) => Promise<Canvas>;
|
||||
}
|
||||
|
||||
export interface SvelteStoryResult<
|
||||
|
44
code/renderers/svelte/template/stories/Button.svelte
Normal file
44
code/renderers/svelte/template/stories/Button.svelte
Normal file
@ -0,0 +1,44 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
/**
|
||||
* Is this the principal call to action on the page?
|
||||
*/
|
||||
export let primary = false;
|
||||
|
||||
/**
|
||||
* What background color to use
|
||||
*/
|
||||
export let backgroundColor = undefined;
|
||||
|
||||
/**
|
||||
* How large should the button be?
|
||||
*/
|
||||
export let size = 'medium';
|
||||
|
||||
/**
|
||||
* Button contents
|
||||
*/
|
||||
export let label = '';
|
||||
|
||||
$: mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
|
||||
|
||||
$: style = backgroundColor ? `background-color: ${backgroundColor}` : '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
/**
|
||||
* Optional click handler
|
||||
*/
|
||||
export let onClick = (event) => {
|
||||
dispatch('click', event);
|
||||
};
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
|
||||
{style}
|
||||
on:click={onClick}
|
||||
>
|
||||
{label}
|
||||
</button>
|
@ -0,0 +1,15 @@
|
||||
import Button from './Button.svelte';
|
||||
import type { StoryObj } from '@storybook/svelte';
|
||||
|
||||
export default {
|
||||
component: Button,
|
||||
};
|
||||
|
||||
export const Basic: StoryObj = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
async play({ mount, args }) {
|
||||
await mount(Button, { props: { ...args, label: 'set in play' } });
|
||||
},
|
||||
};
|
@ -11,6 +11,8 @@ import * as stories from './Button.stories';
|
||||
import type Button from './Button.vue';
|
||||
import { composeStories, composeStory, setProjectAnnotations } from '../../portable-stories';
|
||||
|
||||
setProjectAnnotations({ testingLibraryRender: render });
|
||||
|
||||
// example with composeStories, returns an object with all stories composed with args/decorators
|
||||
const { CSF3Primary, LoaderStory } = composeStories(stories);
|
||||
|
||||
@ -58,6 +60,7 @@ describe('projectAnnotations', () => {
|
||||
it('renders with default projectAnnotations', () => {
|
||||
setProjectAnnotations([
|
||||
{
|
||||
testingLibraryRender: render,
|
||||
parameters: { injected: true },
|
||||
globalTypes: {
|
||||
locale: { defaultValue: 'en' },
|
||||
@ -100,9 +103,7 @@ describe('CSF3', () => {
|
||||
it('renders with play function', async () => {
|
||||
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
|
||||
|
||||
render(CSF3InputFieldFilled);
|
||||
|
||||
await CSF3InputFieldFilled.play!();
|
||||
await CSF3InputFieldFilled.play();
|
||||
|
||||
const input = screen.getByTestId('input') as HTMLInputElement;
|
||||
expect(input.value).toEqual('Hello world!');
|
||||
@ -143,14 +144,8 @@ describe('ComposeStories types', () => {
|
||||
// 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 === 'CSF2StoryWithLocale') {
|
||||
return;
|
||||
}
|
||||
|
||||
await Story.load();
|
||||
const { baseElement } = await render(Story);
|
||||
await Story.play?.();
|
||||
if (typeof Story === 'string' || _storyName === 'CSF2StoryWithLocale') return;
|
||||
await Story.play();
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(baseElement).toMatchSnapshot();
|
||||
expect(document.body).toMatchSnapshot();
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
export const parameters: {} = { renderer: 'vue3' };
|
||||
export { render, renderToCanvas } from './render';
|
||||
export { decorateStory as applyDecorators } from './decorateStory';
|
||||
export { mount } from './mount';
|
||||
|
13
code/renderers/vue3/src/mount.ts
Normal file
13
code/renderers/vue3/src/mount.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { type StoryContext, type VueRenderer } from './public-types';
|
||||
import { h } from 'vue';
|
||||
import { type BaseAnnotations } from '@storybook/types';
|
||||
|
||||
export const mount: BaseAnnotations<VueRenderer>['mount'] = (context: StoryContext) => {
|
||||
return async (Component, options) => {
|
||||
if (Component) {
|
||||
context.originalStoryFn = () => () => h(Component, options?.props, options?.slots);
|
||||
}
|
||||
await context.renderToCanvas();
|
||||
return context.canvas;
|
||||
};
|
||||
};
|
@ -11,6 +11,7 @@ import type {
|
||||
Store_CSFExports,
|
||||
StoriesWithPartialProps,
|
||||
} from 'storybook/internal/types';
|
||||
import { TestingLibraryMustBeConfiguredError } from 'storybook/internal/preview-errors';
|
||||
import { h } from 'vue';
|
||||
|
||||
import * as defaultProjectAnnotations from './entry-preview';
|
||||
@ -48,6 +49,16 @@ export function setProjectAnnotations(
|
||||
originalSetProjectAnnotations<VueRenderer>(projectAnnotations);
|
||||
}
|
||||
|
||||
// This will not be necessary once we have auto preset loading
|
||||
export const vueProjectAnnotations: ProjectAnnotations<VueRenderer> = {
|
||||
...defaultProjectAnnotations,
|
||||
renderToCanvas: ({ storyFn, storyContext: { testingLibraryRender: render, canvasElement } }) => {
|
||||
if (render == null) throw new TestingLibraryMustBeConfiguredError();
|
||||
const { unmount } = render(storyFn(), { baseElement: canvasElement });
|
||||
return unmount;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Function that will receive a story along with meta (e.g. a default export from a .stories file)
|
||||
* and optionally projectAnnotations e.g. (import * from '../.storybook/preview)
|
||||
@ -85,7 +96,7 @@ export function composeStory<TArgs extends Args = Args>(
|
||||
story as StoryAnnotationsOrFn<VueRenderer, Args>,
|
||||
componentAnnotations,
|
||||
projectAnnotations,
|
||||
defaultProjectAnnotations,
|
||||
vueProjectAnnotations,
|
||||
exportsName
|
||||
);
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
// this file tests Typescript types that's why there are no assertions
|
||||
import { describe, it } from 'vitest';
|
||||
import { satisfies } from 'storybook/internal/common';
|
||||
import type { ComponentAnnotations, StoryAnnotations } from 'storybook/internal/types';
|
||||
import type { Canvas, ComponentAnnotations, StoryAnnotations } from 'storybook/internal/types';
|
||||
import { expectTypeOf } from 'expect-type';
|
||||
import type { SetOptional } from 'type-fest';
|
||||
import { h } from 'vue';
|
||||
@ -196,3 +196,13 @@ it('Infer type of slots', () => {
|
||||
type Expected = StoryAnnotations<VueRenderer, Props, Props>;
|
||||
expectTypeOf(Basic).toEqualTypeOf<Expected>();
|
||||
});
|
||||
|
||||
it('mount accepts a Component', () => {
|
||||
const Basic: StoryObj<typeof Button> = {
|
||||
async play({ mount }) {
|
||||
const canvas = await mount(Button, { label: 'label', disabled: true });
|
||||
expectTypeOf(canvas).toEqualTypeOf<Canvas>();
|
||||
},
|
||||
};
|
||||
expectTypeOf(Basic).toEqualTypeOf<StoryObj<typeof Button>>();
|
||||
});
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { type StoryContext as StoryContextBase, type WebRenderer } from 'storybook/internal/types';
|
||||
import {
|
||||
type Canvas,
|
||||
type StoryContext as StoryContextBase,
|
||||
type WebRenderer,
|
||||
} from 'storybook/internal/types';
|
||||
import type { App, ConcreteComponent } from 'vue';
|
||||
|
||||
export type { RenderContext } from 'storybook/internal/types';
|
||||
@ -21,4 +25,10 @@ export interface VueRenderer extends WebRenderer {
|
||||
// Try not omitting, and check the type errros in the test file, if you want to learn more.
|
||||
component: Omit<ConcreteComponent<this['T']>, 'props'>;
|
||||
storyResult: StoryFnVueReturnType;
|
||||
|
||||
mount: (
|
||||
Component?: StoryFnVueReturnType,
|
||||
// TODO add proper typesafety
|
||||
options?: { props?: Record<string, any>; slots?: Record<string, any> }
|
||||
) => Promise<Canvas>;
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
import type { StoryObj } from '@storybook/vue3';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
const Button = defineComponent({
|
||||
template: '<button :disabled="disabled">{{label}}</button>',
|
||||
props: ['disabled', 'label'],
|
||||
});
|
||||
|
||||
export default {
|
||||
component: Button,
|
||||
};
|
||||
|
||||
export const Basic: StoryObj<typeof Button> = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
async play({ mount, args }) {
|
||||
await mount(Button, { props: { ...args, label: 'set in play' } });
|
||||
},
|
||||
};
|
138
code/yarn.lock
138
code/yarn.lock
@ -501,7 +501,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.24.1, @babel/helper-create-class-features-plugin@npm:^7.24.4":
|
||||
"@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.24.0, @babel/helper-create-class-features-plugin@npm:^7.24.1, @babel/helper-create-class-features-plugin@npm:^7.24.4":
|
||||
version: 7.24.4
|
||||
resolution: "@babel/helper-create-class-features-plugin@npm:7.24.4"
|
||||
dependencies:
|
||||
@ -692,9 +692,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-string-parser@npm:^7.23.4":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/helper-string-parser@npm:7.24.1"
|
||||
checksum: 10c0/2f9bfcf8d2f9f083785df0501dbab92770111ece2f90d120352fda6dd2a7d47db11b807d111e6f32aa1ba6d763fe2dc6603d153068d672a5d0ad33ca802632b2
|
||||
version: 7.23.4
|
||||
resolution: "@babel/helper-string-parser@npm:7.23.4"
|
||||
checksum: 10c0/f348d5637ad70b6b54b026d6544bd9040f78d24e7ec245a0fc42293968181f6ae9879c22d89744730d246ce8ec53588f716f102addd4df8bbc79b73ea10004ac
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -705,7 +705,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/helper-validator-option@npm:^7.23.5":
|
||||
"@babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.23.5":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/helper-validator-option@npm:7.23.5"
|
||||
checksum: 10c0/af45d5c0defb292ba6fd38979e8f13d7da63f9623d8ab9ededc394f67eb45857d2601278d151ae9affb6e03d5d608485806cd45af08b4468a0515cf506510e94
|
||||
@ -816,15 +816,15 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-proposal-decorators@npm:^7.13.5, @babel/plugin-proposal-decorators@npm:^7.22.7":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/plugin-proposal-decorators@npm:7.24.1"
|
||||
version: 7.24.0
|
||||
resolution: "@babel/plugin-proposal-decorators@npm:7.24.0"
|
||||
dependencies:
|
||||
"@babel/helper-create-class-features-plugin": "npm:^7.24.1"
|
||||
"@babel/helper-create-class-features-plugin": "npm:^7.24.0"
|
||||
"@babel/helper-plugin-utils": "npm:^7.24.0"
|
||||
"@babel/plugin-syntax-decorators": "npm:^7.24.1"
|
||||
"@babel/plugin-syntax-decorators": "npm:^7.24.0"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/ffe49522ada6581f1c760b777dbd913afcd204e11e6907c4f2c293ce6d30961449ac19d9960250d8743a1f60e21cb667e51a3af15992dfe7627105e039c46a9b
|
||||
checksum: 10c0/6bf16cb2b5b2f1b63b5ea964853cd3b3419c8285296b5bf64a64127c9d5c1b2e6829e84bd92734e4b71df67686d8f36fb01bb8a45fc52bcece7503b73bc42ec7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -932,14 +932,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-syntax-decorators@npm:^7.24.1":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/plugin-syntax-decorators@npm:7.24.1"
|
||||
"@babel/plugin-syntax-decorators@npm:^7.24.0":
|
||||
version: 7.24.0
|
||||
resolution: "@babel/plugin-syntax-decorators@npm:7.24.0"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.24.0"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/14028a746f86efbdd47e4961456bb53d656e9e3461890f66b1b01032151d15fda5ba99fcaa60232a229a33aa9e73b11c2597b706d5074c520155757e372cd17b
|
||||
checksum: 10c0/6c11801e062772d4e1b0b418a4732574128b1dfc13193a2909fa93937346746aaa7046f88f6026ff3c80777c967d0fe2e4bb19a1d3fb399e8349c81741e4f471
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -965,14 +965,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-syntax-flow@npm:^7.24.1":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/plugin-syntax-flow@npm:7.24.1"
|
||||
"@babel/plugin-syntax-flow@npm:^7.22.5":
|
||||
version: 7.22.5
|
||||
resolution: "@babel/plugin-syntax-flow@npm:7.22.5"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.24.0"
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/618de04360a96111408abdaafaba2efbaef0d90faad029d50e0281eaad5d7c7bd2ce4420bbac0ee27ad84c2b7bbc3e48f782064f81ed5bc40c398637991004c7
|
||||
checksum: 10c0/07afc7df02141597968532bfbfa3f6c0ad21a2bdd885d0e5e035dcf60fdf35f0995631c9750b464e1a6f2feea14160a82787f914e88e8f7115dc99f09853e43e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -1354,15 +1354,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-flow-strip-types@npm:^7.24.1":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/plugin-transform-flow-strip-types@npm:7.24.1"
|
||||
"@babel/plugin-transform-flow-strip-types@npm:^7.22.5":
|
||||
version: 7.22.5
|
||||
resolution: "@babel/plugin-transform-flow-strip-types@npm:7.22.5"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.24.0"
|
||||
"@babel/plugin-syntax-flow": "npm:^7.24.1"
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
"@babel/plugin-syntax-flow": "npm:^7.22.5"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/e6aa9cbad0441867598d390d4df65bc8c6b797574673e4eedbdae0cc528e81e00f4b2cd38f7d138b0f04bcdd2540384a9812d5d76af5abfa06aee1c7fc20ca58
|
||||
checksum: 10c0/5949a8e5214e3fc65d31dab0551423cea9d9eef35faa5d0004707ba7347baf96166aa400907ce7498f754db4e1e9d039ca434a508546b0dc9fdae9a42e814c1a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -1536,13 +1536,13 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-object-assign@npm:^7.8.3":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/plugin-transform-object-assign@npm:7.24.1"
|
||||
version: 7.22.5
|
||||
resolution: "@babel/plugin-transform-object-assign@npm:7.22.5"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.24.0"
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/eb30beac71a5930ecdfc8740b184f22dd2043b1ac6f9f6818fb2e10ddfbdd6536b4ddb0d00af2c9f4a375823f52a566915eb598bea0633484aa5ff5db4e547fd
|
||||
checksum: 10c0/c80ca956ccc45c68a6f35e8aea80e08c0a653e4baf243727d4258f242d312d71be20e3fad35a1f2cd9d58b30dcbb5cdf5f8d6c6614a3f8c6079d90f9b1dadee6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -1668,24 +1668,24 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self@npm:^7.18.6":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/plugin-transform-react-jsx-self@npm:7.24.1"
|
||||
version: 7.22.5
|
||||
resolution: "@babel/plugin-transform-react-jsx-self@npm:7.22.5"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.24.0"
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/ea362ff94b535c753f560eb1f5e063dc72bbbca17ed58837a949a7b289d5eacc7b0a28296d1932c94429b168d6040cdee5484a59b9e3c021f169e0ee137e6a27
|
||||
checksum: 10c0/263091bdede1f448cb2c59b84eb69972c15d3f022c929a75337bd20d8b65551ac38cd26dad1946eaa93289643506b10ddaea3445a28cb8fca5a773a22a0df90b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source@npm:^7.19.6":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/plugin-transform-react-jsx-source@npm:7.24.1"
|
||||
version: 7.22.5
|
||||
resolution: "@babel/plugin-transform-react-jsx-source@npm:7.22.5"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.24.0"
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/ea8e3263c0dc51fbc97c156cc647150a757cc56de10781287353d0ce9b2dcd6b6d93d573c0142d7daf5d6fb554c74fa1971ae60764924ea711161d8458739b63
|
||||
checksum: 10c0/defc9debb76b4295e3617ef7795a0533dbbecef6f51bf5ba4bfc162df892a84fd39e14d5f1b9a5aad7b09b97074fef4c6756f9d2036eef5a9874acabe198f75a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -2080,15 +2080,15 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@babel/preset-flow@npm:^7.13.13, @babel/preset-flow@npm:^7.22.15":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/preset-flow@npm:7.24.1"
|
||||
version: 7.22.15
|
||||
resolution: "@babel/preset-flow@npm:7.22.15"
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils": "npm:^7.24.0"
|
||||
"@babel/helper-validator-option": "npm:^7.23.5"
|
||||
"@babel/plugin-transform-flow-strip-types": "npm:^7.24.1"
|
||||
"@babel/helper-plugin-utils": "npm:^7.22.5"
|
||||
"@babel/helper-validator-option": "npm:^7.22.15"
|
||||
"@babel/plugin-transform-flow-strip-types": "npm:^7.22.5"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/e2209158d68a456b8f9d6cd6c810e692f3ab8ca28edba99afcecaacd657ace7cc905e566f84d6da06e537836a2f830bc6ddf4cb34006d57303ff9a40a94fa433
|
||||
checksum: 10c0/7eef0c84ec1889d6c4f7a67d7d1a81703420eed123a8c23f25af148eead77907f0bd701f3e729fdb37d3ddb2a373bf43938b36a9ba17f546111ddb9521466b92
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -2137,17 +2137,17 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@babel/register@npm:^7.13.16, @babel/register@npm:^7.22.15":
|
||||
version: 7.23.7
|
||||
resolution: "@babel/register@npm:7.23.7"
|
||||
version: 7.22.15
|
||||
resolution: "@babel/register@npm:7.22.15"
|
||||
dependencies:
|
||||
clone-deep: "npm:^4.0.1"
|
||||
find-cache-dir: "npm:^2.0.0"
|
||||
make-dir: "npm:^2.1.0"
|
||||
pirates: "npm:^4.0.6"
|
||||
pirates: "npm:^4.0.5"
|
||||
source-map-support: "npm:^0.5.16"
|
||||
peerDependencies:
|
||||
"@babel/core": ^7.0.0-0
|
||||
checksum: 10c0/b2466e41a4394e725b57e139ba45c3f61b88546d3cb443e84ce46cb34071b60c6cdb706a14c58a1443db530691a54f51da1f0c97f6c1aecbb838a2fb7eb5dbb9
|
||||
checksum: 10c0/895cc773c3b3eae909478ea2a9735ef6edd634b04b4aaaad2ce576fd591c2b3c70ff8c90423e769a291bee072186e7e4801480c1907e31ba3053c6cdba5571cb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -2159,12 +2159,12 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime-corejs3@npm:^7.10.2":
|
||||
version: 7.24.4
|
||||
resolution: "@babel/runtime-corejs3@npm:7.24.4"
|
||||
version: 7.23.1
|
||||
resolution: "@babel/runtime-corejs3@npm:7.23.1"
|
||||
dependencies:
|
||||
core-js-pure: "npm:^3.30.2"
|
||||
regenerator-runtime: "npm:^0.14.0"
|
||||
checksum: 10c0/121bec9a0b505e2995c4b71cf480167e006e8ee423f77bccc38975bfbfbfdb191192ff03557c18fad6de8f2b85c12c49aaa4b92d1d5fe0c0e136da664129be1e
|
||||
checksum: 10c0/6e2c2b11779ff56c88b1f3a8742498640f7271ad4fcf9cfd24052bbb236a5e7c4c7c8d81cda751da3b4effa678736303deb78441c5752e63bfb90d6453fd870f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -2802,11 +2802,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@figspec/components@npm:^1.0.1":
|
||||
version: 1.0.3
|
||||
resolution: "@figspec/components@npm:1.0.3"
|
||||
version: 1.0.2
|
||||
resolution: "@figspec/components@npm:1.0.2"
|
||||
dependencies:
|
||||
lit: "npm:^2.1.3"
|
||||
checksum: 10c0/78f5ee600ea1d15af7848b9fc601063acd537f49deb08aa16b95d3452c3a5783352e142c970f9a7375817099f2f54c3f9b42911f21c44ce6dab72eab20e6e20a
|
||||
checksum: 10c0/8e889140d6577f6bdf31a6b460539127f1614e42c8d08b545b8dd500dbb606edae87e7619933fc6039370fc7552a4b68458e23957e0d7c28ca90eecf7b06cdce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -5148,8 +5148,8 @@ __metadata:
|
||||
linkType: soft
|
||||
|
||||
"@storybook/addon-designs@npm:^7.0.4":
|
||||
version: 7.0.9
|
||||
resolution: "@storybook/addon-designs@npm:7.0.9"
|
||||
version: 7.0.7
|
||||
resolution: "@storybook/addon-designs@npm:7.0.7"
|
||||
dependencies:
|
||||
"@figspec/react": "npm:^1.0.0"
|
||||
peerDependencies:
|
||||
@ -5166,7 +5166,7 @@ __metadata:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: 10c0/e27528e64a9bb21cd194d6940d852d88828ca6b2e473b9fa69ac8d4446322935bf73286d98d22bdd6316937db176a1295a6befd1ba4a8021105b5eebb4099950
|
||||
checksum: 10c0/650cb4254a2e12b5c80cc999fb9048efc6ce9bf0d9a29b78a5b5e4fc1e9a67d0b1e5f58ee3fa14780efcb3c896967017d56b11dde989ea1931db409e3ca534f1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -5270,7 +5270,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@storybook/addon-links@workspace:addons/links"
|
||||
dependencies:
|
||||
"@storybook/csf": "npm:0.1.10"
|
||||
"@storybook/csf": "npm:0.1.11"
|
||||
"@storybook/global": "npm:^5.0.0"
|
||||
fs-extra: "npm:^11.1.0"
|
||||
ts-dedent: "npm:^2.0.0"
|
||||
@ -5493,7 +5493,7 @@ __metadata:
|
||||
resolution: "@storybook/blocks@workspace:lib/blocks"
|
||||
dependencies:
|
||||
"@storybook/addon-actions": "workspace:*"
|
||||
"@storybook/csf": "npm:0.1.10"
|
||||
"@storybook/csf": "npm:0.1.11"
|
||||
"@storybook/global": "npm:^5.0.0"
|
||||
"@storybook/icons": "npm:^1.2.5"
|
||||
"@storybook/test": "workspace:*"
|
||||
@ -5645,7 +5645,7 @@ __metadata:
|
||||
"@babel/preset-env": "npm:^7.24.4"
|
||||
"@babel/types": "npm:^7.24.0"
|
||||
"@storybook/core": "workspace:*"
|
||||
"@storybook/csf": "npm:0.1.10"
|
||||
"@storybook/csf": "npm:0.1.11"
|
||||
"@types/cross-spawn": "npm:^6.0.2"
|
||||
"@types/jscodeshift": "npm:^0.11.10"
|
||||
ansi-regex: "npm:^5.0.1"
|
||||
@ -5739,7 +5739,7 @@ __metadata:
|
||||
"@radix-ui/react-dialog": "npm:^1.0.5"
|
||||
"@radix-ui/react-scroll-area": "npm:^1.0.5"
|
||||
"@radix-ui/react-slot": "npm:^1.0.2"
|
||||
"@storybook/csf": "npm:0.1.10"
|
||||
"@storybook/csf": "npm:0.1.11"
|
||||
"@storybook/docs-mdx": "npm:3.1.0-next.0"
|
||||
"@storybook/global": "npm:^5.0.0"
|
||||
"@storybook/icons": "npm:^1.2.5"
|
||||
@ -5875,12 +5875,12 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@storybook/csf@npm:0.1.10":
|
||||
version: 0.1.10
|
||||
resolution: "@storybook/csf@npm:0.1.10"
|
||||
"@storybook/csf@npm:0.1.11":
|
||||
version: 0.1.11
|
||||
resolution: "@storybook/csf@npm:0.1.11"
|
||||
dependencies:
|
||||
type-fest: "npm:^2.19.0"
|
||||
checksum: 10c0/c5bd17b92aeb8be5918cfad238bfef4c08553f8c60b6284e1cabb8646aeb6f8d6ab4343a77954a5c9924ca717cf306c239c0b061915918137136aa0c9b4be5ab
|
||||
checksum: 10c0/c5329fc13e7d762049b5c91df1bc1c0e510a1a898c401b72b68f1ff64139a85ab64a92f8e681d2fcb226c0a4a55d0f23b569b2bdb517e0f067bd05ea46228356
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -6472,7 +6472,7 @@ __metadata:
|
||||
"@storybook/codemod": "workspace:*"
|
||||
"@storybook/core": "workspace:*"
|
||||
"@storybook/core-webpack": "workspace:*"
|
||||
"@storybook/csf": "npm:0.1.10"
|
||||
"@storybook/csf": "npm:0.1.11"
|
||||
"@storybook/csf-plugin": "workspace:*"
|
||||
"@storybook/ember": "workspace:*"
|
||||
"@storybook/eslint-config-storybook": "npm:^4.0.0"
|
||||
@ -6607,7 +6607,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@storybook/server@workspace:renderers/server"
|
||||
dependencies:
|
||||
"@storybook/csf": "npm:0.1.10"
|
||||
"@storybook/csf": "npm:0.1.11"
|
||||
"@storybook/global": "npm:^5.0.0"
|
||||
"@types/fs-extra": "npm:^11.0.1"
|
||||
fs-extra: "npm:^11.1.0"
|
||||
@ -6623,7 +6623,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@storybook/source-loader@workspace:lib/source-loader"
|
||||
dependencies:
|
||||
"@storybook/csf": "npm:0.1.10"
|
||||
"@storybook/csf": "npm:0.1.11"
|
||||
estraverse: "npm:^5.2.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
prettier: "npm:^3.1.1"
|
||||
@ -6725,7 +6725,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@storybook/test@workspace:lib/test"
|
||||
dependencies:
|
||||
"@storybook/csf": "npm:0.1.10"
|
||||
"@storybook/csf": "npm:0.1.11"
|
||||
"@storybook/instrumenter": "workspace:*"
|
||||
"@testing-library/dom": "npm:10.1.0"
|
||||
"@testing-library/jest-dom": "npm:6.4.5"
|
||||
@ -22132,7 +22132,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pirates@npm:^4.0.6":
|
||||
"pirates@npm:^4.0.5":
|
||||
version: 4.0.6
|
||||
resolution: "pirates@npm:4.0.6"
|
||||
checksum: 10c0/00d5fa51f8dded94d7429700fb91a0c1ead00ae2c7fd27089f0c5b63e6eca36197fe46384631872690a66f390c5e27198e99006ab77ae472692ab9c2ca903f36
|
||||
|
@ -38,7 +38,7 @@ const sbInit = async (
|
||||
flags?: string[],
|
||||
debug?: boolean
|
||||
) => {
|
||||
const sbCliBinaryPath = join(__dirname, `../../code/lib/cli/bin/index.js`);
|
||||
const sbCliBinaryPath = join(__dirname, `../../code/lib/cli/bin/index.cjs`);
|
||||
console.log(`🎁 Installing storybook`);
|
||||
const env = { STORYBOOK_DISABLE_TELEMETRY: 'true', ...envVars };
|
||||
const fullFlags = ['--yes', ...(flags || [])];
|
||||
|
@ -4,11 +4,12 @@ import { ReactRenderer } from '@storybook/react';
|
||||
import { setProjectAnnotations } from '@storybook/nextjs';
|
||||
import * as addonInteractions from '@storybook/addon-interactions/preview';
|
||||
import * as addonActions from '@storybook/addon-essentials/actions/preview';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
/**
|
||||
* For some weird reason, Jest in Nextjs throws the following error:
|
||||
* Cannot find module '.storybook/preview' from 'jest.setup.ts
|
||||
*
|
||||
*
|
||||
* when using import sbAnnotations from './.storybook/preview';
|
||||
*/
|
||||
const sbAnnotations = require('./.storybook/preview');
|
||||
@ -17,4 +18,5 @@ setProjectAnnotations([
|
||||
sbAnnotations,
|
||||
addonInteractions as ProjectAnnotations<ReactRenderer>, // instruments actions as spies
|
||||
addonActions as ProjectAnnotations<ReactRenderer>, // creates actions from argTypes
|
||||
{ testingLibraryRender: render },
|
||||
]);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { describe, it, expect } from '@jest/globals'
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { composeStories } from '@storybook/nextjs';
|
||||
import { composeStories, setProjectAnnotations } from '@storybook/nextjs';
|
||||
import * as imageStories from './Image.stories';
|
||||
import * as navigationStories from './Navigation.stories';
|
||||
import * as linkStories from './Link.stories';
|
||||
@ -20,14 +20,12 @@ const runTests = (name: string, storiesModule: any) => {
|
||||
const composedStories = composeStories(storiesModule);
|
||||
Object.entries(composedStories).forEach(([name, Story]: [any, any]) => {
|
||||
it(`renders ${name}`, async () => {
|
||||
await Story.load();
|
||||
const { baseElement } = render(<Story />);
|
||||
await Story.play?.();
|
||||
expect(baseElement).toMatchSnapshot();
|
||||
expect(document.body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// example with composeStory, returns a single story composed with args/decorators
|
||||
describe('renders', () => {
|
||||
@ -42,4 +40,4 @@ describe('renders', () => {
|
||||
runTests('fontStories', fontStories);
|
||||
runTests('headStories', headStories);
|
||||
runTests('getImagePropsStories', getImagePropsStories);
|
||||
});
|
||||
});
|
||||
|
@ -4,9 +4,11 @@ import { ReactRenderer, setProjectAnnotations } from '@storybook/react';
|
||||
import sbAnnotations from './.storybook/preview';
|
||||
import * as addonInteractions from '@storybook/addon-interactions/preview';
|
||||
import * as addonActions from '@storybook/addon-essentials/actions/preview';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
setProjectAnnotations([
|
||||
sbAnnotations,
|
||||
addonInteractions as ProjectAnnotations<ReactRenderer>, // instruments actions as spies
|
||||
addonActions as ProjectAnnotations<ReactRenderer>, // creates actions from argTypes
|
||||
{ testingLibraryRender: render },
|
||||
]);
|
||||
|
@ -2,48 +2,33 @@
|
||||
import * as stories from './Button.stories';
|
||||
import { composeStories } from '@storybook/react';
|
||||
|
||||
const { CSF3Primary, WithLoader, CSF3InputFieldFilled, Modal } = composeStories(stories)
|
||||
const { CSF3Primary, WithLoader, Modal } = composeStories(stories);
|
||||
|
||||
describe('<Button />', () => {
|
||||
it('renders with loaders and play function', () => {
|
||||
cy.then(async() => {
|
||||
cy.then(async () => {
|
||||
await WithLoader.load();
|
||||
});
|
||||
|
||||
cy.mount(<WithLoader />);
|
||||
|
||||
cy.then(async() => {
|
||||
await WithLoader.play!({ canvasElement: document.querySelector('[data-cy-root]') as HTMLElement });
|
||||
cy.then(async () => {
|
||||
cy.get('[data-testid="loaded-data"]').should('contain.text', 'loaded data');
|
||||
cy.get('[data-testid="mock-data"]').should('contain.text', 'mockFn return value');
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('renders primary button', async () => {
|
||||
cy.mount(<CSF3Primary />)
|
||||
cy.mount(<CSF3Primary />);
|
||||
cy.get('[data-decorator]').should('exist');
|
||||
})
|
||||
});
|
||||
|
||||
it('renders primary button with custom args', async () => {
|
||||
cy.mount(<CSF3Primary>bar</CSF3Primary>)
|
||||
cy.mount(<CSF3Primary>bar</CSF3Primary>);
|
||||
cy.get('button').should('contain.text', 'bar');
|
||||
})
|
||||
|
||||
it('renders with play function', () => {
|
||||
cy.mount(<CSF3InputFieldFilled />);
|
||||
|
||||
cy.then(async() => {
|
||||
await CSF3InputFieldFilled.play!({ canvasElement: document.querySelector('[data-cy-root]') as HTMLElement });
|
||||
cy.get('[data-testid="input"]').should('contain.value', 'Hello world!');
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('renders modal story', () => {
|
||||
cy.mount(<Modal />);
|
||||
|
||||
cy.then(async() => {
|
||||
await Modal.play!({ canvasElement: document.querySelector('[data-cy-root]') as HTMLElement });
|
||||
cy.get('[role="dialog"]').should('exist');
|
||||
});
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
@ -4,16 +4,16 @@ import { addons } from 'storybook/internal/preview-api';
|
||||
import { setProjectAnnotations, composeStories, composeStory } from '@storybook/react';
|
||||
import * as stories from './Button.stories';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// 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);
|
||||
describe('renders', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders primary button', () => {
|
||||
render(<CSF3Primary>Hello world</CSF3Primary>);
|
||||
const buttonElement = screen.getByText(/Hello world/i);
|
||||
@ -46,10 +46,6 @@ describe('renders', () => {
|
||||
});
|
||||
|
||||
describe('projectAnnotations', () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders with default projectAnnotations', () => {
|
||||
const WithEnglishText = composeStory(stories.CSF2StoryWithLocale, stories.default);
|
||||
const { getByText } = render(<WithEnglishText />);
|
||||
@ -67,7 +63,7 @@ describe('projectAnnotations', () => {
|
||||
});
|
||||
|
||||
it('renders with custom projectAnnotations via setProjectAnnotations', () => {
|
||||
setProjectAnnotations([{ parameters: { injected: true } }]);
|
||||
setProjectAnnotations([{ parameters: { injected: true }, testingLibraryRender: render }]);
|
||||
const Story = composeStory(stories.CSF2StoryWithLocale, stories.default);
|
||||
expect(Story.parameters?.injected).toBe(true);
|
||||
});
|
||||
@ -96,9 +92,7 @@ describe('CSF3', () => {
|
||||
it('renders with play function', async () => {
|
||||
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
|
||||
|
||||
const { container } = render(<CSF3InputFieldFilled />);
|
||||
|
||||
await CSF3InputFieldFilled.play({ canvasElement: container });
|
||||
await CSF3InputFieldFilled.play();
|
||||
|
||||
const input = screen.getByTestId('input') as HTMLInputElement;
|
||||
expect(input.value).toEqual('Hello world!');
|
||||
@ -123,9 +117,6 @@ it('should pass with decorators that need addons channel', () => {
|
||||
// Batch snapshot testing
|
||||
const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName, Story]);
|
||||
it.each(testCases)('Renders %s story', async (_storyName, Story) => {
|
||||
cleanup();
|
||||
await Story.load();
|
||||
const { baseElement } = await render(<Story />);
|
||||
await Story.play?.();
|
||||
expect(baseElement).toMatchSnapshot();
|
||||
await Story.play();
|
||||
expect(document.body).toMatchSnapshot();
|
||||
});
|
||||
|
@ -6,6 +6,8 @@ import * as stories from './Button.stories';
|
||||
// import type Button from './Button.svelte';
|
||||
import { composeStories, composeStory, setProjectAnnotations } from '@storybook/svelte';
|
||||
|
||||
setProjectAnnotations({ testingLibraryRender: render });
|
||||
|
||||
// example with composeStories, returns an object with all stories composed with args/decorators
|
||||
const { CSF3Primary, LoaderStory } = composeStories(stories);
|
||||
|
||||
@ -69,6 +71,7 @@ describe('projectAnnotations', () => {
|
||||
globalTypes: {
|
||||
locale: { defaultValue: 'en' },
|
||||
},
|
||||
testingLibraryRender: render,
|
||||
},
|
||||
]);
|
||||
const WithEnglishText = composeStory(stories.CSF2StoryWithLocale, stories.default);
|
||||
@ -111,9 +114,7 @@ describe('CSF3', () => {
|
||||
it('renders with play function without canvas element', async () => {
|
||||
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
|
||||
|
||||
render(CSF3InputFieldFilled.Component, CSF3InputFieldFilled.props);
|
||||
|
||||
await CSF3InputFieldFilled.play!();
|
||||
await CSF3InputFieldFilled.play();
|
||||
|
||||
const input = screen.getByTestId('input') as HTMLInputElement;
|
||||
expect(input.value).toEqual('Hello world!');
|
||||
@ -122,12 +123,15 @@ describe('CSF3', () => {
|
||||
it('renders with play function with canvas element', async () => {
|
||||
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);
|
||||
|
||||
const { container } = render(CSF3InputFieldFilled.Component, CSF3InputFieldFilled.props);
|
||||
const div = document.createElement('div');
|
||||
document.body.appendChild(div);
|
||||
|
||||
await CSF3InputFieldFilled.play!({ canvasElement: container });
|
||||
await CSF3InputFieldFilled.play({ canvasElement: div });
|
||||
|
||||
const input = screen.getByTestId('input') as HTMLInputElement;
|
||||
expect(input.value).toEqual('Hello world!');
|
||||
|
||||
document.body.removeChild(div);
|
||||
});
|
||||
});
|
||||
|
||||
@ -136,17 +140,7 @@ 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();
|
||||
|
||||
if (_storyName === 'CSF2StoryWithLocale') {
|
||||
return;
|
||||
}
|
||||
|
||||
await Story.load();
|
||||
|
||||
const { container } = await render(Story.Component, Story.props);
|
||||
|
||||
await Story.play?.({ canvasElement: container });
|
||||
expect(container).toMatchSnapshot();
|
||||
if (_storyName === 'CSF2StoryWithLocale') return;
|
||||
await Story.play();
|
||||
expect(document.body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -2,48 +2,22 @@
|
||||
|
||||
exports[`Renders CSF2Secondary story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
label coming from story args!
|
||||
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
label coming from story args!
|
||||
|
||||
</button>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF2StoryWithParamsAndDecorator story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
style="margin: 3em;"
|
||||
>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF3Button story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
style="margin: 3em;"
|
||||
>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
@ -53,51 +27,90 @@ exports[`Renders CSF3Button story 1`] = `
|
||||
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF3Button story 1`] = `
|
||||
<body>
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
|
||||
</button>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF3ButtonWithRender story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div>
|
||||
<p
|
||||
data-testid="custom-render"
|
||||
>
|
||||
I am a custom render function
|
||||
</p>
|
||||
<p
|
||||
data-testid="custom-render"
|
||||
>
|
||||
I am a custom render function
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
|
||||
<button
|
||||
class="storybook-button storybook-button--medium storybook-button--secondary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF3InputFieldFilled story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<input
|
||||
data-testid="input"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
<input
|
||||
data-testid="input"
|
||||
/>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders CSF3Primary story 1`] = `
|
||||
<body>
|
||||
<button
|
||||
class="storybook-button storybook-button--large storybook-button--primary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
|
||||
</button>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders LoaderStory story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
data-testid="loaded-data"
|
||||
>
|
||||
loaded data
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="spy-data"
|
||||
>
|
||||
mockFn return value
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders NewStory story 1`] = `
|
||||
<body>
|
||||
<div
|
||||
style="margin: 3em;"
|
||||
>
|
||||
<button
|
||||
class="storybook-button storybook-button--large storybook-button--primary"
|
||||
style=""
|
||||
@ -107,53 +120,8 @@ exports[`Renders CSF3Primary story 1`] = `
|
||||
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders LoaderStory story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div>
|
||||
<div
|
||||
data-testid="loaded-data"
|
||||
>
|
||||
loaded data
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-testid="spy-data"
|
||||
>
|
||||
mockFn return value
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`Renders NewStory story 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
<div
|
||||
style="margin: 3em;"
|
||||
>
|
||||
<button
|
||||
class="storybook-button storybook-button--large storybook-button--primary"
|
||||
style=""
|
||||
type="button"
|
||||
>
|
||||
foo
|
||||
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
`;
|
||||
|
@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"module": "Preserve",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
|
Loading…
x
Reference in New Issue
Block a user