├── .gitignore
├── register.js
├── .babelrc
├── .npmignore
├── docs
├── screenshot.png
└── snapshot-jest.png
├── .storybook
├── __conf__
│ ├── jestMockConfig.js
│ ├── enzymeConfig.js
│ └── mochaMockConfig.js
├── addons.js
├── __tests__
│ ├── __snapshots__
│ │ └── sample.ci.jest.stories.js.snap
│ ├── sample.stories.js
│ ├── sample.ci.jest.stories.js
│ └── sample.ci.mocha.stories.js
├── __mocks__
│ ├── facade-mocha.js
│ └── facade.js
├── webpack.config.js
├── config.js
└── facade.js
├── .travis.yml
├── src
├── index.js
├── components
│ └── Specifications
│ │ ├── index.js
│ │ └── style.js
├── manager.js
├── containers
│ └── Specifications
│ │ └── index.js
└── preview.js
├── CHANGELOG.md
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | dist/
--------------------------------------------------------------------------------
/register.js:
--------------------------------------------------------------------------------
1 | require('./dist').register();
2 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-0", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .babelrc
3 | .idea
4 | .storybook
5 | src
--------------------------------------------------------------------------------
/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthuret/storybook-addon-specifications/HEAD/docs/screenshot.png
--------------------------------------------------------------------------------
/docs/snapshot-jest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mthuret/storybook-addon-specifications/HEAD/docs/snapshot-jest.png
--------------------------------------------------------------------------------
/.storybook/__conf__/jestMockConfig.js:
--------------------------------------------------------------------------------
1 | jest.mock('../facade');
2 |
3 | global.requestAnimationFrame = function(callback) {
4 | setTimeout(callback, 0)
5 | };
6 |
7 |
--------------------------------------------------------------------------------
/.storybook/__conf__/enzymeConfig.js:
--------------------------------------------------------------------------------
1 | import {configure} from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------
/.storybook/addons.js:
--------------------------------------------------------------------------------
1 | // Uncomment to register defaults
2 | // import '@kadira/storybook/addons';
3 |
4 | // Use the line below to register this addon
5 | import '../register';
6 | import '@storybook/react/addons';
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: stable
3 | install:
4 | - npm install --global yarn
5 | - yarn
6 | branches:
7 | only:
8 | - master
9 | - /^greenkeeper-.*$/
10 | cache:
11 | directories:
12 | - ~/.yarn
13 | - ~/.nvm
14 |
--------------------------------------------------------------------------------
/.storybook/__tests__/__snapshots__/sample.ci.jest.stories.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Hello Earth 1`] = `
4 |
9 | `;
10 |
11 | exports[`Hello World 1`] = `
12 |
17 | `;
18 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // addons, panels and events get unique names using a prefix
2 | export const ADDON_ID = 'storybook-addon-specifications';
3 | export const PANEL_ID = `${ADDON_ID}/specifications-panel`;
4 | export const EVENT_ID = `${ADDON_ID}/specifications-event`;
5 |
6 | export { register } from './manager';
7 | export { specs, describe, it,
8 | beforeEach, afterEach, after, before,
9 | xit, fit, xdescribe} from './preview';
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Changelog
2 |
3 | ### v1.0.9
4 |
5 | * First stable release
6 |
7 | ### v1.0.11
8 |
9 | #2: log specs failure in the console
10 | #3: improve facade.js to avoid modifying your stories declaration when adding ci
11 |
12 | ### v1.0.14
13 |
14 | * change specs display to improve failures readability
15 |
16 | ### v1.0.15
17 |
18 | * add hooks and jest/mocha specifics functions :
19 | * before, after, beforeEach, afterEach
20 | * jest : xit, fit, xdescribe
21 | * mocha : describe.only, describe.skip, it.only, it.skip
--------------------------------------------------------------------------------
/.storybook/__mocks__/facade-mocha.js:
--------------------------------------------------------------------------------
1 | export const storiesOf = function storiesOf() {
2 | var api = {};
3 | api.add = (name, func)=> {
4 | func();
5 | return api;
6 | };
7 | api.addWithInfo = (name, func)=> {
8 | func();
9 | return api;
10 | };
11 | return api;
12 | };
13 | export const action = () => {};
14 |
15 | export const linkTo = () => {};
16 |
17 | export const specs = (spec) => {
18 | spec();
19 | };
20 |
21 | export const describe = describe;
22 | export const it = it;
23 | export const after = after;
24 | export const before = before;
25 | export const afterEach = afterEach;
26 | export const beforeEach = beforeEach;
--------------------------------------------------------------------------------
/.storybook/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const config = {
4 | module: {
5 | loaders: [
6 | {
7 | test: /\.css?$/,
8 | loaders: ['style', 'raw'],
9 | include: path.resolve(__dirname, '../'),
10 | },
11 | {
12 | test: /\.json?$/,
13 | loaders: ['json'],
14 | include: path.resolve(__dirname, '../'),
15 | }
16 | ],
17 | },
18 | externals: {
19 | 'jsdom': 'window',
20 | 'cheerio': 'window',
21 | 'react/lib/ExecutionEnvironment': true,
22 | 'react/lib/ReactContext': 'window',
23 | 'react/addons': true,
24 | }
25 | };
26 |
27 | module.exports = config;
28 |
--------------------------------------------------------------------------------
/.storybook/config.js:
--------------------------------------------------------------------------------
1 | import * as storybook from '@storybook/react';
2 |
3 |
4 | //THIS IS NEEDED ONLY IF YOU ARE USING MOCHA AS A TEST RUNNER
5 |
6 | import {storiesOf, action, linkTo, specs, describe, it,
7 | after, before, beforeEach, afterEach} from "./facade";
8 |
9 | global.storiesOf = storiesOf;
10 | global.action = action;
11 | global.linkTo = linkTo;
12 | global.specs = specs;
13 | global.describe = describe;
14 | global.it = it;
15 | global.after = after;
16 | global.before = before;
17 | global.beforeEach = beforeEach;
18 | global.afterEach = afterEach;
19 |
20 | // END OF SPECIFIC MOCHA CONF
21 |
22 | const req = require.context('./', true, /stories\.js$/);
23 |
24 | function loadStories() {
25 | req.keys().forEach(req)
26 | }
27 |
28 | storybook.configure(loadStories, module);
--------------------------------------------------------------------------------
/src/components/Specifications/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from "react";
2 | import {css} from "aphrodite";
3 | import specs from "./style"
4 |
5 | export default class Specifications extends Component {
6 |
7 | render() {
8 | let {results} = this.props;
9 | return (
10 |
11 | {results.wrongResults.map((r, idx) =>
12 | -
13 |
{r.spec}
14 | {r.message}
)}
15 |
16 | {results.goodResults.map((r, idx) =>
17 | -
18 |
{r}
19 |
20 | )}
21 |
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.storybook/__tests__/sample.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {storiesOf, action} from '@storybook/react'
3 | import { specs, describe, it} from '../../src'
4 | import {mount} from "enzyme";
5 | import expect from "expect";
6 |
7 |
8 | const stories = storiesOf('Button', module);
9 |
10 | stories.add('Hello World', function () {
11 | const helloWorldStory =
12 | ;
15 |
16 | specs(() => describe('Hello World', function () {
17 | it('Should have the Hello World label', function () {
18 | let output = mount(helloWorldStory);
19 | expect(output.text()).toContain('Hello Wrld');
20 | });
21 |
22 | it('Should have the Hello World label', function () {
23 | let output = mount(helloWorldStory);
24 | expect(output.text()).toContain('Hello World');
25 | });
26 | }));
27 |
28 | return helloWorldStory;
29 | });
--------------------------------------------------------------------------------
/.storybook/__conf__/mochaMockConfig.js:
--------------------------------------------------------------------------------
1 | import {storiesOf, action, linkTo, specs, describe, it,} from "../__mocks__/facade-mocha";
2 | global.storiesOf = storiesOf;
3 | global.action = action;
4 | global.linkTo = linkTo;
5 | global.specs = specs;
6 | global.describe = describe;
7 | global.it = it;
8 | global.after = after;
9 | global.before = before;
10 | global.beforeEach = beforeEach;
11 | global.afterEach = afterEach;
12 |
13 | import { jsdom } from 'jsdom';
14 |
15 | /**
16 | * Mocking browser-like DOM
17 | */
18 | global.document = jsdom('', {
19 | headers: {
20 | 'User-Agent':
21 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7)' +
22 | ' AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.71 Safari/534.24'
23 | }
24 | });
25 | global.window = document.defaultView;
26 | global.navigator = global.window.navigator;
27 |
28 |
29 | global.requestAnimationFrame = function(callback) {
30 | setTimeout(callback, 0)
31 | };
32 |
33 |
--------------------------------------------------------------------------------
/.storybook/facade.js:
--------------------------------------------------------------------------------
1 | import {
2 | storiesOf as storiesOfReal,
3 | action as actionReal,
4 | linkTo as linkToReal
5 | } from '@storybook/react';
6 |
7 | import {
8 | specs as specsReal,
9 | describe as describeReal,
10 | it as itReal,
11 | beforeEach as beforeEachReal,
12 | before as beforeReal,
13 | after as afterReal,
14 | afterEach as afterEachReal,
15 | xit as xitReal,
16 | fit as fitReal,
17 | xdescribe as xdescribeReal
18 | } from '../src';
19 |
20 | export const storiesOf = storiesOfReal;
21 | export const action = actionReal;
22 | export const linkTo = linkToReal;
23 | export const specs = specsReal;
24 | export const describe = describeReal;
25 | export const it = itReal;
26 |
27 | export const beforeEach = beforeEachReal;
28 | export const afterEach = afterEachReal;
29 | export const before = beforeReal;
30 | export const after = afterReal;
31 |
32 | export const xit = xitReal;
33 | export const fit = fitReal;
34 | export const xdescribe = xdescribeReal;
35 |
--------------------------------------------------------------------------------
/src/components/Specifications/style.js:
--------------------------------------------------------------------------------
1 | import {StyleSheet} from "aphrodite";
2 |
3 | export default StyleSheet.create({
4 | wrapper: {
5 | flex: 1,
6 | display: 'flex',
7 | flexDirection: 'column',
8 | fontFamily: '-apple-system, ".SFNSText-Regular", "San Francisco", Roboto, "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif',
9 | fontSize: 12,
10 | letterSpacing: 1,
11 | textDecoration: 'none',
12 | listStyleType: 'none',
13 | border: '1px solid transparent'
14 | },
15 | error: {
16 | ':before': {
17 | content: "'✘'",
18 | padding: '3px 5px',
19 | backgroundColor: 'red'
20 | }
21 | },
22 |
23 | li: {
24 | ':before': {
25 | marginRight: '5px',
26 | marginTop: '11px',
27 | fontSize: '70%',
28 | color: 'white',
29 | fontWeight: 'bold',
30 | borderRadius: '12px',
31 | float: 'left'
32 | }
33 | },
34 |
35 | message: {
36 | padding: '10px',
37 | margin: '10px'
38 | },
39 |
40 | pass: {
41 | ':before': {
42 | content: "'✔'",
43 | padding: '4px 5px',
44 | backgroundColor: 'green'
45 | }
46 | },
47 | });
48 |
--------------------------------------------------------------------------------
/src/manager.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import addons from '@storybook/addons';
3 | import Specifications from './containers/Specifications'
4 | import { ADDON_ID, PANEL_ID } from './';
5 |
6 | // register function will call addons.register to register an addon loader
7 | // This is executed when importing `@kadira/storybook-addon-hello/register`
8 | export function register() {
9 | // addons.register can be used to register a new addon loader function.
10 | // Addon loader will receive `api` as an argument which can be used to
11 | // interact with the storybook manager. We're not using it in this addon.
12 | addons.register(ADDON_ID, api => {
13 | // get `channel` from the addon API
14 | const channel = addons.getChannel();
15 | // addons.addPanel can be used to add a new panel to storybook manager
16 | // The `title` field will be used as the tab title and the `render` field
17 | // will be executed to render the tab content.
18 | addons.addPanel(PANEL_ID, {
19 | title: 'Specifications',
20 | render: ({ active, key }) =>
21 | });
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/.storybook/__mocks__/facade.js:
--------------------------------------------------------------------------------
1 | export const storiesOf = function storiesOf() {
2 | var api = {};
3 | var story;
4 | api.add = (name, func)=> {
5 | story = func();
6 | snapshot(name, story);
7 | return api;
8 | };
9 | api.addWithInfo = (name, func)=> {
10 | story = func();
11 | snapshot(name, story);
12 | return api;
13 | };
14 | return api;
15 | };
16 | export const action = () => {};
17 |
18 | export const linkTo = () => {};
19 |
20 | export const specs = (spec) => {
21 | spec()
22 | };
23 |
24 | export const snapshot = (name, story) => {
25 | it(name, function () {
26 | let renderer = require("react-test-renderer");
27 | const tree = renderer.create(story).toJSON();
28 | expect(tree).toMatchSnapshot();
29 | });
30 | };
31 |
32 | export const describe = jasmine.currentEnv_.describe;
33 | export const it = jasmine.currentEnv_.it;
34 | export const beforeEach = jasmine.currentEnv_.beforeEach;
35 | export const afterEach = jasmine.currentEnv_.afterEach;
36 | export const xit = jasmine.currentEnv_.xit;
37 | export const xdescribe = jasmine.currentEnv_.xdescribe;
38 | export const fit = jasmine.currentEnv_.fit;
39 | export const after = () => {};
40 | export const before = () => {};
41 |
42 |
--------------------------------------------------------------------------------
/src/containers/Specifications/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from "react";
2 | import SpecificationsComponent from "../../components/Specifications/";
3 | import {EVENT_ID} from "../../";
4 |
5 |
6 | export default class Specifications extends Component {
7 | constructor(props, ...args) {
8 | super(props, ...args);
9 | this.state = { storyName: null, results: { wrongResults: [], goodResults: [] } };
10 | this._listener = ({ asyncResultsUpdate, storyName, results }) => {
11 | if (asyncResultsUpdate) {
12 | if (storyName === this.state.storyName) {
13 | this.setState({ results });
14 | }
15 | } else {
16 | this.setState({ storyName, results });
17 | }
18 | }
19 | }
20 |
21 | componentDidMount() {
22 | this.props.channel.on(EVENT_ID, this._listener);
23 | this.props.api.on("story", (data) => this.setState({ storyName: null, results: { wrongResults: [], goodResults: [] } }));
24 | }
25 |
26 | componentWillUnmount() {
27 | this.props.channel.removeListener(EVENT_ID, this._listener);
28 | }
29 |
30 | render() {
31 | const results = this.state.results;
32 | const { active } = this.props;
33 |
34 | return active ? (
35 |
36 | ) : null;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.storybook/__tests__/sample.ci.jest.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {storiesOf, action, describe, it, specs,
3 | beforeEach, before, after, afterEach, xdescribe,
4 | fit, xit} from "../facade";
5 | import {mount} from "enzyme";
6 | import expect from "expect";
7 |
8 |
9 | const stories = storiesOf('Button - CI JEST Sample', module);
10 |
11 | stories.add('Hello World', function () {
12 | const helloWorldStory =
13 | ;
16 |
17 | specs(() => describe('Hello World', function () {
18 | let output;
19 | beforeEach(function() {
20 | console.log('BEFORE EACH');
21 | output = mount(helloWorldStory);
22 | });
23 |
24 | afterEach(function() {
25 | console.log('AFTER EACH');
26 | });
27 |
28 | it('Should have the Hello World label', function () {
29 | expect(output.text()).toContain('Hello World');
30 | });
31 |
32 | it('Should have the Hello World label', function () {
33 | expect(output.text()).toContain('Hello World');
34 | });
35 |
36 | }));
37 | return helloWorldStory;
38 | });
39 |
40 | stories.add('Hello Earth', function () {
41 | const helloEarthStory =
42 | ;
45 |
46 | specs(() => describe('Hello Earth', function () {
47 | it('Should have the Hello Earth label', function () {
48 | let output = mount(helloEarthStory);
49 | expect(output.text()).toContain('Hello Earth');
50 | });
51 | }));
52 |
53 | return helloEarthStory;
54 | });
--------------------------------------------------------------------------------
/.storybook/__tests__/sample.ci.mocha.stories.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {mount} from "enzyme";
3 | import expect from "expect";
4 |
5 | const stories = storiesOf('Button - CI MOCHA Sample', module);
6 |
7 | stories.add('Hello World', function () {
8 | const helloWorldStory =
9 | ;
12 |
13 | specs(() => describe('Hello World', function () {
14 | let output;
15 | beforeEach(function() {
16 | console.log('BEFORE EACH');
17 | output = mount(helloWorldStory);
18 | });
19 |
20 | before(function() {
21 | console.log('BEFORE');
22 | });
23 |
24 | afterEach(function() {
25 | console.log('AFTER EACH');
26 | });
27 |
28 | after(function() {
29 | console.log('AFTER ');
30 | });
31 |
32 | it('Should have the Hello World label', function () {
33 | expect(output.text()).toContain('Hello World');
34 | });
35 |
36 | it('Should have the Hello World label', function () {
37 | expect(output.text()).toContain('Hello World');
38 | });
39 |
40 | }));
41 | return helloWorldStory;
42 | });
43 |
44 | stories.add('Hello Earth', function () {
45 | const helloEarthStory =
46 | ;
49 |
50 | specs(() => describe('Hello Earth', function () {
51 | it('Should have the Hello Earth label', function () {
52 | let output = mount(helloEarthStory);
53 | expect(output.text()).toContain('Hello Earth');
54 | });
55 | }));
56 |
57 | return helloEarthStory;
58 | });
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "storybook-addon-specifications",
3 | "version": "2.2.0",
4 | "description": "Add tests to your react storybook stories",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "clean": "rimraf dist",
8 | "deploy-storybook": "storybook-to-ghpages",
9 | "prepublish": "npm-run-all clean test",
10 | "storybook": "start-storybook -p 9001",
11 | "babel": "babel --ignore __tests__ src --out-dir dist",
12 | "jest": "jest",
13 | "mocha": "mocha --compilers js:babel-register --require babel-polyfill ./.storybook/__conf__/mochaMockConfig.js ./.storybook/__conf__/enzymeConfig.js \"./.storybook/**/*.mocha.stories.js\"",
14 | "test": "npm-run-all babel jest mocha"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/mthuret/storybook-addon-specifications.git"
19 | },
20 | "keywords": [
21 | "storybook",
22 | "tests",
23 | "react",
24 | "specifications"
25 | ],
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/mthuret/storybook-addon-specifications/issues"
29 | },
30 | "homepage": "https://github.com/mthuret/storybook-addon-specifications#readme",
31 | "devDependencies": {
32 | "@storybook/addons": "5.2.0-beta.40",
33 | "@storybook/react": "5.2.0-beta.40",
34 | "@storybook/ui": "3.2.17",
35 | "babel-cli": "^6.11.4",
36 | "babel-preset-es2015": "^6.9.0",
37 | "babel-preset-react": "^6.11.1",
38 | "babel-preset-stage-0": "^6.5.0",
39 | "babel-runtime": "^6.26.0",
40 | "chai": "^4.0.2",
41 | "enzyme": "3.2.0",
42 | "enzyme-adapter-react-16": "^1.1.0",
43 | "expect": "^1.20.2",
44 | "jest": "^21.0.0",
45 | "json": "^9.0.4",
46 | "mocha": "^3.0.2",
47 | "npm-run-all": "^4.0.2",
48 | "prop-types": "^15.6.0",
49 | "react": "^16.2.0",
50 | "react-dom": "^16.2.0",
51 | "react-test-renderer": "^16.2.0",
52 | "rimraf": "^2.5.4"
53 | },
54 | "dependencies": {
55 | "aphrodite": "^1.1.0"
56 | },
57 | "peerDependencies": {
58 | "@storybook/addons": "^6.0.0",
59 | "react": "^0.14.7 || ^15.0.0 || ^16.0.0",
60 | "react-dom": "^0.14.7 || ^15.0.0 || ^16.0.0"
61 | },
62 | "jest": {
63 | "setupFiles": [
64 | "./.storybook/__conf__/jestMockConfig.js",
65 | "./.storybook/__conf__/enzymeConfig.js"
66 | ],
67 | "testRegex": "../.*.ci.jest.stories.js$",
68 | "automock": false,
69 | "globals": {
70 | "__TESTS__": true
71 | },
72 | "unmockedModulePathPatterns": [
73 | "/node_modules/react/",
74 | "/node_modules/lodash/",
75 | "/node_modules/material-ui/",
76 | "/node_modules/react-dom/",
77 | "/node_modules/enzyme/",
78 | "/node_modules/chai/",
79 | "/node_modules/core-js/",
80 | "/node_modules/sinon/",
81 | "/node_modules/uuid/",
82 | "/node_modules/es6-shim/",
83 | "/node_modules/react-addons-test-utils/"
84 | ]
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/preview.js:
--------------------------------------------------------------------------------
1 | import addons from '@storybook/addons';
2 | import { EVENT_ID } from './';
3 |
4 | let currentStory = "";
5 | const results = {};
6 | const beforeEachFunc = {};
7 | const afterFunc = {};
8 | const afterEachFunc = {};
9 |
10 | export function specs(specs) {
11 | let storyName = specs();
12 | const channel = addons.getChannel();
13 | channel.emit(EVENT_ID, { storyName, results: results[storyName] });
14 | }
15 |
16 | export const describe = (storyName, func) => {
17 | currentStory = storyName;
18 | results[currentStory] = { goodResults: [], wrongResults: [] };
19 | func();
20 | if(afterFunc[currentStory]) afterFunc[currentStory]();
21 | return storyName;
22 | };
23 |
24 | export const it = function(desc, func) {
25 | const storyName = currentStory;
26 |
27 | const pushGoodResult = () => {
28 | results[storyName].goodResults.push(desc);
29 | };
30 |
31 | const pushWrongResult = (e) => {
32 | console.error(`${storyName} - ${desc} : ${e}`);
33 | results[storyName].wrongResults.push({ spec: desc, message: e.message });
34 | };
35 |
36 | const emitAsyncResultsUpdate = () => {
37 | const channel = addons.getChannel();
38 | channel.emit(EVENT_ID, { asyncResultsUpdate: true, storyName, results: results[storyName] });
39 | };
40 |
41 | const done = (e) => {
42 | if (e) pushWrongResult(e);
43 | else pushGoodResult();
44 | emitAsyncResultsUpdate();
45 | };
46 |
47 | if (beforeEachFunc[storyName]) beforeEachFunc[storyName]();
48 |
49 | try {
50 | if (func.length) func(done);
51 | else {
52 | func();
53 | pushGoodResult();
54 | }
55 | } catch (e) {
56 | pushWrongResult(e);
57 | }
58 |
59 | if (afterEachFunc[storyName]) afterEachFunc[storyName]();
60 | };
61 |
62 | export const before = function(func) {
63 | func()
64 | };
65 |
66 | export const beforeEach = function(func) {
67 | beforeEachFunc[currentStory] = func;
68 | };
69 |
70 | export const after = function(func) {
71 | afterFunc[currentStory] = func;
72 | };
73 |
74 | export const afterEach = function(func) {
75 | afterEachFunc[currentStory] = func;
76 | };
77 |
78 | export const fit = function (desc, func) {
79 | it(desc, func)
80 | };
81 |
82 | export const xit = function (desc, func) {
83 |
84 | };
85 |
86 | export const xdescribe = function (storyName, func){
87 | currentStory = storyName;
88 | results[currentStory] = {
89 | goodResults: [],
90 | wrongResults: []
91 | };
92 | return storyName;
93 | };
94 |
95 | describe.skip = function (storyName, func){
96 | currentStory = storyName;
97 | results[currentStory] = {
98 | goodResults: [],
99 | wrongResults: []
100 | };
101 | return storyName;
102 | };
103 |
104 | it.only = function (desc, func) {
105 | it(desc, func);
106 | };
107 |
108 | it.skip = function (desc, func) {
109 |
110 | };
111 |
112 | describe.only = function (storyName, func) {
113 | return describe(storyName, func)
114 | };
115 |
116 | export const fdescribe = function (storyName, func) {
117 | return describe(storyName, func)
118 | };
119 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Specifications Addon [](https://www.npmjs.com/package/storybook-addon-specifications)
2 |
3 | > **Needs at least [react-storybook](https://github.com/kadirahq/react-storybook) 2.2.1**
4 |
5 | This addon for storybook will allow you to write tests based on your stories and display results directly inside storybook.
6 |
7 | ⚠️ I don't have time to maintain this project anymore. If someone is interested to take over the maintainance and further development, please contact me.
8 |
9 | > **If you want to learn more about the ideas behind this addon, you can read this article : [Building a react components living documentation using react-storybook.](https://medium.com/@mlthuret/building-a-react-components-living-documentation-using-react-storybook-5f11f0e7d23e#.5g58g5i3t).**
10 |
11 | 
12 |
13 | ## Table of contents
14 |
15 | * [Getting Started](#getting-started)
16 | * [Using enzyme](#using-enzyme)
17 | * [Use your stories with a test runner](#use-your-stories-with-a-test-runner)
18 | * [Using Jest](#using-jest)
19 | * [Hooks and specifics jest features](#hooks-and-specifics-jest-features)
20 | * [Snapshot all your stories automatically](#snapshot-all-your-stories-automatically)
21 | * [Using Mocha](#using-mocha)
22 | * [Hooks and specifics mocha features](#hooks-and-specifics-mocha-features)
23 | * [Loading External Test Files](#loading-external-test-files)
24 |
25 | ## Getting Started
26 |
27 | First, install the addon
28 |
29 | ```shell
30 | npm install -D storybook-addon-specifications
31 | ```
32 |
33 | Add this line to your `addons.js` file (create this file inside your storybook config directory if needed).
34 |
35 | ```js
36 | import 'storybook-addon-specifications/register';
37 | ```
38 |
39 | Import the `specs`, `describe` and `it` functions and use it to write your tests. This example uses *enzyme* and *expect* to perform the testing.
40 |
41 | The first parameter of the describe function **must be the same** as the story's name.
42 |
43 | ```js
44 | import { storiesOf } from '@kadira/storybook'
45 | import { specs, describe, it } from 'storybook-addon-specifications'
46 |
47 | import {mount} from "enzyme";
48 | import expect from "expect";
49 |
50 | const stories = storiesOf('Button', module);
51 |
52 | stories.add('Hello World', function () {
53 | const story =
54 | ;
57 |
58 | specs(() => describe('Hello World', function () {
59 | it('Should have the Hello World label', function () {
60 | let output = mount(story);
61 | expect(output.text()).toContain('Hello World');
62 | });
63 | }));
64 |
65 | return story;
66 | });
67 | ```
68 |
69 |
70 | You can use `beforeEach`, `before`, `after` and `afterEach` functions to mutualize or clean up some stuff.
71 |
72 |
73 | ## Using enzyme
74 |
75 | To use enzyme inside storybooks, you will need to do the following:
76 |
77 | 1. Configure enzyme with an appropriate adapter inside your .storybook/config.js:
78 | ```js
79 | import {configure} from 'enzyme';
80 | import Adapter from 'enzyme-adapter-react-16';
81 |
82 | configure({ adapter: new Adapter() });
83 | ```
84 | 2. Add the following lines to your webpack.config.js:
85 | ```
86 | externals: {
87 | 'jsdom': 'window',
88 | 'cheerio': 'window',
89 | 'react/lib/ExecutionEnvironment': true,
90 | 'react/lib/ReactContext': 'window',
91 | 'react/addons': true,
92 | }
93 | ```
94 | 3. Add the **json library** to your dev dependencies.
95 |
96 |
97 |
98 | ## Use your stories with a test runner
99 |
100 | Writing tests directly next to the component declaration used for the story is already a great thing, but it would be better if those tests can be reused with our test runner, and later be used with our CI.
101 |
102 | To do that, the idea is to add to the test runner, all the files used for declaring stories.
103 | But because this addon redefine describe and it functions, you'll need some extra-configuration to make the tests pass within the test runner.
104 |
105 | This repository has a [directory full of examples](https://github.com/mthuret/storybook-addon-specifications/tree/master/.storybook) where you can find everything that is describe here.
106 |
107 | ### Using Jest
108 |
109 | You can use the mocking functionality of Jest to switch between the real describe and its implementation of Jest or
110 | the one for this addon.
111 |
112 | Inside .storybook, add a facade.js file with the following content :
113 |
114 | ```js
115 | import {storiesOf as storiesOfReal, action as actionReal, linkTo as linkToReal} from "@kadira/storybook"
116 | import { specs as specsReal, describe as describeReal, it as itReal } from 'storybook-addon-specifications'
117 |
118 | export const storiesOf = storiesOfReal;
119 | export const action = actionReal;
120 | export const linkTo = linkToReal;
121 | export const specs = specsReal;
122 | export const describe = describeReal;
123 | export const it = itReal;
124 | ```
125 |
126 | Create a \_\_mocks\_\_ directory within .storybook and add also a facade.js file.
127 |
128 | ```js
129 | export const storiesOf = function storiesOf() {
130 | var api = {};
131 | api.add = (name, func)=> {
132 | func();
133 | return api;
134 | };
135 | api.addWithInfo = (name, func)=> {
136 | func();
137 | return api;
138 | };
139 | return api;
140 | };
141 | export const action = () => {};
142 |
143 | export const linkTo = () => {};
144 |
145 | export const specs = (spec) => {
146 | spec();
147 | };
148 |
149 | export const describe = jasmine.currentEnv_.describe;
150 | export const it = jasmine.currentEnv_.it;
151 | ```
152 |
153 | Create or add to your Jest config file the following line :
154 |
155 | ```js
156 | jest.mock('./.storybook/facade');
157 | ```
158 |
159 | > **Inside your stories file you must now use the .storybook/facade.js file for imports**.
160 |
161 | Finally add this to your Jest configuration :
162 |
163 | ```js
164 | "jest":{
165 | "setupFiles": [
166 | "./path/to/your/jest/config/file.js"
167 | ],
168 | "automock": false,
169 | }
170 | ```
171 |
172 | #### Hooks and specifics jest features
173 |
174 | This addon now supports :
175 | * beforeEach
176 | * afterEach
177 | * fit (no effect on storybook specs execution)
178 | * xit
179 | * xdescribe
180 |
181 | Please refer to Jest documentation to know how to use them.
182 |
183 | If you want to use that with storybook, you'll need to add them to your facade and facade-mock files.
184 | You can find the complete configuration by looking at the [samples directory](https://github.com/mthuret/storybook-addon-specifications/tree/master/.storybook)
185 |
186 | #### Snapshot all your stories automatically
187 |
188 | >**Warning :** This part will describe how to automatically add Jest snapshots to every story you write. It will allow you to take advantage of this Jest feature but will not have any effect inside storybook. Indeed, you don't even need to add this addon to your project if you don't plan to use the specs() function. If I describe the idea here, it's only because it uses the trick I explained before allowing you to write tests inside stories and still be able to execute them with a test runner.
189 |
190 | 
191 |
192 | The only thing to do is to modify the facade.js mock file (the one used by Jest) to look like this :
193 |
194 | ```js
195 | export const storiesOf = function storiesOf() {
196 | var api = {};
197 | var story;
198 | api.add = (name, func)=> {
199 | story = func();
200 | snapshot(name, story);
201 | return api;
202 | };
203 | api.addWithInfo = (name, func)=> {
204 | story = func();
205 | snapshot(name, story);
206 | return api;
207 | };
208 | return api;
209 | };
210 | export const action = () => {};
211 |
212 | export const linkTo = () => {};
213 |
214 | export const specs = (spec) => {
215 | spec()
216 | };
217 |
218 | export const snapshot = (name, story) => {
219 | it(name, function () {
220 | let renderer = require("react-test-renderer");
221 | const tree = renderer.create(story).toJSON();
222 | expect(tree).toMatchSnapshot();
223 | });
224 | };
225 |
226 | export const describe = jasmine.currentEnv_.describe;
227 | export const it = jasmine.currentEnv_.it;
228 | ```
229 |
230 | Every story added to storybook, will now have a snapshot.
231 |
232 | If for any reason you want to choose when to snapshot a story, that's also possible.
233 | 1. remove snapshot() function calls from add and addWithInfo in facade.js mock file.
234 | 2. use the snapshot() function directly inside the story like you do with specs()
235 | 3. Add this line to the facade.js file used for import functions.
236 | ```js
237 | export const snapshot = () => {};
238 | ```
239 | When storybook is going to run, it will do nothing with the snapshot function.
240 |
241 | ### Using Mocha
242 |
243 | Please note that when using mocha as a test runner, all storybook functions that you
244 | use on your stories files are going to become globally defined. (see step4).
245 | The reason for that simple, unlike Jest, mocking functions is going to be made
246 | by redefining them globally (see step 3).
247 |
248 | 1. Create the same facade.js file than for the jest configuration
249 |
250 | 2. Create wherever you want a new file that will mock the storybook api
251 |
252 | ```js
253 | export const storiesOf = function storiesOf() {
254 | var api = {};
255 | api.add = (name, func)=> {
256 | func();
257 | return api;
258 | };
259 | api.addWithInfo = (name, func)=> {
260 | func();
261 | return api;
262 | };
263 | return api;
264 | };
265 | export const action = () => {};
266 |
267 | export const linkTo = () => {};
268 |
269 | export const specs = (spec) => {
270 | spec();
271 | };
272 |
273 | export const describe = describe;
274 | export const it = it;
275 | ```
276 |
277 | 3. Then create or add those lines to a mocha config file :
278 |
279 | ```js
280 | import {storiesOf, action, linkTo, specs, describe, it} from "path/to/your/mock/file";
281 | global.storiesOf = storiesOf;
282 | global.action = action;
283 | global.linkTo = linkTo;
284 | global.specs = specs;
285 | global.describe = describe;
286 | global.it = it;
287 | ```
288 |
289 | 4. And also those lines to the storybook config file
290 |
291 | ```js
292 | import {storiesOf, action, linkTo, specs, describe, it} from "./facade";
293 |
294 | global.storiesOf = storiesOf;
295 | global.action = action;
296 | global.linkTo = linkTo;
297 | global.specs = specs;
298 | global.describe = describe;
299 | global.it = it;
300 | ```
301 |
302 | Finally add this to your mocha running script
303 |
304 | ```
305 | -require test/path/to/your/config/file.js
306 | ```
307 |
308 | > **Warning** : if you already have some specific configuration for mocha, please note
309 | that this sample needs to be adapted for your specific use-case. Please also note that
310 | in the sample directory of this repository, the mocha config file is a little bit more
311 | complexe in order to be able to use jsdom.
312 |
313 | >If you need it, and don't use it already, you can add those line :
314 |
315 | >```js
316 | > // choose one of the following
317 | > import { jsdom } from 'jsdom'; // older versions of JSDOM
318 | > import { JSDOM } from 'jsdom'; // newer version
319 | >/**
320 | > *Mocking browser-like DOM
321 | > */
322 | >// old jsdom
323 | >global.document = jsdom('', {
324 | > headers: {
325 | > 'User-Agent':
326 | > 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7)' +
327 | > ' AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.71 Safari/534.24'
328 | > }
329 | >});
330 | // new version
331 | >global.document = (new JSDOM('', {
332 | 324
333 | > headers: {
334 | 325
335 | > 'User-Agent':
336 | 326
337 | > 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7)' +
338 | 327
339 | > ' AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.71 Safari/534.24'
340 | 328
341 | > }
342 | 329
343 | >})).window.document;
344 | >global.window = document.defaultView;
345 | >global.navigator = global.window.navigator;
346 | >```
347 |
348 | or if you are a newer version of jsdom
349 |
350 | >```js
351 | >/**
352 | > *Mocking browser-like DOM
353 | > */
354 | >import { JSDOM } from 'jsdom';
355 | >global.document = (new JSDOM('', {
356 | > headers: {
357 | > 'User-Agent':
358 | > 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7)' +
359 | > ' AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.71 Safari/534.24'
360 | > }
361 | >})).window.document;
362 | >global.window = document.defaultView;
363 | >global.navigator = global.window.navigator;
364 | >```
365 |
366 | #### Hooks and specifics mocha features
367 |
368 | This addon now supports :
369 |
370 | * beforeEach
371 | * afterEach
372 | * before
373 | * after
374 | * describe.only (no effect on storybook specs execution)
375 | * describe.skip
376 | * it.only (no effect on storybook specs execution)
377 | * it.skip
378 |
379 | Please refer to mocha documentation to know how to use them.
380 |
381 | If you want to use that with storybook, you'll need to add them to your mocha config and storybook config files.
382 | You can find the complete configuration by looking at the [samples directory](https://github.com/mthuret/storybook-addon-specifications/tree/master/.storybook)
383 |
384 | #### Loading External Test Files
385 |
386 | It is also possible to load your test files externally from their respective test files.
387 |
388 | ```
389 | |- example.stories.js
390 | |- example.test.js
391 | |- example.js
392 | ```
393 |
394 | This allows us to run both our test runner, and view the test results inside of React Storybook.
395 |
396 | ```js
397 | import React from 'react'
398 | import { storiesOf } from '@kadira/storybook'
399 | import { specs } from 'storybook-addon-specifications'
400 |
401 | import { tests } from './Example.test'
402 | import Example from './example'
403 |
404 | storiesOf('Example', module)
405 | .add('Default', () => {
406 |
407 | // tests are loaded here
408 | specs(() => tests)
409 |
410 | return
411 | })
412 | ```
413 |
414 | We must first override the Jest describe/it/expect blocks to use the storybook-addon-spec's implementation.
415 | Add the following to your storybook config file.
416 |
417 | /.storybook/config.js
418 |
419 | ```js
420 | import { describe, it } from 'storybook-addon-specifications'
421 | import expect from 'expect'
422 |
423 | window.describe = describe
424 | window.it = it
425 | window.expect = expect
426 | ```
427 |
--------------------------------------------------------------------------------