Javascript is required
Β·
8 min read
Β·
12861 views

Document & Test Vue 3 Components With Storybook

Document & Test Vue 3 Components With Storybook Image

Storybook is my tool of choice for UI component documentation. Vue.js is very well supported in the Storybook ecosystem and has first-class integrations with Vuetify and NuxtJS. It also has official support for Vue 3, the latest major installment of Vue.js.

This article will demonstrate how you can set up Storybook with zero-config and built-in TypeScript support, auto-generate controls & documentation, and perform automated snapshot tests for your Vue components.

Why Storybook?

We have components that can have many props, states, slots, etc., which influences its visual representation and more.

This circumstance causes some typical problems for any front-end developer:

  • How can I create documentation for my component that doesn't get outdated?
  • How can I get an overview of all different states and kinds of my component?
  • How can I guarantee that my changes don't influence other states and kinds?
  • How can I show the current implementation to non-developer team members?

Storybook will help us here.

Storybook Setup

First, we need to create a Vue 3 application. We'll use Vite, a new build tool from Evan You, the creator of Vue.js:

bash
npm init vite@latest

Setting up Storybook in an existing Vue 3 project can be done with zero configuration:

bash
npx sb init

This command installs Storybook with its dependencies, configures the Storybook instance, and generates some demo components and stories which are located at src/stories:

Storybook Vue 3 Generated Files

We can now run the following command, which starts a local development server for Storybook and automatically opens it in a new browser tab:

bash
npm run storybook

Storybook Vue 3 Demo

These generated Vue components and stories are good examples of how to write Vue 3 stories. I want to show you some advanced documentation examples using a custom component.

Custom Component Demo

I created a Counter.vue demo component to demonstrate the Storybook integration for this article. The source code is available at GitHub.

The component provides basic counter functionality, has two different visual variants and two slots for custom content.

Let's take a look at the component's code:

1<template>
2  <p>{{ label }}</p>
3  <!-- @slot Slot to show content below label -->
4  <slot name="sub-label" />
5  <div class="container" :class="variant">
6    <button @click="increment()">+</button>
7    <p class="value">{{ count }}</p>
8    <button @click="decrement()">-</button>
9  </div>
10  <!-- @slot Default slot to show any content below the counter -->
11  <slot />
12</template>
13
14<script lang="ts">
15import { ref, watch, PropType } from 'vue'
16import { Variant } from './types'
17
18/**
19 * This is my amazing counter component
20 *
21 * It can increment and decrement!
22 */
23export default {
24  props: {
25    /**
26     * The initial value for the counter
27     */
28    initialValue: {
29      type: Number,
30      default: 0,
31    },
32    /**
33     * Text shown above the counter
34     */
35    label: {
36      type: String,
37      default: 'Counter',
38    },
39    /**
40     * If true, the counter can show negative numbers
41     */
42    allowNegativeValues: {
43      type: Boolean,
44      default: false,
45    },
46    /**
47     * Defines the visual appearance of the counter
48     */
49    variant: {
50      type: String as PropType<Variant>,
51      default: Variant.Default,
52    },
53  },
54  emits: ['counter-update'],
55  setup(props, context) {
56    const count = ref(props.initialValue)
57
58    const increment = () => {
59      count.value += 1
60    }
61
62    const decrement = () => {
63      const newValue = count.value - 1
64      if (newValue < 0 && !props.allowNegativeValues) {
65        count.value = 0
66      } else {
67        count.value -= 1
68      }
69    }
70
71    watch(count, (value) => {
72      context.emit('counter-update', value)
73    })
74
75    return {
76      count,
77      increment,
78      decrement,
79    }
80  },
81}
82</script>
83<style scoped></style>

In the above code, you can see that I've annotated the Vue component with JSDoc comments. Storybook converts them into living documentation alongside our stories.

Warning

