diff --git a/code/lib/store/template/stories/rendering.stories.ts b/code/lib/store/template/stories/rendering.stories.ts index 395d94ea4424..71e2f1a025cd 100644 --- a/code/lib/store/template/stories/rendering.stories.ts +++ b/code/lib/store/template/stories/rendering.stories.ts @@ -9,14 +9,14 @@ export default { label: 'Click me', }, }; - +// this is the story should be remove it will always fail, because forceReRender Api is removed in V7 export const ForceReRender = { play: async ({ canvasElement }: PlayFunctionContext) => { const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; const button = await within(canvasElement).findByRole('button'); await button.focus(); await expect(button).toHaveFocus(); - + // forceRender is not called in V7 // By forcing the component to rerender, we reset the focus state await channel.emit('forceReRender'); await waitFor(() => expect(button).not.toHaveFocus()); diff --git a/code/renderers/vue3/src/render.ts b/code/renderers/vue3/src/render.ts index 2af91f011291..0f4b72d12e25 100644 --- a/code/renderers/vue3/src/render.ts +++ b/code/renderers/vue3/src/render.ts @@ -1,5 +1,6 @@ +/* eslint-disable no-param-reassign */ import { dedent } from 'ts-dedent'; -import { createApp, h } from 'vue'; +import { createApp, h, reactive } from 'vue'; import type { RenderContext, ArgsStoryFn } from '@storybook/types'; import type { Args, StoryContext } from '@storybook/csf'; @@ -21,26 +22,27 @@ export const setup = (fn: (app: any) => void) => { setupFunction = fn; }; -const map = new Map>(); +const map = new Map< + VueRenderer['canvasElement'], + { vueApp: ReturnType; reactiveArgs: any } +>(); export function renderToCanvas( - { title, name, storyFn, showMain, showError, showException }: RenderContext, + { + storyFn, + name, + showMain, + showError, + showException, + id, + title, + forceRemount, + storyContext, + }: RenderContext, canvasElement: VueRenderer['canvasElement'] ) { // TODO: explain cyclical nature of these app => story => mount - let element: StoryFnVueReturnType; - const storybookApp = createApp({ - unmounted() { - map.delete(canvasElement); - }, - render() { - map.set(canvasElement, storybookApp); - setupFunction(storybookApp); - return h(element); - }, - }); - storybookApp.config.errorHandler = (e: unknown) => showException(e as Error); - element = storyFn(); + const element: StoryFnVueReturnType = storyFn(); if (!element) { showError({ @@ -50,15 +52,38 @@ export function renderToCanvas( Use "() => ({ template: '' })" or "() => ({ components: MyComp, template: '' })" when defining the story. `, }); - return; + return () => {}; } - showMain(); + const storyArgs = element.props || (element as any).render?.().props || storyContext.args || {}; - map.get(canvasElement)?.unmount(); + const existingApp = map.get(canvasElement); + + if (existingApp && !forceRemount) { + updateArgs(existingApp.reactiveArgs, storyArgs); + return () => { + teardown(existingApp.vueApp, canvasElement); + }; + } + + const reactiveArgs = reactive(storyArgs) as Args; + const storybookApp = createApp({ + render() { + map.set(canvasElement, { vueApp: storybookApp, reactiveArgs }); + return h(element, reactiveArgs); + }, + }); + storybookApp.config.errorHandler = (e: unknown) => showException(e as Error); + setupFunction(storybookApp); storybookApp.mount(canvasElement); + + showMain(); + return () => { + teardown(storybookApp, canvasElement); + }; } + /** * get the slots as functions to be rendered * @param props @@ -73,3 +98,25 @@ function getSlots(props: Args, context: StoryContext) { return Object.fromEntries(slots); } + +/** + * update the reactive args + * @param reactiveArgs + * @param nextArgs + * @returns + */ +function updateArgs(reactiveArgs: Args, nextArgs: Args) { + if (!nextArgs) return; + Object.keys(reactiveArgs).forEach((key) => { + delete reactiveArgs[key]; + }); + Object.assign(reactiveArgs, nextArgs); +} + +function teardown( + storybookApp: ReturnType, + canvasElement: VueRenderer['canvasElement'] +) { + storybookApp?.unmount(); + if (map.has(canvasElement)) map.delete(canvasElement); +} diff --git a/code/renderers/vue3/template/stories/ReactiveArgs.stories.js b/code/renderers/vue3/template/stories/ReactiveArgs.stories.js new file mode 100644 index 000000000000..2aa7cc555eda --- /dev/null +++ b/code/renderers/vue3/template/stories/ReactiveArgs.stories.js @@ -0,0 +1,44 @@ +import { expect } from '@storybook/jest'; +import { global as globalThis } from '@storybook/global'; +import { within, userEvent } from '@storybook/testing-library'; +import { UPDATE_STORY_ARGS, STORY_ARGS_UPDATED, RESET_STORY_ARGS } from '@storybook/core-events'; +import ReactiveArgs from './ReactiveArgs.vue'; + +export default { + component: ReactiveArgs, + argTypes: { + // To show that other props are passed through + backgroundColor: { control: 'color' }, + }, +}; + +export const ReactiveTest = { + args: { + label: 'Button', + }, + // test that args are updated correctly in rective mode + play: async ({ canvasElement, id }) => { + const channel = globalThis.__STORYBOOK_ADDONS_CHANNEL__; + const canvas = within(canvasElement); + + await channel.emit(RESET_STORY_ARGS, { storyId: id }); + await new Promise((resolve) => { + channel.once(STORY_ARGS_UPDATED, resolve); + }); + const reactiveButton = await canvas.getByRole('button'); + await expect(reactiveButton).toHaveTextContent('Button 0'); + + await userEvent.click(reactiveButton); // click to update the label to increment the count + 1 + await channel.emit(UPDATE_STORY_ARGS, { + storyId: id, + updatedArgs: { label: 'updated' }, + }); + await new Promise((resolve) => { + channel.once(STORY_ARGS_UPDATED, resolve); + }); + await expect(canvas.getByRole('button')).toHaveTextContent('updated 1'); + + await userEvent.click(reactiveButton); // click to update the label to increment the count + 1 + await expect(reactiveButton).toHaveTextContent('updated 2'); + }, +}; diff --git a/code/renderers/vue3/template/stories/ReactiveArgs.vue b/code/renderers/vue3/template/stories/ReactiveArgs.vue new file mode 100644 index 000000000000..ebf97a55cb73 --- /dev/null +++ b/code/renderers/vue3/template/stories/ReactiveArgs.vue @@ -0,0 +1,40 @@ + + +