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

feat: add support for play functions #57

Merged
merged 8 commits into from
May 1, 2023
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
64 changes: 64 additions & 0 deletions examples/vite/src/components/LoginForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<template>
<div class="container">
<form @submit.prevent="handleSubmit">
<label for="email">Email</label>
<input
v-model="email"
id="email"
type="text"
data-testid="email"
/>
<label for="password">Password</label>
<input
v-model="password"
id="password"
type="password"
data-testid="password"
/>
<Button
label="Submit"
type="submit"
primary
></Button>
</form>

<p>
{{ message }}
</p>
</div>
</template>

<script setup>
import { ref } from 'vue'
import Button from './Button.vue'

const message = ref('')
const email = ref('')
const password = ref('')

function handleSubmit() {
if (!email.value) {
message.value = 'Please enter your email'
return
}
if (!password.value) {
message.value = 'Please enter your password'
return
}
message.value =
'Everything is perfect. Your account is ready and we should probably get you started!'
}
</script>

<style scoped>
form > input {
margin: 7px;
}
.container,
form {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Meta, StoryObj } from '@storybook/vue3'

import { userEvent, within } from '@storybook/testing-library'

import { expect } from '@storybook/jest'

import LoginForm from '../components/LoginForm.vue'

const meta: Meta<typeof LoginForm> = {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/vue/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: 'docs/5. Using the play function/classical',
component: LoginForm,
}

export default meta
type Story = StoryObj<typeof meta>

/*
*👇 Render functions are a framework specific feature to allow you control on how the component renders.
* See https://storybook.js.org/docs/vue/api/csf
* to learn how to use render functions.
*/
export const EmptyForm: Story = {
render: () => ({
components: { LoginForm },
template: `<LoginForm />`,
}),
}

/*
* See https://storybook.js.org/docs/vue/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
export const FilledForm: Story = {
render: () => ({
components: { LoginForm },
template: `<LoginForm />`,
}),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement)

// 👇 Simulate interactions with the component
await userEvent.type(canvas.getByTestId('email'), 'email@provider.com')

await userEvent.type(canvas.getByTestId('password'), 'a-random-password')

// See https://storybook.js.org/docs/vue/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
await userEvent.click(canvas.getByRole('button'))

// 👇 Assert DOM structure
await expect(
canvas.getByText(
'Everything is perfect. Your account is ready and we should probably get you started!'
)
).toBeInTheDocument()
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script setup>
import { userEvent, within } from '@storybook/testing-library'
import { expect } from '@storybook/jest'
import LoginForm from '../components/LoginForm.vue'
</script>

<script>
/*
* See https://storybook.js.org/docs/vue/writing-stories/play-function#working-with-the-canvas
* to learn more about using the canvasElement to query the DOM
*/
async function playFunction({ canvasElement }) {
const canvas = within(canvasElement)

// 👇 Simulate interactions with the component
await userEvent.type(canvas.getByTestId('email'), 'email@provider.com')

await userEvent.type(canvas.getByTestId('password'), 'a-random-password')

// See https://storybook.js.org/docs/vue/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
await userEvent.click(canvas.getByRole('button'))

// 👇 Assert DOM structure
await expect(
canvas.getByText(
'Everything is perfect. Your account is ready and we should probably get you started!'
)
).toBeInTheDocument()
}
</script>

<template>
<Stories
title="docs/5. Using the play function/native"
:component="LoginForm"
>
<Story title="Empty Form">
<LoginForm />
</Story>
<Story
title="Filled Form"
:play="playFunction"
>
<LoginForm />
</Story>
</Stories>
</template>
8 changes: 8 additions & 0 deletions src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ interface StoriesProps {
}
type Stories = VueComponent<StoriesProps>

import { Meta, StoryObj } from '@storybook/vue3'

/**
* Story that represents a component example.
*
Expand All @@ -59,6 +61,12 @@ interface StoryProps {
* Display name in the UI.
*/
title: string
/**
* Function that is executed after the story is rendered.
*
* Must be defined in a non-setup script
*/
play?: StoryObj<Meta<VueComponent<any>>>['play']
}
type Story = VueComponent<StoryProps>

Expand Down
12 changes: 12 additions & 0 deletions src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ParsedStory {
id: string
title: string
template: string
play?: string
}

