Skip to content

Commit

Permalink
Merge pull request #22685 from storybookjs/main-vue-tests
Browse files Browse the repository at this point in the history
Vue3: Bring back new tests to main
  • Loading branch information
kasperpeulen authored May 24, 2023
2 parents 8c78fcd + 7100975 commit 3d829bc
Show file tree
Hide file tree
Showing 20 changed files with 234 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Meta } from '@storybook/vue3';
import { h } from 'vue';
import Reactivity from './Reactivity.vue';
import * as ReactiveDecorators from './ReactiveDecorators.stories';

const meta = {
...ReactiveDecorators.default,
component: Reactivity,
// storybook render function is not a functional component. it returns a functional component or a component options
render: (args) => {
// create the slot contents as a functional components
const header = ({ title }: { title: string }) => h('h3', `${args.header} - Title: ${title}`);
const defaultSlot = () => h('p', `${args.default}`);
const footer = () => h('p', `${args.footer}`);
// vue render function is a functional components
return () =>
h('div', [
`Custom render uses a functional component, and passes slots to the component:`,
h(Reactivity, args, { header, default: defaultSlot, footer }),
]);
},
} satisfies Meta<typeof Reactivity>;

export default meta;

export {
NoDecorators,
DecoratorFunctionalComponent,
DecoratorFunctionalComponentArgsFromContext,
DecoratorComponentOptions,
DecoratorComponentOptionsArgsFromData,
} from './ReactiveDecorators.stories';
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Meta } from '@storybook/vue3';
import { defineComponent, shallowReactive } from 'vue';
import Reactivity from './Reactivity.vue';
import * as ReactiveDecorators from './ReactiveDecorators.stories';

// when you use custom render, you can use any vue api to create your story and garanti reactivity, otherwise i can ease kill the reactivity.
const state = shallowReactive<{ header: any; default: any; footer: any }>({
header: '',
default: '',
footer: '',
}); // or reactive

const meta = {
...ReactiveDecorators.default,
component: Reactivity,
render: (args, { argTypes }) => {
state.header = args.header;
state.default = args.default;
state.footer = args.footer;
// return a component options
return defineComponent({
data: () => ({ args, header: state.header, default: state.default, footer: state.footer }),
components: {
Reactivity,
},
template: `<div>Custom render uses options api and binds args to data:
<Reactivity v-bind="args">
<template #header="{title}"><h3>{{ args.header }} - Title: {{ title }}</h3></template>
<template #default>{{ args.default }}</template>
<template #footer>{{ args.footer }} </template>
</Reactivity>
</div>`,
});
},
} satisfies Meta<typeof Reactivity>;

export default meta;

export {
NoDecorators,
DecoratorFunctionalComponent,
DecoratorFunctionalComponentArgsFromContext,
DecoratorComponentOptions,
DecoratorComponentOptionsArgsFromData,
} from './ReactiveDecorators.stories';
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { global as globalThis } from '@storybook/global';
import { userEvent, within } from '@storybook/testing-library';
import type { Meta, StoryObj } from '@storybook/vue3';
import { h } from 'vue';
import { RESET_STORY_ARGS, STORY_ARGS_UPDATED, UPDATE_STORY_ARGS } from '@storybook/core-events';
import Reactivity from './Reactivity.vue';

const meta = {
component: Reactivity,
argTypes: {
header: { control: { type: 'text' } },
footer: { control: { type: 'text' } },
default: { control: { type: 'text' } },
},
args: {
label: 'If you see this then the label arg was not reactive.',
default: 'If you see this then the default slot was not reactive.',
header: 'If you see this, the header slot was not reactive.', // this can be useless if you have custom render function that overrides the slot
footer: 'If you see this, the footer slot was not reactive.',
},
play: async ({ canvasElement, id, args }) => {
const channel = (globalThis as any).__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 input = await canvas.findByLabelText<HTMLInputElement>('Some input:');
await userEvent.type(input, 'value');

await channel.emit(UPDATE_STORY_ARGS, {
storyId: id,
updatedArgs: {
label: 'updated label',
header: 'updated header slot', // this can be useless if you have custom render function that overrides the slot which the case here
footer: 'updated footer slot',
default: 'updated default slot',
},
});
await new Promise((resolve) => channel.once(STORY_ARGS_UPDATED, resolve));
},
} satisfies Meta<typeof Reactivity>;

export default meta;
type Story = StoryObj<typeof meta>;

export const NoDecorators: Story = {};

export const DecoratorFunctionalComponent: Story = {
decorators: [
(storyFn, context) => {
const story = storyFn();
return () => h('div', [h('h2', ['Decorator not using args']), [h(story)]]);
},
],
};

export const DecoratorFunctionalComponentArgsFromContext: Story = {
decorators: [
(storyFn, context) => {
const story = storyFn();
return () =>
h('div', [h('h2', ['Decorator using args.label: ', context.args.label]), [h(story)]]);
},
],
};

