Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Storyshots addon refactoring #3745

Merged
merged 11 commits into from
Jun 12, 2018
27 changes: 25 additions & 2 deletions addons/storyshots/storyshots-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,32 @@ initStoryshots({

Take a snapshot of a shallow-rendered version of the component. Note that this option will be overriden if you pass a `renderer` option.

### `getSnapshotFileName`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably worth mentioning in MIGRATION.md, as well as puppeteer moving to other package

### `stories2snapsConverter`
This parameter should be an instance of the [`Stories2SnapsConverter`](src/Stories2SnapsConverter.js) (or a derived from it) Class that is used to convert story-file name to snapshot-file name and vice versa.

Utility function used in `multiSnapshotWithOptions`. This is made available for users who implement custom test functions that also want to take advantage of multi-file storyshots.
By default, the instance of this class is created with these default options:

```js
{
snapshotsDirName: '__snapshots__',
snapshotExtension: '.storyshot',
storiesExtensions: ['.js', '.jsx', '.ts', '.tsx'],
}
```

This class might be overridden to extend the existing conversion functionality or instantiated to provide different options:

```js
import initStoryshots, { Stories2SnapsConverter } from '@storybook/addon-storyshots';

initStoryshots({
stories2snapsConverter: new Stories2SnapsConverter({
snapshotExtension: '.storypuke',
storiesExtensions: ['.foo'],
}),
});

```

###### Example:

Expand Down
50 changes: 50 additions & 0 deletions addons/storyshots/storyshots-core/src/Stories2SnapsConverter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import path from 'path';

const defaultOptions = {
snapshotsDirName: '__snapshots__',
snapshotExtension: '.storyshot',
storiesExtensions: ['.js', '.jsx', '.ts', '.tsx'],
};

class DefaultStories2SnapsConverter {
constructor(options = {}) {
this.options = {
...defaultOptions,
...options,
};
}

getSnapshotExtension = () => this.options.snapshotExtension;

getStoryshotFile(fileName) {
const { dir, name } = path.parse(fileName);
const { snapshotsDirName, snapshotExtension } = this.options;

return path.format({ dir: path.join(dir, snapshotsDirName), name, ext: snapshotExtension });
}

getSnapshotFileName(context) {
const { fileName } = context;

if (!fileName) {
return null;
}

return this.getStoryshotFile(fileName);
}

getPossibleStoriesFiles(storyshotFile) {
const { dir, name } = path.parse(storyshotFile);
const { storiesExtensions } = this.options;

return storiesExtensions.map(ext =>
path.format({
dir: path.dirname(dir),
name,
ext,
})
);
}
}

export default DefaultStories2SnapsConverter;
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { getPossibleStoriesFiles, getSnapshotFileName } from './utils';
import Stories2SnapsConverter from './Stories2SnapsConverter';

const target = new Stories2SnapsConverter();

describe('getSnapshotFileName', () => {
it('fileName is provided - snapshot is stored in __snapshots__ dir', () => {
const context = { fileName: 'foo.js' };

const result = getSnapshotFileName(context);
const result = target.getSnapshotFileName(context);
const platformAgnosticResult = result.replace(/\\|\//g, '/');

expect(platformAgnosticResult).toBe('__snapshots__/foo.storyshot');
Expand All @@ -13,7 +15,7 @@ describe('getSnapshotFileName', () => {
it('fileName with multiple extensions is provided - only the last extension is replaced', () => {
const context = { fileName: 'foo.web.stories.js' };

const result = getSnapshotFileName(context);
const result = target.getSnapshotFileName(context);
const platformAgnosticResult = result.replace(/\\|\//g, '/');

expect(platformAgnosticResult).toBe('__snapshots__/foo.web.stories.storyshot');
Expand All @@ -22,7 +24,7 @@ describe('getSnapshotFileName', () => {
it('fileName with dir is provided - __snapshots__ dir is created inside another dir', () => {
const context = { fileName: 'test/foo.js' };

const result = getSnapshotFileName(context);
const result = target.getSnapshotFileName(context);
const platformAgnosticResult = result.replace(/\\|\//g, '/');

expect(platformAgnosticResult).toBe('test/__snapshots__/foo.storyshot');
Expand All @@ -33,7 +35,7 @@ describe('getPossibleStoriesFiles', () => {
it('storyshots is provided and all the posible stories file names are returned', () => {
const storyshots = 'test/__snapshots__/foo.web.stories.storyshot';

const result = getPossibleStoriesFiles(storyshots);
const result = target.getPossibleStoriesFiles(storyshots);
const platformAgnosticResult = result.map(path => path.replace(/\\|\//g, '/'));

expect(platformAgnosticResult).toEqual([
Expand Down
90 changes: 90 additions & 0 deletions addons/storyshots/storyshots-core/src/api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import global, { describe } from 'global';
import addons, { mockChannel } from '@storybook/addons';
import snapshotsTests from './snapshotsTestsTemplate';
import integrityTest from './integrityTestTemplate';
import getIntegrityOptions from './getIntegrityOptions';
import loadFramework from '../frameworks/frameworkLoader';
import Stories2SnapsConverter from '../Stories2SnapsConverter';
import { snapshotWithOptions } from '../test-bodies';

global.STORYBOOK_REACT_CLASSES = global.STORYBOOK_REACT_CLASSES || {};

const defaultStories2SnapsConverter = new Stories2SnapsConverter();
const methods = ['beforeAll', 'beforeEach', 'afterEach', 'afterAll'];

function ensureOptionsDefaults(options) {
const {
suite = 'Storyshots',
storyNameRegex,
storyKindRegex,
renderer,
serializer,
stories2snapsConverter = defaultStories2SnapsConverter,
test: testMethod = snapshotWithOptions({ renderer, serializer }),
} = options;

const integrityOptions = getIntegrityOptions(options);

return {
suite,
storyNameRegex,
storyKindRegex,
stories2snapsConverter,
testMethod,
integrityOptions,
};
}

function callTestMethodGlobals(testMethod) {
methods.forEach(method => {
if (typeof testMethod[method] === 'function') {
global[method](testMethod[method]);
}
});
}

function testStorySnapshots(options = {}) {
if (typeof describe !== 'function') {
throw new Error('testStorySnapshots is intended only to be used inside jest');
}

addons.setChannel(mockChannel());

const { storybook, framework, renderTree, renderShallowTree } = loadFramework(options);
const storiesGroups = storybook.getStorybook();

if (storiesGroups.length === 0) {
throw new Error('storyshots found 0 stories');
}

const {
suite,
storyNameRegex,
storyKindRegex,
stories2snapsConverter,
testMethod,
integrityOptions,
} = ensureOptionsDefaults(options);

const testMethodParams = {
renderTree,
renderShallowTree,
stories2snapsConverter,
};

callTestMethodGlobals(testMethod);

snapshotsTests({
groups: storiesGroups,
suite,
framework,
storyKindRegex,
storyNameRegex,
testMethod,
testMethodParams,
});

integrityTest(integrityOptions, stories2snapsConverter);
}

export default testStorySnapshots;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import fs from 'fs';
import glob from 'glob';
import { describe, it } from 'global';

function integrityTest(integrityOptions, stories2snapsConverter) {
if (integrityOptions === false) {
return;
}

describe('Storyshots Integrity', () => {
it('Abandoned Storyshots', () => {
const snapshotExtension = stories2snapsConverter.getSnapshotExtension();
const storyshots = glob.sync(`**/*${snapshotExtension}`, integrityOptions);

const abandonedStoryshots = storyshots.filter(fileName => {
const possibleStoriesFiles = stories2snapsConverter.getPossibleStoriesFiles(fileName);
return !possibleStoriesFiles.some(fs.existsSync);
});
expect(abandonedStoryshots).toHaveLength(0);
});
});
}

export default integrityTest;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, it } from 'global';

function snapshotTest({ story, kind, fileName, framework, testMethod, testMethodParams }) {
const { name } = story;

it(name, () => {
const context = { fileName, kind, story: name, framework };

return testMethod({
story,
context,
...testMethodParams,
});
});
}

function snapshotTestSuite({ kind, stories, suite, storyNameRegex, ...restParams }) {
describe(suite, () => {
describe(kind, () => {
// eslint-disable-next-line
for (const story of stories) {
if (storyNameRegex && !story.name.match(storyNameRegex)) {
// eslint-disable-next-line
continue;
}

snapshotTest({ story, kind, ...restParams });
}
});
});
}

function snapshotsTests({ groups, storyKindRegex, ...restParams }) {
// eslint-disable-next-line
for (const group of groups) {
const { fileName, kind, stories } = group;

if (storyKindRegex && !kind.match(storyKindRegex)) {
// eslint-disable-next-line
continue;
}

snapshotTestSuite({ stories, kind, fileName, ...restParams });
}
}

export default snapshotsTests;
19 changes: 0 additions & 19 deletions addons/storyshots/storyshots-core/src/frameworkLoader.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import appOptions from '@storybook/angular/options';

import runWithRequireContext from '../require_context';
import hasDependency from '../hasDependency';
import loadConfig from '../config-loader';
Expand All @@ -22,6 +20,8 @@ function test(options) {
function load(options) {
setupAngularJestPreset();

const appOptions = require.requireActual('@storybook/angular/options').default;

const { content, contextOpts } = loadConfig({
configDirPath: options.configPath,
appOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ function getConfigContent({ resolvedConfigDirPath, configPath, appOptions }) {
return babel.transformFileSync(configPath, babelConfig).code;
}

function load({ configDirPath, babelConfigPath }) {
function load({ configDirPath, appOptions }) {
const resolvedConfigDirPath = path.resolve(configDirPath || '.storybook');
const configPath = path.join(resolvedConfigDirPath, 'config.js');

const content = getConfigContent({ resolvedConfigDirPath, configPath, babelConfigPath });
const content = getConfigContent({ resolvedConfigDirPath, configPath, appOptions });
const contextOpts = { filename: configPath, dirname: resolvedConfigDirPath };

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* eslint-disable global-require,import/no-dynamic-require */
import fs from 'fs';
import path from 'path';

const loaderScriptName = 'loader.js';

const isDirectory = source => fs.lstatSync(source).isDirectory();

function getLoaders() {
return fs
.readdirSync(__dirname)
.map(name => path.join(__dirname, name))
.filter(isDirectory)
.map(framework => path.join(framework, loaderScriptName))
.filter(fs.existsSync)
.map(loader => require(loader).default);
}

function loadFramework(options) {
const loaders = getLoaders();

const loader = loaders.find(frameworkLoader => frameworkLoader.test(options));

if (!loader) {
throw new Error('storyshots is intended only to be used with storybook');
}

return loader.load(options);
}

export default loadFramework;
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import appOptions from '@storybook/angular/options';

import global from 'global';
import runWithRequireContext from '../require_context';
import loadConfig from '../config-loader';
Expand All @@ -11,6 +9,8 @@ function test(options) {
function load(options) {
global.STORYBOOK_ENV = 'html';

const appOptions = require.requireActual('@storybook/html/options').default;

const { content, contextOpts } = loadConfig({
configDirPath: options.configPath,
appOptions,
Expand Down
Loading