Skip to content

Commit

Permalink
feat: add support for play functions (#57)
Browse files Browse the repository at this point in the history
* Add suport for play function

* Added test for play function support

* Fixed typo in test name

* Added play to StoryProps

* Update test/index.test.ts

* Added examples

* Moved code into new line and updated tests
  • Loading branch information
AntarEspadas authored May 1, 2023
1 parent 4993db7 commit e309234
Show file tree
Hide file tree
Showing 7 changed files with 246 additions and 1 deletion.
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 protected]')

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 protected]')
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\` } },
};
"
`)
})
})

0 comments on commit e309234

Please sign in to comment.