Unfortunately, I found no way to add JSDoc comments to the counter-update event. I think it is currently not supported in vue-docgen-api, which Storybook uses under the hood to extract code comments into descriptions. Leave a comment if you know a way how to document events in Vue 3.

Storybook uses so-called stories:

A story captures the rendered state of a UI component. Developers write multiple stories per component that describe all the β€œinteresting” states a component can support.

A component’s stories are defined in a story file that lives alongside the component file. The story file is for development-only, it won't be included in your production bundle.

Now, let's take a look at the code of our Counter.stories.ts:

1import Counter from './Counter.vue'
2import { Variant } from './types'
3
4//πŸ‘‡ This default export determines where your story goes in the story list
5export default {
6  title: 'Counter',
7  component: Counter,
8  //πŸ‘‡ Creates specific argTypes with options
9  argTypes: {
10    variant: {
11      options: Variant,
12    },
13  },
14}
15
16//πŸ‘‡ We create a β€œtemplate” of how args map to rendering
17const Template = (args) => ({
18  components: { Counter },
19  setup() {
20    //πŸ‘‡ The args will now be passed down to the template
21    return { args }
22  },
23  template: '<Counter v-bind="args">{{ args.slotContent }}</Counter>',
24})
25
26//πŸ‘‡ Each story then reuses that template
27export const Default = Template.bind({})
28Default.args = {
29  label: 'Default',
30}
31
32export const Colored = Template.bind({})
33Colored.args = {
34  label: 'Colored',
35  variant: Variant.Colored,
36}
37
38export const NegativeValues = Template.bind({})
39NegativeValues.args = {
40  allowNegativeValues: true,
41  initialValue: -1,
42}
43
44export const Slot = Template.bind({})
45Slot.args = {
46  slotContent: 'SLOT CONTENT',
47}

This code is written in Component Story Format and generates four stories:

  • Default: The counter component in its default state
  • Colored: The counter component in the colored variation
  • NegativeValue: The counter component that allows negative values
  • Slot: The counter component with a slot content

Let's take a look at our living documentation in Storybook:

Storybook Demo Running

As already mentioned, Storybook converts the JSDoc comments from our code snippet above into documentation, shown in the following picture:

Storybook Generated Docs

Testing

Now that we have our living documentation in Storybook we can run tests against them.

Jest Setup

I chose Jest as the test runner. It has a fast & straightforward setup process and includes a test runner, an assertion library, and a DOM implementation to mount our Vue components.

To install Jest in our existing Vue 3 + Vite project, we need to run the following command:

bash
npm install jest @types/jest ts-jest vue-jest@next @vue/test-utils@next --save-dev

Then we need to create a jest.config.js config file in the root directory:

1module.exports = {
2  moduleFileExtensions: ['js', 'ts', 'json', 'vue'],
3  transform: {
4    '^.+\\.ts$': 'ts-jest',
5    '^.+\\.vue$': 'vue-jest',
6  },
7  collectCoverage: true,
8  collectCoverageFrom: ['/src/**/*.vue'],
9}

The next step is to add a script that executes the tests in our package.json:

json
1"scripts": {
2  "test": "jest src"
3}

Unit testing with Storybook

Unit tests help verify functional aspects of components. They prove that the output of a component remains the same given a fixed input.

Let's take a look at a simple unit test for our Storybook story:

1import { mount } from '@vue/test-utils'
2
3import Counter from './Counter.vue'
4
5//πŸ‘‡ Imports a specific story for the test
6import { Colored, Default } from './Counter.stories'
7
8it('renders default button', () => {
9  const wrapper = mount(Counter, {
10    propsData: Default.args,
11  })
12  expect(wrapper.find('.container').classes()).toContain('default')
13})
14
15it('renders colored button', () => {
16  const wrapper = mount(Counter, {
17    propsData: Colored.args,
18  })
19  expect(wrapper.find('.container').classes()).toContain('colored')
20})

