Skip to content

Commit

Permalink
Merge pull request #11 from kadirahq/refactor
Browse files Browse the repository at this point in the history
Refactor
  • Loading branch information
arunoda authored Sep 22, 2016
2 parents 3bb69cb + 1d990d1 commit 6666da6
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 12 deletions.
31 changes: 20 additions & 11 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

import runTests from './test_runner';
import { getStorybook } from '@kadira/storybook';
import Runner from './test_runner';
import path from 'path';
import fs from 'fs';
import program from 'commander';
import chokidar from 'chokidar';
import EventEmitter from 'events';
import loadBabelConfig from '@kadira/storybook/dist/server/babel_config';

const { jasmine } = global;
import { filterStorybook } from './util';

program
.option('-c, --config-dir [dir-name]',
Expand All @@ -27,7 +26,8 @@ const {
configDir = './.storybook',
polyfills: polyfillsPath = require.resolve('./default_config/polyfills.js'),
loaders: loadersPath = require.resolve('./default_config/loaders.js'),
} = program
grep
} = program;

const configPath = path.resolve(`${configDir}`, 'config');

Expand All @@ -41,6 +41,7 @@ require('babel-polyfill');

// load loaders
const loaders = require(path.resolve(loadersPath));

Object.keys(loaders).forEach(ext => {
const loader = loaders[ext];
require.extensions[`.${ext}`] = (m, filepath) => loader(filepath);
Expand All @@ -49,23 +50,31 @@ Object.keys(loaders).forEach(ext => {
// load polyfills
require(path.resolve(polyfillsPath));

async function main () {
// set userAgent so storybook knows we're storyshots
if(!global.navigator) {
global.navigator = {}
};
global.navigator.userAgent = 'storyshots';

const runner = new Runner(program);

async function main() {
try {
require(configPath);
const storybook = require('@kadira/storybook').getStorybook();
const addons = require('@kadira/storybook-addons').default;

// Channel for addons is created by storybook manager from the client side.
// We need to polyfill it for the server side.
const channel = new EventEmitter()
const channel = new EventEmitter();
addons.setChannel(channel);
await runTests(storybook, program);
} catch(e) {
await runner.run(filterStorybook(storybook, grep));
} catch (e) {
console.log(e.stack);
}
}

if(program.watch) {
if (program.watch) {
var watcher = chokidar.watch('.', {
ignored: 'node_modules', // TODO: Should node_modules also be watched?
persistent: true
Expand All @@ -78,11 +87,11 @@ if(program.watch) {
// changes were made.
Object.keys(require.cache).forEach(key => {
delete require.cache[key];
})
});

main();
});
})
});
}

main();
2 changes: 1 addition & 1 deletion src/default_config/polyfills.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Object.keys(document.defaultView).forEach((property) => {
});

global.navigator = {
userAgent: 'node.js',
userAgent: 'storyshots',
};

global.localStorage = global.window.localStorage = {
Expand Down
126 changes: 126 additions & 0 deletions src/test_runner/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import chalk from 'chalk';
import SnapshotRunner from './snapshot_runner';

export default class Runner {
constructor(options) {
const {
configDir = './.storybook',
update,
updateInteractive: interactive,
} = options;

this.configDir = configDir;
this.update = update;
this.interactive = interactive;

this.testState = {
added: 0,
matched: 0,
unmatched: 0,
updated: 0,
obsolete: 0,
errored: 0,
};

this.runner = new SnapshotRunner(configDir);
}

updateState(result) {
this.testState[result.state]++;
logState(result);
}

completed() {
logSummary(this.testState);
}

async run(storybook) {
const options = {
update: this.update,
interactive: this.interactive,
};

for (const group of storybook) {
try {
this.runner.startKind(group.kind);
this.updateState({state: 'started-kind', name: group.kind});
for (const story of group.stories) {
try {
const result = await this.runner.runStory(story, options);
this.updateState({...result, name: story.name});
} catch (err) {
// Error on story
this.updateState({state: 'errored', message: err, name: story.name});
}
}
this.runner.endKind(options);
} catch (err) {
// Error on kind
this.updateState({state: 'errored-kind', message: err});
}
}

this.completed();
}
}

function logState({state, name, message}) {
switch (state) {
case 'added':
process.stdout.write(chalk.cyan(`+ ${name}: Added`));
break;
case 'updated':
process.stdout.write(chalk.cyan(`● ${name}: Updated`));
break;
case 'matched':
process.stdout.write(chalk.green(`✓ ${name}`));
break;
case 'unmatched':
process.stdout.write('\n');
process.stdout.write(chalk.red(`✗ ${name}\n`));
process.stdout.write(' ' + message.split('\n').join('\n '));
process.stdout.write('\n');
break;
case 'errored':
case 'errored-kind':
process.stdout.write('\n');
process.stdout.write(chalk.red(`✗ ${name}: ERROR\n`));
const output = message.stack || message;
process.stdout.write(chalk.dim(' ' + output.split('\n').join('\n ')));
process.stdout.write('\n');
break;
case 'started-kind':
process.stdout.write('\n');
process.stdout.write(chalk.underline(name));
break;
default:
process.stdout.write(`Error occured when testing ${state}`);
}
process.stdout.write('\n');
}

function logSummary(state) {
const { added, matched, unmatched, updated, errored, obsolete } = state;
const total = added + matched + unmatched + updated + errored;
process.stdout.write(chalk.bold('Test summary\n'));
process.stdout.write(`> ${total} stories tested.\n`);
if (matched > 0) {
process.stdout.write(chalk.green(`> ${matched}/${total} stories matched with snapshots.\n`));
}
if (unmatched > 0) {
process.stdout.write(chalk.red(`> ${unmatched}/${total} differ from snapshots.\n`));
}
if (updated > 0) {
process.stdout.write(chalk.cyan(`> ${updated} snapshots updated to match current stories.\n`));
}
if (added > 0) {
process.stdout.write(chalk.cyan(`> ${added} snapshots newly added.\n`));
}
if (errored > 0) {
process.stdout.write(chalk.red(`> ${errored} tests errored.\n`));
}
if (obsolete > 0) {
process.stdout.write(chalk.cyan(`> ${obsolete} unused snapshots remaining. Run with -u to remove them.\n`));
}
}

95 changes: 95 additions & 0 deletions src/test_runner/snapshot_runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import path from 'path';
import jestSnapshot from 'jest-snapshot';
import ReactTestRenderer from 'react-test-renderer';
import diff from 'jest-diff';
import promptly from 'promptly';

export default class SnapshotRunner {
constructor(configDir) {
this.configDir = configDir;
this.kind = '';
}

startKind(kind) {
const filePath = path.resolve(this.configDir, kind);

const fakeJasmine = {
Spec: () => {}
};
this.state = jestSnapshot.getSnapshotState(fakeJasmine, filePath);
this.kind = kind;
}

async runStory(story, {update, interactive}) {
this.state.setSpecName(story.name);
this.state.setCounter(0);
const snapshot = this.state.snapshot;

const key = story.name;
const hasSnapshot = snapshot.has(key);
const context = { kind: this.kind, story };
const tree = story.render(context);
const renderer = ReactTestRenderer.create(tree);
const actual = renderer.toJSON();

if (!snapshot.fileExists() || !hasSnapshot) {
// If the file does not exist of snapshot of this name is not present
// add it.
snapshot.add(key, actual);
return {state: 'added'};
}

const matches = snapshot.matches(key, actual);
const pass = matches.pass;
if (pass) {
// Snapshot matches with the story
return {state: 'matched'};
}

// Snapshot does not match story
if (update) {
snapshot.add(key, actual);
return {state: 'updated'};
}

const diffMessage = diff(
matches.expected.trim(),
matches.actual.trim(),
{
aAnnotation: 'Snapshot',
bAnnotation: 'Current story',
},
);

if (interactive) {
const shouldUpdate = await this.confirmUpate(diffMessage);
if (shouldUpdate) {
snapshot.add(key, actual);
return {state: 'updated'};
}
}

return {state: 'unmatched', message: diffMessage};
}

endKind({update}) {
const snapshot = this.state.snapshot;
if (update) {
snapshot.removeUncheckedKeys();
}
snapshot.save(update);
}

async confirmUpate(diffMessage) {
process.stdout.write('\nReceived story is different from stored snapshot.\n');
process.stdout.write(' ' + diffMessage.split('\n').join('\n '));
let ans = await promptly.prompt('Should this snapshot be updated?(y/n)');
while (ans !== 'y' && ans !== 'n') {
process.stdout.write('Enter only y (yes) or n (no)\n');
ans = await promptly.prompt('Should this snapshot be updated?(y/n)');
}
process.stdout.write('\n');

return ans === 'y';
}
}

0 comments on commit 6666da6

Please sign in to comment.