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

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;
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