export function parse(code: string) {
Expand Down Expand Up @@ -75,6 +76,8 @@ function parseTemplate(content: string): {
const title = extractTitle(story)
if (!title) throw new Error('Story is missing a title')

const play = extractPlay(story)

const storyTemplate = parseSFC(
story.loc.source
.replace(/<Story/, '<template')
Expand All @@ -85,6 +88,7 @@ function parseTemplate(content: string): {
stories.push({
id: sanitize(title).replace(/[^a-zA-Z0-9]/g, '_'),
title,
play,
template: storyTemplate,
})
}
Expand All @@ -107,6 +111,14 @@ function extractComponent(node: ElementNode) {
: undefined
}

function extractPlay(node: ElementNode) {
const prop = extractProp(node, 'play')
if (prop && prop.type === 7)
return prop.exp?.type === 4
? prop.exp?.content.replace('_ctx.', '')
: undefined
}

// Minimal version of https://github.com/vitejs/vite/blob/57916a476924541dd7136065ceee37ae033ca78c/packages/plugin-vue/src/main.ts#L297
function resolveScript(descriptor: SFCDescriptor) {
if (descriptor.script || descriptor.scriptSetup)
Expand Down
3 changes: 2 additions & 1 deletion src/core/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ function generateDefaultImport(
}

function generateStoryImport(
{ id, title, template }: ParsedStory,
{ id, title, play, template }: ParsedStory,
resolvedScript?: SFCScriptBlock
) {
const { code } = compileTemplate({
Expand All @@ -137,6 +137,7 @@ function generateStoryImport(
${renderFunction}
export const ${id} = () => Object.assign({render: render${id}}, _sfc_main)
${id}.storyName = '${title}'
${play ? `${id}.play = ${play}` : ''}
${id}.parameters = {
docs: { source: { code: \`${template.trim()}\` } },
};`
Expand Down
53 changes: 53 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('transform', () => {
export const Primary = () =>
Object.assign({ render: renderPrimary }, _sfc_main);
Primary.storyName = \\"Primary\\";

Primary.parameters = {
docs: { source: { code: \`hello\` } },
};
Expand All @@ -44,6 +45,7 @@ describe('transform', () => {
export const Primary = () =>
Object.assign({ render: renderPrimary }, _sfc_main);
Primary.storyName = \\"Primary\\";

Primary.parameters = {
docs: { source: { code: \`hello\` } },
};
Expand Down Expand Up @@ -75,6 +77,7 @@ describe('transform', () => {
export const Primary = () =>
Object.assign({ render: renderPrimary }, _sfc_main);
Primary.storyName = \\"Primary\\";

Primary.parameters = {
docs: { source: { code: \`hello\` } },
};
Expand All @@ -98,6 +101,7 @@ describe('transform', () => {
export const Primary_story = () =>
Object.assign({ render: renderPrimary_story }, _sfc_main);
Primary_story.storyName = \\"Primary story\\";

Primary_story.parameters = {
docs: { source: { code: \`hello\` } },
};
Expand All @@ -121,6 +125,7 @@ describe('transform', () => {
export const Primary = () =>
Object.assign({ render: renderPrimary }, _sfc_main);
Primary.storyName = \\"Primary\\";

Primary.parameters = {
docs: { source: { code: \`hello\` } },
};
Expand Down Expand Up @@ -149,6 +154,7 @@ describe('transform', () => {
export const Primary = () =>
Object.assign({ render: renderPrimary }, _sfc_main);
Primary.storyName = \\"Primary\\";

Primary.parameters = {
docs: { source: { code: \`hello\` } },
};
Expand All @@ -159,6 +165,7 @@ describe('transform', () => {
export const Secondary = () =>
Object.assign({ render: renderSecondary }, _sfc_main);
Secondary.storyName = \\"Secondary\\";

Secondary.parameters = {
docs: { source: { code: \`world\` } },
};
Expand Down Expand Up @@ -195,6 +202,7 @@ describe('transform', () => {
export const Primary = () =>
Object.assign({ render: renderPrimary }, _sfc_main);
Primary.storyName = \\"Primary\\";

Primary.parameters = {
docs: { source: { code: \`<Button>\` } },
};
Expand All @@ -207,6 +215,7 @@ describe('transform', () => {
export const Secondary = () =>
Object.assign({ render: renderSecondary }, _sfc_main);
Secondary.storyName = \\"Secondary\\";

Secondary.parameters = {
docs: { source: { code: \`<Button>\` } },
};
Expand Down Expand Up @@ -262,6 +271,7 @@ describe('transform', () => {
export const Primary = () =>
Object.assign({ render: renderPrimary }, _sfc_main);
Primary.storyName = \\"Primary\\";

Primary.parameters = {
docs: { source: { code: \`<test></test>\` } },
};
Expand Down Expand Up @@ -298,6 +308,7 @@ describe('transform', () => {
export const Primary = () =>
Object.assign({ render: renderPrimary }, _sfc_main);
Primary.storyName = \\"Primary\\";

Primary.parameters = {
docs: { source: { code: \`hello\` } },
}; /*@jsxRuntime automatic @jsxImportSource react*/
Expand Down Expand Up @@ -346,4 +357,46 @@ describe('transform', () => {
"
`)
})

it('supports play functions', async () => {
const code = `
<template>
<Stories>
<Story title="Primary" :play="playFunction">
hello
</Story>
</Stories>
</template>

<script lang="ts">
function playFunction({canvasElement}: any) {
console.log("playFunction")
}
</script>
`
const result = await transform(code)
expect(result).toMatchInlineSnapshot(`
"function playFunction({ canvasElement }: any) {
console.log(\\"playFunction\\");
}

const _sfc_main = {};
export default {
//decorators: [ ... ],
parameters: {},
};

function renderPrimary(_ctx, _cache, $props, $setup, $data, $options) {
return \\"hello\\";
}
export const Primary = () =>
Object.assign({ render: renderPrimary }, _sfc_main);
Primary.storyName = \\"Primary\\";
Primary.play = playFunction;
Primary.parameters = {
docs: { source: { code: \`hello\` } },
};
"
`)
})
})