Skip to content

Commit

Permalink
Merge pull request #3745 from storybooks/storyshots-refactoring
Browse files Browse the repository at this point in the history
Storyshots addon refactoring
  • Loading branch information
igor-dv authored Jun 12, 2018
2 parents e4c0704 + 3063a77 commit 5180de5
Show file tree
Hide file tree
Showing 30 changed files with 312 additions and 162 deletions.
7 changes: 7 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- [Keyboard shortcuts moved](#keyboard-shortcuts-moved)
- [Removed addWithInfo](#removed-add-with-info)
- [Removed RN addons](#removed-rn-addons)
- [Storyshots imageSnapshot test function moved to a separate package](#storyshots-imagesnapshot-moved)
- [Storyshots changes](#storyshots-changes)
- [From version 3.3.x to 3.4.x](#from-version-33x-to-34x)
- [From version 3.2.x to 3.3.x](#from-version-32x-to-33x)
- [Refactored Knobs](#refactored-knobs)
Expand Down Expand Up @@ -40,6 +42,11 @@ With 4.0 as our first major release in over a year, we've collected a lot of cle

The `@storybook/react-native` had built-in addons (`addon-actions` and `addon-links`) that have been marked as deprecated since 3.x. They have been fully removed in 4.x. If your project still uses the built-ins, you'll need to add explicit dependencies on `@storybook/addon-actions` and/or `@storybook/addon-links` and import directly from those packages.

### Storyshots Changes

1. `imageSnapshot` test function was extracted from `addon-storyshots` and moved to a new package - `addon-storyshots-puppeteer` that now will be dependant on puppeteer
2. `getSnapshotFileName` export was replaced with the `Stories2SnapsConverter` class that now can be overridden for a custom implementation of the snapshot-name generation

## From version 3.3.x to 3.4.x

There are no expected breaking changes in the 3.4.x release, but 3.4 contains a major refactor to make it easier to support new frameworks, and we will document any breaking changes here if they arise.
Expand Down
31 changes: 29 additions & 2 deletions addons/storyshots/storyshots-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,33 @@ initStoryshots({

This option only needs to be set if the default `snapshotSerializers` is not set in your jest config.

### `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.

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'],
}),
});

```

## Exports

Apart from the default export (`initStoryshots`), Storyshots also exports some named test functions (see the `test` option above):
Expand Down Expand Up @@ -307,9 +334,9 @@ 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`
### `Stories2SnapsConverter`

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.
This is a class that generates snapshot's name based on the story (kind, story & filename) and vice versa.

###### 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;
24 changes: 24 additions & 0 deletions addons/storyshots/storyshots-core/src/api/integrityTestTemplate.js
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
Loading

0 comments on commit 5180de5

Please sign in to comment.