├── .prettierrc
├── .prettierignore
├── example
├── babel.config.js
├── src
│ ├── shims-vue.d.ts
│ ├── shims-tsx.d.ts
│ ├── Button.spec.ts
│ ├── button.css
│ ├── Button.vue
│ └── Button.stories.ts
├── cypress.json
├── .storybook
│ ├── main.js
│ └── preview.js
├── tsconfig.json
├── cypress
│ ├── support
│ │ ├── index.js
│ │ └── commands.js
│ └── plugins
│ │ └── index.js
└── package.json
├── .gitignore
├── tsconfig.json
├── .github
└── workflows
│ └── release.yml
├── LICENSE
├── package.json
├── CHANGELOG.md
├── src
├── decorateStory.ts
└── index.ts
└── README.md
/.prettierrc:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/example/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/cli-plugin-babel/preset'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/example/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.vue' {
2 | import Vue from 'vue'
3 | export default Vue
4 | }
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | storybook-static/
4 | build-storybook.log
5 | .DS_Store
6 | .env
7 |
8 | example/node_modules
9 |
--------------------------------------------------------------------------------
/example/cypress.json:
--------------------------------------------------------------------------------
1 | {
2 | "fixturesFolder": false,
3 | "componentFolder": "src",
4 | "testFiles": "**/*.spec.{js,ts,jsx,tsx}"
5 | }
6 |
--------------------------------------------------------------------------------
/example/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "stories": [
3 | "../src/**/*.stories.mdx",
4 | "../src/**/*.stories.@(js|jsx|ts|tsx)"
5 | ],
6 | "addons": [
7 | "@storybook/addon-links",
8 | "@storybook/addon-essentials"
9 | ]
10 | }
--------------------------------------------------------------------------------
/example/src/shims-tsx.d.ts:
--------------------------------------------------------------------------------
1 | import Vue, { VNode } from 'vue'
2 |
3 | declare global {
4 | namespace JSX {
5 | // tslint:disable no-empty-interface
6 | interface Element extends VNode {}
7 | // tslint:disable no-empty-interface
8 | interface ElementClass extends Vue {}
9 | interface IntrinsicElements {
10 | [elem: string]: any
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/example/src/Button.spec.ts:
--------------------------------------------------------------------------------
1 | import { mount } from '@cypress/vue';
2 | import { composeStories } from '../../dist';
3 |
4 | import * as stories from './Button.stories';
5 |
6 | const { Primary, Secondary } = composeStories(stories);
7 |
8 | describe('', () => {
9 | it('Primary', () => {
10 | mount(Primary());
11 | cy.get('button').should('exist');
12 | });
13 |
14 | it('Secondary', () => {
15 | mount(Secondary({ label: 'overriden label' }));
16 | cy.get('button').should('exist');
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "baseUrl": ".",
5 | "emitDecoratorMetadata": true,
6 | "esModuleInterop": true,
7 | "experimentalDecorators": true,
8 | "incremental": false,
9 | "isolatedModules": true,
10 | "jsx": "react",
11 | "lib": ["es2017", "dom"],
12 | "module": "ES2015",
13 | "moduleResolution": "node",
14 | "noImplicitAny": true,
15 | "rootDir": "./src",
16 | "skipLibCheck": true,
17 | "target": "es5"
18 | },
19 | "include": ["src/**/*"]
20 | }
21 |
--------------------------------------------------------------------------------
/example/src/button.css:
--------------------------------------------------------------------------------
1 | .storybook-button {
2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
3 | font-weight: 700;
4 | border: 0;
5 | border-radius: 3em;
6 | cursor: pointer;
7 | display: inline-block;
8 | line-height: 1;
9 | }
10 | .storybook-button--primary {
11 | color: white;
12 | background-color: #1ea7fd;
13 | }
14 | .storybook-button--secondary {
15 | color: #333;
16 | background-color: transparent;
17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
18 | }
19 | .storybook-button--small {
20 | font-size: 12px;
21 | padding: 10px 16px;
22 | }
23 | .storybook-button--medium {
24 | font-size: 14px;
25 | padding: 11px 20px;
26 | }
27 | .storybook-button--large {
28 | font-size: 16px;
29 | padding: 12px 24px;
30 | }
31 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on: [push]
4 |
5 | jobs:
6 | release:
7 | runs-on: ubuntu-latest
8 | if: "!contains(github.event.head_commit.message, 'ci skip') && !contains(github.event.head_commit.message, 'skip ci')"
9 | steps:
10 | - uses: actions/checkout@v2
11 |
12 | - name: Prepare repository
13 | run: git fetch --unshallow --tags
14 |
15 | - name: Use Node.js 14.x
16 | uses: actions/setup-node@v1
17 | with:
18 | node-version: 14.x
19 |
20 | - name: Install dependencies
21 | uses: bahmutov/npm-install@v1
22 |
23 | - name: Create Release
24 | env:
25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
27 | run: |
28 | yarn release
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "jsx": "preserve",
7 | "importHelpers": true,
8 | "moduleResolution": "node",
9 | "skipLibCheck": true,
10 | "esModuleInterop": true,
11 | "allowSyntheticDefaultImports": true,
12 | "sourceMap": true,
13 | "baseUrl": ".",
14 | "types": [
15 | "webpack-env"
16 | ],
17 | "paths": {
18 | "@/*": [
19 | "src/*"
20 | ]
21 | },
22 | "lib": [
23 | "esnext",
24 | "dom",
25 | "dom.iterable",
26 | "scripthost"
27 | ]
28 | },
29 | "include": [
30 | "src/**/*.ts",
31 | "src/**/*.tsx",
32 | "src/**/*.vue",
33 | "tests/**/*.ts",
34 | "tests/**/*.tsx"
35 | ],
36 | "exclude": [
37 | "node_modules"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/example/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands';
18 | import * as sbPreview from '../../.storybook/preview';
19 | import { setGlobalConfig } from '../../../dist';
20 |
21 | setGlobalConfig(sbPreview);
22 |
23 | // Alternatively you can use CommonJS syntax:
24 | // require('./commands')
25 |
--------------------------------------------------------------------------------
/example/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/example/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | export const parameters = {
2 | actions: { argTypesRegex: "^on[A-Z].*" },
3 | controls: {
4 | matchers: {
5 | color: /(background|color)$/i,
6 | date: /Date$/,
7 | },
8 | },
9 | };
10 |
11 | export const decorators = [
12 | (story, context) =>
13 | ({
14 | component: { story },
15 | template: `
16 | Global decorator ${JSON.stringify(context.globals.locale)}
17 |
18 |
`
19 | }),
20 | ];
21 |
22 | export const globalTypes = {
23 | locale: {
24 | name: 'Locale',
25 | description: 'Language',
26 | defaultValue: 'en',
27 | toolbar: {
28 | icon: 'circlehollow',
29 | items: [
30 | { value: 'en', icon: 'circlehollow', title: 'Hello' },
31 | { value: 'pt-br', icon: 'circle', title: 'Olá' },
32 | { value: 'fr', icon: 'circle', title: 'Bonjour' },
33 | ],
34 | },
35 | },
36 | }
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cy-ct-vue2-ts-storybook",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "storybook": "start-storybook -p 6006",
9 | "build-storybook": "build-storybook",
10 | "cy": "cypress open-ct"
11 | },
12 | "dependencies": {
13 | "core-js": "^3.6.5",
14 | "vue": "^2.6.11"
15 | },
16 | "devDependencies": {
17 | "@cypress/vue": "^2.2.3",
18 | "@cypress/webpack-dev-server": "^1.3.0",
19 | "@storybook/addon-actions": "~6.3.0",
20 | "@storybook/addon-essentials": "~6.3.0",
21 | "@storybook/addon-links": "~6.3.0",
22 | "@storybook/vue": "~6.3.0",
23 | "@vue/cli-plugin-babel": "~4.5.0",
24 | "@vue/cli-plugin-typescript": "~4.5.0",
25 | "@vue/cli-service": "~4.5.0",
26 | "cypress": "^7.3.0",
27 | "typescript": "~4.1.5",
28 | "vue-loader": "^15.9.7",
29 | "vue-template-compiler": "^2.6.11"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Storybook contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/example/src/Button.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
55 |
--------------------------------------------------------------------------------
/example/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | const { startDevServer } = require("@cypress/webpack-dev-server");
2 |
3 | const webpackConfig = require("@vue/cli-service/webpack.config.js");
4 |
5 | webpackConfig.resolve.alias["vue$"] = "vue/dist/vue.esm.js";
6 |
7 | ///
8 | // ***********************************************************
9 | // This example plugins/index.js can be used to load plugins
10 | //
11 | // You can change the location of this file or turn off loading
12 | // the plugins file with the 'pluginsFile' configuration option.
13 | //
14 | // You can read more here:
15 | // https://on.cypress.io/plugins-guide
16 | // ***********************************************************
17 | // This function is called when a project is opened or re-opened (e.g. due to
18 | // the project's config changing)
19 |
20 | /**
21 | * @type {Cypress.PluginConfig}
22 | */
23 | // eslint-disable-next-line no-unused-vars
24 | module.exports = (on, config) => {
25 | // `on` is used to hook into various events Cypress emits
26 | // `config` is the resolved Cypress config
27 |
28 | if (config.testingType === "component") {
29 | on("dev-server:start", (options) =>
30 | startDevServer({
31 | options,
32 | webpackConfig,
33 | })
34 | );
35 | }
36 |
37 | return config; // IMPORTANT to return a config
38 | };
39 |
--------------------------------------------------------------------------------
/example/src/Button.stories.ts:
--------------------------------------------------------------------------------
1 | import MyButton from './Button.vue';
2 | import { Story } from '@storybook/vue';
3 |
4 | export default {
5 | title: 'Example/Button',
6 | component: MyButton,
7 | decorators: [
8 | () => `
9 | Component Decorator
10 |
11 |
`,
12 | ],
13 | argTypes: {
14 | onClick: { action: 'onClick' },
15 | backgroundColor: { control: 'color' },
16 | size: {
17 | control: { type: 'select', options: ['small', 'medium', 'large'] },
18 | },
19 | },
20 | };
21 |
22 | const Template: Story = (args, { argTypes }) => ({
23 | props: Object.keys(argTypes),
24 | components: { MyButton },
25 | template: '',
26 | });
27 |
28 | export const Primary: Story = Template.bind({});
29 | Primary.args = {
30 | primary: true,
31 | label: 'Primary Button',
32 | };
33 | Primary.decorators = [
34 | () => `
35 | Story Decorator
36 |
37 |
`,
38 | ];
39 |
40 | export const Secondary: Story = Template.bind({});
41 | Secondary.args = {
42 | label: 'Secondary Button',
43 | };
44 |
45 | export const Large: Story = Template.bind({});
46 | Large.args = {
47 | size: 'large',
48 | label: 'Button',
49 | };
50 |
51 | export const Small = Template.bind({});
52 | Small.args = {
53 | size: 'small',
54 | label: 'Button',
55 | };
56 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@storybook/testing-vue",
3 | "version": "0.0.4",
4 | "description": "Testing utilities that allow you to reuse your stories in your unit tests",
5 | "keywords": [
6 | "storybook-addons",
7 | "style",
8 | "test"
9 | ],
10 | "license": "MIT",
11 | "main": "dist/index.js",
12 | "typings": "dist/index.d.ts",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/storybookjs/testing-vue"
16 | },
17 | "files": [
18 | "dist",
19 | "src",
20 | "README.md"
21 | ],
22 | "engines": {
23 | "node": ">=10"
24 | },
25 | "scripts": {
26 | "start": "tsdx watch",
27 | "build": "tsdx build",
28 | "lint": "tsdx lint",
29 | "prepare": "tsdx build",
30 | "size": "size-limit",
31 | "analyze": "size-limit --why",
32 | "release": "yarn build && auto shipit"
33 | },
34 | "peerDependencies": {
35 | "vue": "^2.0.0",
36 | "@storybook/vue": "^6 || >=6.0.0-0 || >=6.1.0-0 || >=6.2.0-0 || >=6.3.0-0 ",
37 | "@storybook/addons": "^6 || >=6.0.0-0 || >=6.1.0-0 || >=6.2.0-0 || >=6.3.0-0 ",
38 | "@storybook/client-api": "^6 || >=6.0.0-0 || >=6.1.0-0 || >=6.2.0-0 || >=6.3.0-0 "
39 | },
40 | "husky": {
41 | "hooks": {
42 | "pre-commit": "tsdx lint"
43 | }
44 | },
45 | "prettier": {
46 | "printWidth": 80,
47 | "semi": true,
48 | "singleQuote": true,
49 | "trailingComma": "es5"
50 | },
51 | "author": "yannbf@gmail.com",
52 | "module": "dist/testing-vue.esm.js",
53 | "size-limit": [
54 | {
55 | "path": "dist/testing-vue.cjs.production.min.js",
56 | "limit": "10 KB"
57 | },
58 | {
59 | "path": "dist/testing-vue.esm.js",
60 | "limit": "10 KB"
61 | }
62 | ],
63 | "devDependencies": {
64 | "@cypress/vue": "^2.2.3",
65 | "@size-limit/preset-small-lib": "^4.10.1",
66 | "@storybook/addons": "~6.3.0",
67 | "@storybook/client-api": "~6.3.0",
68 | "@storybook/vue": "~6.3.0",
69 | "auto": "^10.20.4",
70 | "concurrently": "^6.0.0",
71 | "husky": "^5.1.3",
72 | "size-limit": "^4.10.1",
73 | "tsdx": "^0.14.1",
74 | "tslib": "^2.1.0",
75 | "typescript": "^4.2.3",
76 | "vue": "^2.6.12"
77 | },
78 | "resolutions": {
79 | "**/typescript": "^4.2.3"
80 | },
81 | "publishConfig": {
82 | "access": "public"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # v0.0.4 (Fri Apr 21 2023)
2 |
3 | #### 🐛 Bug Fix
4 |
5 | - fix: replace addons default export with named export [#9](https://github.com/storybookjs/testing-vue/pull/9) ([@Hanawa02](https://github.com/Hanawa02))
6 |
7 | #### 📝 Documentation
8 |
9 | - Add instructions for Jest users to map .vue files to a version of vue including the template-compiler [#8](https://github.com/storybookjs/testing-vue/pull/8) ([@raphael-yapla](https://github.com/raphael-yapla))
10 |
11 | #### Authors: 2
12 |
13 | - [@raphael-yapla](https://github.com/raphael-yapla)
14 | - Laura Caroline ([@Hanawa02](https://github.com/Hanawa02))
15 |
16 | ---
17 |
18 | # v0.0.3 (Tue Dec 21 2021)
19 |
20 | #### 🐛 Bug Fix
21 |
22 | - Fix TypeScript Types and argTypes generation [#3](https://github.com/storybookjs/testing-vue/pull/3) ([@dwightjack](https://github.com/dwightjack))
23 |
24 | #### ⚠️ Pushed to `main`
25 |
26 | - chore: lock storybook to 6.3 in example repo ([@yannbf](https://github.com/yannbf))
27 | - chore: lock storybook dev dependency to 6.3 ([@yannbf](https://github.com/yannbf))
28 |
29 | #### Authors: 2
30 |
31 | - Marco Solazzi ([@dwightjack](https://github.com/dwightjack))
32 | - Yann Braga ([@yannbf](https://github.com/yannbf))
33 |
34 | ---
35 |
36 | # v0.0.2 (Tue Sep 28 2021)
37 |
38 | #### 🐛 Bug Fix
39 |
40 | - Fix: support addons that use channel api [#4](https://github.com/storybookjs/testing-vue/pull/4) ([@mphstudios](https://github.com/mphstudios))
41 |
42 | #### Authors: 1
43 |
44 | - matthew p hrudka ([@mphstudios](https://github.com/mphstudios))
45 |
46 | ---
47 |
48 | # v0.0.1 (Sat May 22 2021)
49 |
50 | #### ⚠️ Pushed to `main`
51 |
52 | - chore(example): fix example test ([@yannbf](https://github.com/yannbf))
53 | - chore(example): add button file ([@yannbf](https://github.com/yannbf))
54 | - fix: add vue 2 as a peer depenedency ([@elevatebart](https://github.com/elevatebart))
55 | - chore: add example ([@elevatebart](https://github.com/elevatebart))
56 | - feat: add first library ([@elevatebart](https://github.com/elevatebart))
57 | - build: add yarn lock ([@elevatebart](https://github.com/elevatebart))
58 | - chore: cleanup unnecessary files ([@yannbf](https://github.com/yannbf))
59 | - docs(README): update logo ([@yannbf](https://github.com/yannbf))
60 | - chore: update package json and readme ([@yannbf](https://github.com/yannbf))
61 | - Initial commit ([@yannbf](https://github.com/yannbf))
62 |
63 | #### Authors: 2
64 |
65 | - Barthélémy Ledoux ([@elevatebart](https://github.com/elevatebart))
66 | - Yann Braga ([@yannbf](https://github.com/yannbf))
67 |
--------------------------------------------------------------------------------
/src/decorateStory.ts:
--------------------------------------------------------------------------------
1 | import Vue, { ComponentOptions, VueConstructor } from 'vue';
2 | import { StoryFn, DecoratorFunction, StoryContext } from '@storybook/addons';
3 |
4 | export const WRAPS = 'STORYBOOK_WRAPS';
5 | export const VALUES = 'STORYBOOK_VALUES';
6 |
7 | export type VueStory =
8 | | (ComponentOptions & { options: Record })
9 | | VueConstructor;
10 | type StoryFnVueReturnType = string | ComponentOptions | VueConstructor;
11 |
12 | function getType(fn: Function) {
13 | const match = fn && fn.toString().match(/^\s*function (\w+)/);
14 | return match ? match[1] : '';
15 | }
16 |
17 | // https://github.com/vuejs/vue/blob/dev/src/core/util/props.js#L92
18 | function resolveDefault({ type, default: def }: any) {
19 | if (typeof def === 'function' && getType(type) !== 'Function') {
20 | // known limitation: we don't have the component instance to pass
21 | return def.call();
22 | }
23 |
24 | return def;
25 | }
26 |
27 | export function extractProps(component: VueConstructor) {
28 | // @ts-ignore this options business seems not good according to the types
29 | return Object.entries(component.options.props || {})
30 | .map(([name, prop]) => ({ [name]: resolveDefault(prop) }))
31 | .reduce((wrap, prop) => ({ ...wrap, ...prop }), {});
32 | }
33 |
34 | function prepare(rawStory: StoryFnVueReturnType, innerStory?: VueStory) {
35 | let story: ComponentOptions | VueConstructor;
36 |
37 | if (typeof rawStory === 'string') {
38 | story = { template: rawStory };
39 | } else if (rawStory != null) {
40 | story = rawStory as ComponentOptions;
41 | } else {
42 | return null;
43 | }
44 |
45 | let storyVue: VueConstructor;
46 | // @ts-ignore
47 | // eslint-disable-next-line no-underscore-dangle
48 | if (!story._isVue) {
49 | if (innerStory) {
50 | story.components = { ...(story.components || {}), story: innerStory };
51 | }
52 | storyVue = Vue.extend(story);
53 | // @ts-ignore // https://github.com/storybookjs/storybook/pull/7578#discussion_r307984824
54 | } else if (story.options[WRAPS]) {
55 | storyVue = story as VueConstructor;
56 | return storyVue;
57 | } else {
58 | storyVue = story as VueConstructor;
59 | }
60 |
61 | return Vue.extend({
62 | // @ts-ignore // https://github.com/storybookjs/storybook/pull/7578#discussion_r307985279
63 | [WRAPS]: story,
64 | // @ts-ignore // https://github.com/storybookjs/storybook/pull/7578#discussion_r307984824
65 | [VALUES]: {
66 | ...(innerStory && 'options' in innerStory
67 | ? innerStory.options[VALUES]
68 | : {}),
69 | ...extractProps(storyVue),
70 | },
71 | functional: true,
72 | render(h, { data, parent, children }) {
73 | return h(
74 | story,
75 | {
76 | ...data,
77 | // @ts-ignore // https://github.com/storybookjs/storybook/pull/7578#discussion_r307986196
78 | props: { ...(data.props || {}), ...parent.$root[VALUES] },
79 | },
80 | children
81 | );
82 | },
83 | });
84 | }
85 |
86 | const defaultContext: StoryContext = {
87 | id: 'unspecified',
88 | name: 'unspecified',
89 | kind: 'unspecified',
90 | parameters: {},
91 | args: {},
92 | argTypes: {},
93 | globals: {},
94 | };
95 |
96 | function decorateStory(
97 | storyFn: StoryFn,
98 | decorators: DecoratorFunction[]
99 | ) {
100 | return decorators.reduce(
101 | // @ts-ignore
102 | (decorated, decorator) => (context: StoryContext = defaultContext) => {
103 | let story: VueStory;
104 |
105 | const decoratedStory = decorator(
106 | ({ parameters, ...innerContext } = {} as StoryContext) =>
107 | decorated({ ...context, ...innerContext }),
108 | context
109 | );
110 |
111 | if (!story) {
112 | story = decorated(context) as VueStory;
113 | }
114 |
115 | if (decoratedStory === story) {
116 | return story;
117 | }
118 |
119 | return prepare(decoratedStory, story);
120 | },
121 | (context: StoryContext) => prepare(storyFn(context))
122 | );
123 | }
124 |
125 | export default decorateStory;
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > ⚠️ IMPORTANT NOTE:
2 | > This package is deprecated, alongside Vue2 support for Storybook. If you have no plans on upgrading to Vue 3, you can stick to Storybook 7.
3 | > The composeStories API has been promoted to a first-class Storybook functionality in Storybook 8, and you can now use it as part of @storybook/vue3.
4 | > More info: https://github.com/storybookjs/testing-vue3/issues/16
5 |
6 | -----------------------------
7 |
8 |
9 |
10 |
11 |
12 | Testing utilities that allow you to reuse your stories in your unit tests
13 |
14 |
15 |
16 | > ⚠️ If you're using Storybook with **Vue 3**, please check [@storybook/testing-vue3](https://github.com/storybookjs/testing-vue3) instead!
17 |
18 | ## Installation
19 |
20 | This library should be installed as one of your project's `devDependencies`:
21 |
22 | via [npm](https://www.npmjs.com/)
23 |
24 | ```
25 | npm install --save-dev @storybook/testing-vue
26 | ```
27 |
28 | or via [yarn](https://classic.yarnpkg.com/)
29 |
30 | ```
31 | yarn add --dev @storybook/testing-vue
32 | ```
33 |
34 | ## Setup
35 |
36 | ### Jest configuration
37 |
38 | If you're using Jest to run your tests you should map `.vue` files to use a version of Vue that includes its template compiler in your `jest.config.js`:
39 | ```js
40 | moduleNameMapper: {
41 | '^vue$': 'vue/dist/vue.common.dev.js'
42 | },
43 | ```
44 |
45 | ### Storybook CSF
46 |
47 | This library requires you to be using Storybook's [Component Story Format (CSF)](https://storybook.js.org/docs/react/api/csf) and [hoisted CSF annotations](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#hoisted-csf-annotations), which is the recommended way to write stories since Storybook 6.
48 |
49 | Essentially, if your stories look similar to this, you're good to go!
50 |
51 | ```jsx
52 | // CSF: default export (meta) + named exports (stories)
53 | export default {
54 | title: 'Example/Button',
55 | component: Button,
56 | };
57 |
58 | export const Primary = () => ({
59 | template: '',
60 | });
61 | ```
62 |
63 | ### Global config
64 |
65 | > This is an optional step. If you don't have [global decorators](https://storybook.js.org/docs/react/writing-stories/decorators#global-decorators), there's no need to do this. However, if you do, this is a necessary step for your global decorators to be applied.
66 |
67 | If you have global decorators/parameters/etc and want them applied to your stories when testing them, you first need to set this up. You can do this by adding to or creating a jest [setup file](https://jestjs.io/docs/configuration#setupfiles-array):
68 |
69 | ```tsx
70 | // setupFile.js <-- this will run before the tests in jest.
71 | import { setGlobalConfig } from '@storybook/testing-vue';
72 | import * as globalStorybookConfig from './.storybook/preview'; // path of your preview.js file
73 |
74 | setGlobalConfig(globalStorybookConfig);
75 | ```
76 |
77 | For the setup file to be picked up, you need to pass it as an option to jest in your test command:
78 |
79 | ```json
80 | // package.json
81 | {
82 | "test": "jest --setupFiles ./setupFile.js"
83 | }
84 | ```
85 |
86 | ## Usage
87 |
88 | ### `composeStories`
89 |
90 | `composeStories` will process all stories from the component you specify, compose args/decorators in all of them and return an object containing the composed stories.
91 |
92 | If you use the composed story (e.g. PrimaryButton), the component will render with the args that are passed in the story. However, you are free to pass any props on top of the component, and those props will override the default values passed in the story's args.
93 |
94 | ```tsx
95 | import { render, screen } from '@testing-library/vue';
96 | import { composeStories } from '@storybook/testing-vue';
97 | import * as stories from './Button.stories'; // import all stories from the stories file
98 |
99 | // Every component that is returned maps 1:1 with the stories, but they already contain all decorators from story level, meta level and global level.
100 | const { Primary, Secondary } = composeStories(stories);
101 |
102 | test('renders primary button with default args', () => {
103 | render(Primary());
104 | const buttonElement = screen.getByText(
105 | /Text coming from args in stories file!/i
106 | );
107 | expect(buttonElement).not.toBeNull();
108 | });
109 |
110 | test('renders primary button with overriden props', () => {
111 | render(Secondary({ label: 'Hello world' })); // you can override props and they will get merged with values from the Story's args
112 | const buttonElement = screen.getByText(/Hello world/i);
113 | expect(buttonElement).not.toBeNull();
114 | });
115 | ```
116 |
117 | ### `composeStory`
118 |
119 | You can use `composeStory` if you wish to apply it for a single story rather than all of your stories. You need to pass the meta (default export) as well.
120 |
121 | ```tsx
122 | import { render, screen } from '@testing-library/vue';
123 | import { composeStory } from '@storybook/testing-vue';
124 | import Meta, { Primary as PrimaryStory } from './Button.stories';
125 |
126 | // Returns a component that already contain all decorators from story level, meta level and global level.
127 | const Primary = composeStory(PrimaryStory, Meta);
128 |
129 | test('onclick handler is called', async () => {
130 | const onClickSpy = jest.fn();
131 | render(Primary({ onClick: onClickSpy }));
132 | const buttonElement = screen.getByRole('button');
133 | buttonElement.click();
134 | expect(onClickSpy).toHaveBeenCalled();
135 | });
136 | ```
137 |
138 | ## License
139 |
140 | [MIT](./LICENSE)
141 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { combineParameters } from '@storybook/client-api'
3 | import { addons, mockChannel, ArgTypes, Parameters, BaseDecorators } from '@storybook/addons'
4 | import { Meta, Story, StoryContext } from '@storybook/vue'
5 | import decorateStory from './decorateStory'
6 | import type { ComponentOptions, VueConstructor } from 'vue'
7 |
8 | type StoryFnVueReturnType = string | ComponentOptions
9 |
10 | interface StoryFactory> extends Story {
11 | (extraArgs?: Args): ComponentOptions;
12 | }
13 |
14 | /**
15 | * Object representing the preview.ts module
16 | *
17 | * Used in storybook testing utilities.
18 | * @see [Unit testing with Storybook](https://storybook.js.org/docs/react/workflows/unit-testing)
19 | */
20 | export type GlobalConfig = {
21 | decorators?: BaseDecorators
22 | parameters?: Parameters
23 | argTypes?: ArgTypes
24 | [key: string]: any
25 | }
26 |
27 | /**
28 | * T represents the whole es module of a stories file. K of T means named exports (basically the Story type)
29 | * 1. pick the keys K of T that have properties that are Story
30 | * 2. infer the actual prop type for each Story
31 | * 3. reconstruct Story with Partial. Story -> Story>
32 | */
33 | export type StoriesWithPartialProps = {
34 | [K in keyof T as T[K] extends Story ? K : never]: T[K] extends Story<
35 | infer P
36 | >
37 | ? StoryFactory>
38 | : unknown
39 | }
40 |
41 | // Some addons use the channel api to communicate between manager/preview, and this is a client only feature, therefore we must mock it.
42 | addons.setChannel(mockChannel());
43 |
44 | let globalStorybookConfig: GlobalConfig = {}
45 |
46 | export function setGlobalConfig(config: GlobalConfig) {
47 | globalStorybookConfig = config
48 | }
49 |
50 | function isStory(story: any): story is Story {
51 | return typeof story === 'function'
52 | }
53 |
54 | export function composeStory(
55 | story: Story,
56 | meta: Meta,
57 | globalConfig: GlobalConfig = globalStorybookConfig
58 | ): StoryFactory {
59 | if (!isStory( story)) {
60 | throw new Error(
61 | `Cannot compose story due to invalid format. @storybook/testing-vue expected a function but received ${typeof story} instead.`
62 | )
63 | }
64 |
65 | if ((story as any).story !== undefined) {
66 | throw new Error(
67 | `StoryFn.story object-style annotation is not supported. @storybook/testing-vue expects hoisted CSF stories.
68 | https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#hoisted-csf-annotations`
69 | )
70 | }
71 |
72 | const finalStoryFn = (context: StoryContext) => {
73 | const { passArgsFirst = true } = context.parameters
74 | if (!passArgsFirst) {
75 | throw new Error(
76 | 'composeStory does not support legacy style stories (with passArgsFirst = false).'
77 | )
78 | }
79 | const component = story(
80 | context.args as GenericArgs,
81 | context
82 | ) as StoryFnVueReturnType
83 |
84 | const cmp =
85 | typeof component === 'string' ? { template: component } : component
86 |
87 | const {args} = context
88 | cmp.props = Object.keys(context.argTypes)
89 |
90 | // augment args with action methods.
91 | // Either match the argTypesRegex parameter
92 | // or an argType with "action" property
93 | const matcher = globalStorybookConfig.parameters?.actions?.argTypesRegex
94 | const matchRegExp = matcher ? new RegExp(matcher) : null
95 |
96 | for (const prop of cmp.props) {
97 | if ((matchRegExp?.test(prop) || context.argTypes[prop].action) && typeof args[prop] !== 'function') {
98 | args[prop] = () => {}
99 | }
100 | }
101 |
102 | return Vue.extend({
103 | render(h) {
104 | return h(cmp, {
105 | props: args,
106 | })
107 | },
108 | })
109 | }
110 |
111 | const combinedDecorators = [
112 | ...(story.decorators || []),
113 | ...(meta?.decorators || []),
114 | ...(globalConfig?.decorators || []),
115 | ]
116 |
117 | const decorated = decorateStory(
118 | finalStoryFn as any,
119 | combinedDecorators as any
120 | )
121 |
122 | const defaultGlobals = Object.entries(
123 | (globalConfig.globalTypes || {}) as Record
124 | ).reduce((acc, [arg, { defaultValue }]) => {
125 | if (defaultValue) {
126 | acc[arg] = defaultValue
127 | }
128 | return acc
129 | }, {} as Record)
130 | return (extraArgs?: Record) => {
131 | const args = {
132 | ...(meta?.args || {}),
133 | ...story.args,
134 | ...extraArgs,
135 | }
136 | // construct basic ArgTypes from args
137 | const argTypes: ArgTypes = {};
138 | for (const type of Object.keys(args)) {
139 | argTypes[type] = {}
140 | }
141 | // merge with actual ArgTypes config
142 | Object.assign(argTypes, story.argTypes, meta.argTypes, globalConfig.argTypes)
143 |
144 | return decorated({
145 | id: '',
146 | kind: '',
147 | name: '',
148 | argTypes,
149 | globals: defaultGlobals,
150 | parameters: combineParameters(
151 | globalConfig.parameters || {},
152 | meta?.parameters || {},
153 | story.parameters || {}
154 | ),
155 | args,
156 | }) as ComponentOptions
157 | }
158 |
159 | }
160 |
161 | export function composeStories<
162 | T extends { default: Meta; __esModule?: boolean }
163 | >(storiesImport: T, globalConfig?: GlobalConfig): StoriesWithPartialProps {
164 | const { default: meta, __esModule, ...stories } = storiesImport
165 | // Compose an object containing all processed stories passed as parameters
166 | const composedStories = Object.entries(stories).reduce(
167 | (storiesMap, [key, story]: [string, Story]) => {
168 | storiesMap[key] = composeStory(story, meta, globalConfig)
169 | return storiesMap
170 | },
171 | {} as { [key: string]: StoryFactory }
172 | )
173 | return composedStories as StoriesWithPartialProps
174 | }
175 |
176 | /**
177 | * Useful function for JSX syntax call of vue stories
178 | */
179 | export const h = (
180 | cmp: (p: Record) => VueConstructor,
181 | { props }: { props?: any } = {}
182 | ) => cmp(props)
183 |
--------------------------------------------------------------------------------