Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue: Return component from composeStory #26317

Merged
merged 4 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 33 additions & 13 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- [Project annotations are now merged instead of overwritten in composeStory](#project-annotations-are-now-merged-instead-of-overwritten-in-composestory)
- [Type change in `composeStories` API](#type-change-in-composestories-api)
- [DOM structure changed in portable stories](#dom-structure-changed-in-portable-stories)
- [Composed Vue stories are now components instead of functions](#composed-vue-stories-are-now-components-instead-of-functions)
- [Tab addons are now routed to a query parameter](#tab-addons-are-now-routed-to-a-query-parameter)
- [Default keyboard shortcuts changed](#default-keyboard-shortcuts-changed)
- [Manager addons are now rendered with React 18](#manager-addons-are-now-rendered-with-react-18)
Expand Down Expand Up @@ -89,17 +90,17 @@
- [Tab addons cannot manually route, Tool addons can filter their visibility via tabId](#tab-addons-cannot-manually-route-tool-addons-can-filter-their-visibility-via-tabid)
- [Removed `config` preset](#removed-config-preset-1)
- [From version 7.5.0 to 7.6.0](#from-version-750-to-760)
- [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated)
- [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated)
- [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated)
- [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop)
- [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react)
- [CommonJS with Vite is deprecated](#commonjs-with-vite-is-deprecated)
- [Using implicit actions during rendering is deprecated](#using-implicit-actions-during-rendering-is-deprecated)
- [typescript.skipBabel deprecated](#typescriptskipbabel-deprecated)
- [Primary doc block accepts of prop](#primary-doc-block-accepts-of-prop)
- [Addons no longer need a peer dependency on React](#addons-no-longer-need-a-peer-dependency-on-react)
- [From version 7.4.0 to 7.5.0](#from-version-740-to-750)
- [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated)
- [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers)
- [`storyStoreV6` and `storiesOf` is deprecated](#storystorev6-and-storiesof-is-deprecated)
- [`storyIndexers` is replaced with `experimental_indexers`](#storyindexers-is-replaced-with-experimental_indexers)
- [From version 7.0.0 to 7.2.0](#from-version-700-to-720)
- [Addon API is more type-strict](#addon-api-is-more-type-strict)
- [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated)
- [Addon API is more type-strict](#addon-api-is-more-type-strict)
- [Addon-controls hideNoControlsWarning parameter is deprecated](#addon-controls-hidenocontrolswarning-parameter-is-deprecated)
- [From version 6.5.x to 7.0.0](#from-version-65x-to-700)
- [7.0 breaking changes](#70-breaking-changes)
- [Dropped support for Node 15 and below](#dropped-support-for-node-15-and-below)
Expand All @@ -125,7 +126,7 @@
- [Deploying build artifacts](#deploying-build-artifacts)
- [Dropped support for file URLs](#dropped-support-for-file-urls)
- [Serving with nginx](#serving-with-nginx)
- [Ignore story files from node_modules](#ignore-story-files-from-node_modules)
- [Ignore story files from node\_modules](#ignore-story-files-from-node_modules)
- [7.0 Core changes](#70-core-changes)
- [7.0 feature flags removed](#70-feature-flags-removed)
- [Story context is prepared before for supporting fine grained updates](#story-context-is-prepared-before-for-supporting-fine-grained-updates)
Expand All @@ -139,7 +140,7 @@
- [Addon-interactions: Interactions debugger is now default](#addon-interactions-interactions-debugger-is-now-default)
- [7.0 Vite changes](#70-vite-changes)
- [Vite builder uses Vite config automatically](#vite-builder-uses-vite-config-automatically)
- [Vite cache moved to node_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook)
- [Vite cache moved to node\_modules/.cache/.vite-storybook](#vite-cache-moved-to-node_modulescachevite-storybook)
- [7.0 Webpack changes](#70-webpack-changes)
- [Webpack4 support discontinued](#webpack4-support-discontinued)
- [Babel mode v7 exclusively](#babel-mode-v7-exclusively)
Expand Down Expand Up @@ -189,7 +190,7 @@
- [Dropped addon-docs manual babel configuration](#dropped-addon-docs-manual-babel-configuration)
- [Dropped addon-docs manual configuration](#dropped-addon-docs-manual-configuration)
- [Autoplay in docs](#autoplay-in-docs)
- [Removed STORYBOOK_REACT_CLASSES global](#removed-storybook_react_classes-global)
- [Removed STORYBOOK\_REACT\_CLASSES global](#removed-storybook_react_classes-global)
- [7.0 Deprecations and default changes](#70-deprecations-and-default-changes)
- [storyStoreV7 enabled by default](#storystorev7-enabled-by-default)
- [`Story` type deprecated](#story-type-deprecated)
Expand Down Expand Up @@ -467,6 +468,25 @@ test("snapshots the story with custom id", () => {
});
```

#### Composed Vue stories are now components instead of functions

`composeStory` (and `composeStories`) from `@storybook/vue3` now returns Vue components rather than story functions that return components. This means that when rendering these composed stories you just pass the composed story _without_ first calling it.
yannbf marked this conversation as resolved.
Show resolved Hide resolved

Previously when using `composeStory` from `@storybook/testing-vue` you would render composed stories with eg. `render(MyStoryComposedStory({ someProps: true}))`. That is now changed to more [closely match how you would render regular Vue components](https://testing-library.com/docs/vue-testing-library/examples). Here's an example using `@testing-library/vue` and Vitest:

```diff
import { it } from 'vitest';
import { render } from '@testing-library/vue';
import * as stories from './Button.stories';
import { composeStory } from '@storybook/vue3';

it('renders primary button', () => {
const ComposedButton = composeStory(sotries.Primary);
- render(ComposedButton({ label: 'Hello world' }));
+ render(ComposedButton, { props: { label: 'Hello world' } });
});
```

### Tab addons are now routed to a query parameter

The URL of a tab used to be: `http://localhost:6006/?path=/my-addon-tab/my-story`.
Expand Down Expand Up @@ -1061,7 +1081,7 @@ The `hideNoControlsWarning` parameter is now removed. [More info here](#addon-co
The `setGlobalConfig` (used for reusing stories in your tests) is now removed in favor of `setProjectAnnotations`.

```ts
import { setProjectAnnotations } from `@storybook/testing-react`.
import { setProjectAnnotations } from `@storybook/react`.
```

#### StorybookViteConfig type from @storybook/builder-vite
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,34 @@ const Secondary = composeStory(stories.CSF2Secondary, stories.default);

describe('renders', () => {
it('renders primary button', () => {
render(CSF3Primary({ label: 'Hello world' }));
render(CSF3Primary, { props: { label: 'Hello world' } });
const buttonElement = screen.getByText(/Hello world/i);
expect(buttonElement).toBeInTheDocument();
});

it('reuses args from composed story', () => {
render(Secondary());
render(Secondary);
const buttonElement = screen.getByRole('button');
expect(buttonElement.textContent).toEqual(Secondary.args.label);
});

it('myClickEvent handler is called', async () => {
const myClickEventSpy = vi.fn();
render(Secondary({ onMyClickEvent: myClickEventSpy }));
render(Secondary, { props: { onMyClickEvent: myClickEventSpy } });
const buttonElement = screen.getByRole('button');
buttonElement.click();
expect(myClickEventSpy).toHaveBeenCalled();
});

it('reuses args from composeStories', () => {
const { getByText } = render(CSF3Primary());
const { getByText } = render(CSF3Primary);
const buttonElement = getByText(/foo/i);
expect(buttonElement).toBeInTheDocument();
});

it('should call and compose loaders data', async () => {
await LoaderStory.load();
const { getByTestId } = render(LoaderStory());
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
Expand All @@ -63,7 +63,7 @@ describe('projectAnnotations', () => {
},
]);
const WithEnglishText = composeStory(stories.CSF2StoryWithLocale, stories.default);
const { getByText } = render(WithEnglishText());
const { getByText } = render(WithEnglishText);
const buttonElement = getByText('Hello!');
expect(buttonElement).toBeInTheDocument();
});
Expand All @@ -72,7 +72,7 @@ describe('projectAnnotations', () => {
const WithPortugueseText = composeStory(stories.CSF2StoryWithLocale, stories.default, {
globals: { locale: 'pt' },
});
const { getByText } = render(WithPortugueseText());
const { getByText } = render(WithPortugueseText);
const buttonElement = getByText('Olá!');
expect(buttonElement).toBeInTheDocument();
});
Expand All @@ -88,22 +88,22 @@ describe('CSF3', () => {
it('renders with inferred globalRender', () => {
const Primary = composeStory(stories.CSF3Button, stories.default);

render(Primary({ label: 'Hello world' }));
render(Primary, { props: { label: 'Hello world' } });
const buttonElement = screen.getByText(/Hello world/i);
expect(buttonElement).toBeInTheDocument();
});

it('renders with custom render function', () => {
const Primary = composeStory(stories.CSF3ButtonWithRender, stories.default);

render(Primary());
render(Primary);
expect(screen.getByTestId('custom-render')).toBeInTheDocument();
});

it('renders with play function', async () => {
const CSF3InputFieldFilled = composeStory(stories.CSF3InputFieldFilled, stories.default);

const { container } = render(CSF3InputFieldFilled());
const { container } = render(CSF3InputFieldFilled);

await CSF3InputFieldFilled.play!({ canvasElement: container as HTMLElement });

Expand All @@ -122,7 +122,7 @@ it('should pass with decorators that need addons channel', () => {
},
],
});
render(PrimaryWithChannels({ label: 'Hello world' }));
render(PrimaryWithChannels, { props: { label: 'Hello world' } });
const buttonElement = screen.getByText(/Hello world/i);
expect(buttonElement).not.toBeNull();
});
Expand Down
11 changes: 10 additions & 1 deletion code/renderers/vue3/src/portable-stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
Store_CSFExports,
StoriesWithPartialProps,
} from '@storybook/types';
import { h } from 'vue';

import * as vueProjectAnnotations from './entry-preview';
import type { Meta } from './public-types';
Expand Down Expand Up @@ -84,13 +85,21 @@ export function composeStory<TArgs extends Args = Args>(
projectAnnotations?: ProjectAnnotations<VueRenderer>,
exportsName?: string
) {
return originalComposeStory<VueRenderer, TArgs>(
const composedStory = originalComposeStory<VueRenderer, TArgs>(
yannbf marked this conversation as resolved.
Show resolved Hide resolved
story as StoryAnnotationsOrFn<VueRenderer, Args>,
componentAnnotations,
projectAnnotations,
defaultProjectAnnotations,
exportsName
);

// Returning h(composedStory) instead makes it an actual Vue component renderable by @testing-library/vue, Playwright CT, etc.
const renderable = (...args: Parameters<typeof composedStory>) => h(composedStory(...args));
Object.assign(renderable, composedStory);

// typing this as newable means TS allows it to be used as a JSX element
// TODO: we should do the same for composeStories as well
return renderable as unknown as typeof composedStory & { new (...args: any[]): any };
}

/**
Expand Down
Loading