export const DecoratorComponentOptions: Story = {
decorators: [
(storyFn, context) => {
return {
template: '<div><h2>Decorator not using args</h2><story/></div>',
};
},
],
};

export const DecoratorComponentOptionsArgsFromData: Story = {
decorators: [
(storyFn, context) => {
return {
data: () => ({ args: context.args }),
template: '<div><h2>Decorator using args.label: {{args.label}}</h2><story/></div>',
};
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script setup lang="ts">
defineProps<{ label: string }>();
</script>
<template>
<div style="padding: 20px;background-color: pink;">
<header data-testid="header-slot">
<slot name="header" title="Header title from the slot">
If you see this, the header slot was not reactive.
</slot>
</header>
<div id="content">
<label>
Some input:
<input style='width: 400px' placeholder='If you see this, an args update caused the input field to loose state' />
</label>
<hr>
<button class="storybook-button storybook-button--primary storybook-button--medium"> {{ label
}}</button>
</div>

<main data-testid="default-slot">
<slot>Default slot placeholder</slot>
</main>
<footer data-testid="footer-slot">
<slot name="footer">
Footer slot placeholder
</slot>
</footer>
</div>
</template>

<style>
header,
footer {
background-color: #fff0ff;
padding: 20px;
}
main,
#content {
background-color: #f0f0f0;
padding: 20px;
}
</style>
35 changes: 25 additions & 10 deletions scripts/tasks/sandbox-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,17 +280,17 @@ function addStoriesEntry(mainConfig: ConfigFile, path: string) {
mainConfig.setFieldValue(['stories'], [...stories, entry]);
}

function addVariantToFolder(variant?: string, folder = 'stories') {
function getStoriesFolderWithVariant(variant?: string, folder = 'stories') {
return variant ? `${folder}_${variant}` : folder;
}

// packageDir is eg 'renderers/react', 'addons/actions'
async function linkPackageStories(
packageDir: string,
{ mainConfig, cwd, linkInDir }: { mainConfig: ConfigFile; cwd: string; linkInDir?: string },
frameworkVariant?: string
variant?: string
) {
const storiesFolderName = frameworkVariant ? addVariantToFolder(frameworkVariant) : 'stories';
const storiesFolderName = variant ? getStoriesFolderWithVariant(variant) : 'stories';
const source = join(CODE_DIRECTORY, packageDir, 'template', storiesFolderName);
// By default we link `stories` directories
// e.g '../../../code/lib/store/template/stories' to 'template-stories/lib/store'
Expand All @@ -299,10 +299,7 @@ async function linkPackageStories(
// The files must be linked in the cwd, in order to ensure that any dependencies they
// reference are resolved in the cwd. In particular 'react' resolved by MDX files.
const target = linkInDir
? resolve(
linkInDir,
frameworkVariant ? addVariantToFolder(frameworkVariant, packageDir) : packageDir
)
? resolve(linkInDir, variant ? getStoriesFolderWithVariant(variant, packageDir) : packageDir)
: resolve(cwd, 'template-stories', packageDir);

await ensureSymlink(source, target);
Expand Down Expand Up @@ -373,6 +370,9 @@ export const addStories: Task['run'] = async (
template.expected.renderer.startsWith('@storybook/') &&
template.expected.renderer !== '@storybook/server';

const sandboxSpecificStoriesFolder = key.replaceAll('/', '-');
const storiesVariantFolder = getStoriesFolderWithVariant(sandboxSpecificStoriesFolder);

if (isCoreRenderer) {
// Link in the template/components/index.js from store, the renderer and the addons
const rendererPath = await workspacePath('renderer', template.expected.renderer);
Expand All @@ -388,6 +388,22 @@ export const addStories: Task['run'] = async (
cwd,
linkInDir: resolve(cwd, storiesPath),
});

if (
await pathExists(
resolve(CODE_DIRECTORY, rendererPath, join('template', storiesVariantFolder))
)
) {
await linkPackageStories(
rendererPath,
{
mainConfig,
cwd,
linkInDir: resolve(cwd, storiesPath),
},
sandboxSpecificStoriesFolder
);
}
}

const isCoreFramework = template.expected.framework.startsWith('@storybook/');
Expand All @@ -404,8 +420,7 @@ export const addStories: Task['run'] = async (
});
}

const frameworkVariant = key.split('/')[1];
const storiesVariantFolder = addVariantToFolder(frameworkVariant);
console.log({ sandboxSpecificStoriesFolder, storiesVariantFolder });

if (
await pathExists(
Expand All @@ -419,7 +434,7 @@ export const addStories: Task['run'] = async (
cwd,
linkInDir: resolve(cwd, storiesPath),
},
frameworkVariant
sandboxSpecificStoriesFolder
);
}
}
Expand Down

0 comments on commit 3d829bc

Please sign in to comment.