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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions code/e2e-tests/framework-vue3.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { expect, test } from '@playwright/test';
import process from 'process';

import { SbPage } from './util';

const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:6006';
const templateName = process.env.STORYBOOK_TEMPLATE_NAME;

test.describe('Vue 3', () => {
test.beforeEach(async ({ page }) => {
await page.goto(storybookUrl);
await new SbPage(page, expect).waitUntilLoaded();
});

test.skip(!templateName?.includes('vue3'), 'Only run these tests on Vue 3');

test('updateArgs works in decorators', async ({ page }) => {
const sbPage = new SbPage(page, expect);

await sbPage.navigateToStory(
'stories/renderers/vue3_vue3-vite-default-ts/decorators',
'update-args'
);
const previewRoot = sbPage.previewRoot();
const button = previewRoot.getByRole('button', { name: 'Add 1' });

await expect(previewRoot).toContainText('0');
await button.click();
await expect(previewRoot).toContainText('1');
await button.click();
await expect(previewRoot).toContainText('2');
});

test('Decorators can consume reactive globals', async ({ page }) => {
const sbPage = new SbPage(page, expect);

await sbPage.navigateToStory(
'stories/renderers/vue3_vue3-vite-default-ts/decorators',
'reactive-global-decorator'
);

// Check the original language
await expect(sbPage.previewRoot()).toContainText('Hello');

// Select spanish in the locale toolbar and check that the text changes
await sbPage.selectToolbar('[aria-label^="Internationalization locale"]', 'text=/Español/');
await expect(sbPage.previewRoot()).toContainText('Hola');
});
});
29 changes: 25 additions & 4 deletions code/renderers/vue3/src/render.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { describe, expect, it } from 'vitest';

import type { Args, Globals } from 'storybook/internal/types';

import { expectTypeOf } from 'expect-type';
import { reactive } from 'vue';
import { computed, reactive } from 'vue';

import { updateArgs } from './render';