We wrote two exemplary unit tests Jest executes against our Storybook story Counter.stories.ts:

  • renders default button: asserts that the component container contains the CSS class default
  • renders colored button: asserts that the component container contains the CSS class colored

The test result looks like this:

bash
1PASS  src/components/Counter.test.ts
2  βœ“ renders default button (25 ms)
3  βœ“ renders colored button (4 ms)
4
5----------|---------|----------|---------|---------|-------------------
6File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
7----------|---------|----------|---------|---------|-------------------
8All files |       0 |        0 |       0 |       0 |
9----------|---------|----------|---------|---------|-------------------
10Test Suites: 1 passed, 1 total
11Tests:       2 passed, 2 total
12Snapshots:   0 total
13Time:        3.674 s, estimated 4 s

Snapshot Testing

Snapshot tests compare the rendered markup of every story against known baselines. It’s an easy way to identify markup changes that trigger rendering errors and warnings.

A snapshot test renders the markup of our story, takes a snapshot, and compares it to a reference snapshot file stored alongside the test.

The test case will fail if the two snapshots do not match. There are two typical causes why a snapshot test fails:

  • The change is expected
  • The reference snapshot needs to be updated

We can use Jest Snapshot Testing as Jest library for snapshot tests.

Let's install it by running the following command:

bash
npm install --save-dev jest-serializer-vue

Next, we need to add it as snapshotSerializers to our jest.config.js config file:

1module.exports = {
2  moduleFileExtensions: ['js', 'ts', 'json', 'vue'],
3  transform: {
4    '^.+\\.ts$': 'ts-jest',
5    '^.+\\.vue$': 'vue-jest',
6  },
7  collectCoverage: true,
8  collectCoverageFrom: ['/src/**/*.vue'],
9  snapshotSerializers: ['jest-serializer-vue'],
10}

Finally, we can write a snapshot test for Storybook story:

1it('renders snapshot', () => {
2  const wrapper = mount(Counter, {
3    propsData: Colored.args,
4  })
5  expect(wrapper.element).toMatchSnapshot()
6})

If we now run our tests, we get the following result:

bash
1> vite-vue-typescript-starter@0.0.0 test
2> jest src
3
4 PASS  src/components/Counter.test.ts
5  βœ“ renders default button (27 ms)
6  βœ“ renders colored button (4 ms)
7  βœ“ renders snapshot (6 ms)
8
9----------|---------|----------|---------|---------|-------------------
10File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
11----------|---------|----------|---------|---------|-------------------
12All files |       0 |        0 |       0 |       0 |
13----------|---------|----------|---------|---------|-------------------
14Test Suites: 1 passed, 1 total
15Tests:       3 passed, 3 total
16Snapshots:   1 passed, 1 total
17Time:        1.399 s, estimated 2 s

The test run generates snapshot reference files that are located at src/components/__snapshots__.

Conclusion

Storybook is a fantastic tool to create living documentation for components. If you keep the story files next to your component's source code, the chances are high that the story gets updated if you modify the component.

Storybook has first-class support for Vue 3, and it works very well. If you want more information about Vue and Storybook, you should look at the official Storybook documentation.

If you liked this article, follow me on Twitter to get notified about new blog posts and more content from me.

Alternatively (or additionally), you can also subscribe to my newsletter.

I will never share any of your personal data. You can unsubscribe at any time.

If you found this article helpful.You will love these ones as well.
Use Shiki to Style Code Blocks in HTML Emails Image

Use Shiki to Style Code Blocks in HTML Emails

Building a Vue 3 Desktop App With Pinia, Electron and Quasar Image

Building a Vue 3 Desktop App With Pinia, Electron and Quasar

Unlocking the Power of v-for Loops in Vue With These Useful Tips Image

Unlocking the Power of v-for Loops in Vue With These Useful Tips

Focus & Code Diff in Nuxt Content Code Blocks Image

Focus & Code Diff in Nuxt Content Code Blocks