Expand All @@ -23,7 +25,7 @@ describe('Render Story', () => {
expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>();

const newArgs = { argFoo: 'foo2', argBar: 'bar2' };
updateArgs(reactiveArgs, newArgs);
updateArgs<Args>(reactiveArgs, newArgs);
expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string; argBar: string } }>();
expect(reactiveArgs).toEqual({
argFoo: 'foo2',
Expand All @@ -37,7 +39,7 @@ describe('Render Story', () => {
expectTypeOf(reactiveArgs).toEqualTypeOf<{ objectArg: { argFoo: string } }>();

const newArgs = { argFoo: 'foo2', argBar: 'bar2' };
updateArgs(reactiveArgs, newArgs);
updateArgs<Args>(reactiveArgs, newArgs);
expect(reactiveArgs).toEqual({ argFoo: 'foo2', argBar: 'bar2' });
});

Expand All @@ -53,7 +55,7 @@ describe('Render Story', () => {
}>();

const newArgs = { argFoo: 'foo2', argBar: 'bar2' };
updateArgs(reactiveArgs, newArgs);
updateArgs<Args>(reactiveArgs, newArgs);

expect(reactiveArgs).toEqual({
argFoo: 'foo2',
Expand Down Expand Up @@ -88,4 +90,23 @@ describe('Render Story', () => {

expect(reactiveArgs).toEqual({ objectArg: { argFoo: 'bar' } });
});

it('update reactive Globals', async () => {
const reactiveGlobals = reactive<Globals>({ theme: 'light', locale: 'en' });

let observedTheme: string | undefined;
const watcher = computed(() => {
observedTheme = reactiveGlobals.theme as string;
return reactiveGlobals.theme;
});

expect(watcher.value).toBe('light');
expect(observedTheme).toBe('light');

updateArgs<Globals>(reactiveGlobals, { theme: 'dark', locale: 'en' });

expect(watcher.value).toBe('dark');
expect(observedTheme).toBe('dark');
expect(reactiveGlobals).toEqual({ theme: 'dark', locale: 'en' });
});
});
23 changes: 17 additions & 6 deletions code/renderers/vue3/src/render.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/* eslint-disable local-rules/no-uncategorized-errors */
import type { Args, ArgsStoryFn, RenderContext, StoryContext } from 'storybook/internal/types';
import type { Globals } from 'storybook/internal/types';
import {
type Args,
type ArgsStoryFn,
type RenderContext,
type StoryContext,
} from 'storybook/internal/types';

import type { PreviewWeb } from 'storybook/preview-api';
import type { App } from 'vue';
Expand Down Expand Up @@ -39,6 +45,7 @@ const map = new Map<
{
vueApp: ReturnType<typeof createApp>;
reactiveArgs: Args;
reactiveGlobals: Globals;
}
>();

Expand All @@ -56,7 +63,8 @@ export async function renderToCanvas(
const element = storyFn(); // call the story function to get the root element with all the decorators
const args = getArgs(element, storyContext); // get args in case they are altered by decorators otherwise use the args from the context

updateArgs(existingApp.reactiveArgs, args);
updateArgs<Args>(existingApp.reactiveArgs, args);
updateArgs<Globals>(existingApp.reactiveGlobals, storyContext.globals);
return () => {
teardown(existingApp.vueApp, canvasElement);
};
Expand All @@ -66,20 +74,19 @@ export async function renderToCanvas(
teardown(existingApp.vueApp, canvasElement);
}

// create vue app for the story

// create vue app for the story
const vueApp = createApp({
setup() {
storyContext.args = reactive(storyContext.args);
storyContext.globals = reactive(storyContext.globals);
const rootElement = storyFn(); // call the story function to get the root element with all the decorators
const args = getArgs(rootElement, storyContext); // get args in case they are altered by decorators otherwise use the args from the context
const appState = {
vueApp,
reactiveArgs: reactive(args),
reactiveGlobals: storyContext.globals,
};
map.set(canvasElement, appState);

return () => {
// not passing args here as props
// treat the rootElement as a component without props
Expand Down Expand Up @@ -141,7 +148,11 @@ function getArgs(element: StoryFnVueReturnType, storyContext: StoryContext<VueRe
* @param nextArgs
* @returns
*/
export function updateArgs(reactiveArgs: Args, nextArgs: Args) {
export function updateArgs<
T extends {
[name: string]: unknown;
},
>(reactiveArgs: T, nextArgs: T) {
if (Object.keys(nextArgs).length === 0) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import type { DecoratorFunction } from 'storybook/internal/types';
import { global as globalThis } from '@storybook/global';
import type { Meta, StoryObj, VueRenderer } from '@storybook/vue3';

import { useArgs } from 'storybook/preview-api';
import { h } from 'vue';
import { computed } from 'vue';

const { Button, Pre } = (globalThis as any).__TEMPLATE_COMPONENTS__;

Expand Down Expand Up @@ -47,6 +49,62 @@ const DynamicWrapperWrapper: DecoratorFunction<VueRenderer> = (storyFn, { args }
computed: { level: () => `${args.level}px` },
});

const getCaptionForLocale = (locale: string) => {
switch (locale) {
case 'es':
return 'Hola!';
case 'kr':
return '안녕하세요!';
case 'zh':
return '你好!';
case 'en':
return 'Hello!';
default:
return undefined;
}
};

const updateArgsDecorator: DecoratorFunction<VueRenderer> = (story, { args }) => {
const [, updateArgs] = useArgs();
return {
components: { story },
setup() {
return {
args,
updateArgs,
};
},
template: `
<div>
<button @click="() => updateArgs({ label: Number(args.label) + 1 })">Add 1</button>
<hr />
<story />
</div>
`,
};
};

const localeDecorator: DecoratorFunction<VueRenderer> = (story, { globals }) => {
return {
components: { story },
setup() {
const ctxGreeting = computed(() => getCaptionForLocale(globals?.locale) || 'Hello!');

return {
ctxGreeting,
globals,
};
},
template: `
<div>
<p>Greeting: {{ctxGreeting}}</p>
<p>Locale: {{globals?.locale}}</p>
<story />
</div>
`,
};
};
Comment thread
Sidnioulz marked this conversation as resolved.

export const ComponentTemplate: Story = {
args: { label: 'With component' },
decorators: [ComponentTemplateWrapper],
Expand Down Expand Up @@ -84,3 +142,13 @@ export const MultipleWrappers = {
DynamicWrapperWrapper,
],
};

export const UpdateArgs = {
args: { label: '0' },
decorators: [updateArgsDecorator],
};

export const ReactiveGlobalDecorator = {
args: { label: 'With reactive global decorator' },
decorators: [localeDecorator],
};
52 changes: 52 additions & 0 deletions docs/_snippets/decorator-with-reactive-globals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
```js filename=".storybook/preview.js" renderer="vue" language="js"
import { computed } from 'vue';

export default {
decorators: [
(story, { globals }) => {
return {
components: { story },
setup() {
const greeting = computed(() => globals?.locale === 'en' ? 'Hello!' : '¡Hola!');

return { greeting, globals };
},
template: `
<div :lang={{globals?.locale || 'en'}}>
<p>Greeting: {{greeting}}</p>
<story />
</div>
`,
};
},
],
};
```

```ts filename=".storybook/preview.ts" renderer="vue" language="ts"
import { computed } from 'vue';
import type { Preview } from '@storybook/vue3-vite';

const preview: Preview = {
decorators: [
(story, { globals }) => {
return {
components: { story },
setup() {
const greeting = computed(() => globals?.locale === 'en' ? 'Hello!' : '¡Hola!');

return { greeting, globals };
},
template: `
<div :lang={{globals?.locale || 'en'}}>
<p>Greeting: {{greeting}}</p>
<story />
</div>
`,
};
},
],
};

export default preview;
```
58 changes: 58 additions & 0 deletions docs/_snippets/decorator-with-updateArgs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
```js filename=".storybook/preview.js" renderer="vue" language="js"
import { useArgs } from 'storybook/preview-api';

const WithIncrementDecorator = {
args: {
counter: 0,
},
decorators: [
(story, { args }) => {
const [, updateArgs] = useArgs();
return {
components: { story },
setup() {
return { args, updateArgs };
},
template: `
<div>
<button @click="() => updateArgs({ counter: args.counter + 1 })">
Increment
</button>
<story />
</div>
`,
};
},
],
};
```

```ts filename=".storybook/preview.ts" renderer="vue" language="ts"
import { useArgs } from 'storybook/preview-api';
import type { Meta, StoryObj } from '@storybook/vue3';

const WithIncrementDecorator: StoryObj<Meta<typeof MyComponent>> = {
args: {
counter: 0,
},
decorators: [
(story, { args }) => {
const [, updateArgs] = useArgs();
return {
components: { story },
setup() {
return { args, updateArgs };
},
template: `
<div>
<button @click="() => updateArgs({ counter: args.counter + 1 })">
Increment
</button>
<story />
</div>
`,
};
},
],
};
```
Loading
Loading