Skip to content

Instantly share code, notes, and snippets.

@tmeasday
Last active January 4, 2021 10:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tmeasday/c0de43edf8f91171fd7ec8bd5c90c064 to your computer and use it in GitHub Desktop.
Save tmeasday/c0de43edf8f91171fd7ec8bd5c90c064 to your computer and use it in GitHub Desktop.
Using Jest transformer to test CSF stories
module.exports = {
// Make sure you match CSF files!
testMatch: ['**/__tests__/**/*.js', '**/?(*.)test.js', '**/?(*.)stories.js'],
// You'll probably want to add some code to mock out things for stories
setupFilesAfterEnv: ['<rootDir>/.storybook/setupFilesAfterEnv'],
transform: {
// This is the key bit!
'^.+\\.stories\\.js$': '<rootDir>/.storybook/transformer',
'\\.js$': 'babel-jest',
},
// ...
};
// add whatever you need to mock out stuff for stories, this would normally be at the top-level in your storyshots.test.js
// This is what we use (Chromatic does the rest!) but you can include any test body you like!
import React from 'react';
const renderer = require('react-test-renderer');
// eslint-disable-next-line no-console
console.warn = msg => {
throw new Error('console.warn: "' + msg + '"');
};
// eslint-disable-next-line no-console
console.error = msg => {
throw new Error('console.error: "' + msg + '"');
};
export function smokeTestStory(Component, args) {
const storyElement = <Component {...args} />;
renderer.create(storyElement, {
createNodeMock(element) {
if (element.type === 'iframe') {
return {
addEventListener: jest.fn(),
contentWindow: {
postMessage: jest.fn(),
},
};
}
if (element.type === 'code') {
return { noHighlight: true };
}
return null;
},
});
}
import { defaultDecorateStory } from '@storybook/client-api';
// Global args don't currently exist but they might one day I guess
import { args as globalArgs, decorators as globalDecorators } from './preview.js';
import { smokeTestStory } from './smokeTestStory';
function matches(storyKey, arrayOrRegex) {
if (Array.isArray(arrayOrRegex)) {
return arrayOrRegex.includes(storyKey);
}
return storyKey.match(arrayOrRegex);
}
export function testCsf({ default: defaultExport, ...otherExports }) {
if (!defaultExport) {
throw new Error('Story file not in CSF, please fix!');
}
const {
args: componentArgs,
decorators: componentDecorators = [],
excludeStories = [],
includeStories,
} = defaultExport;
let storyEntries = Object.entries(otherExports);
if (includeStories) {
storyEntries = storyEntries.filter(([storyKey]) => matches(storyKey, includeStories));
}
if (excludeStories) {
storyEntries = storyEntries.filter(([storyKey]) => !matches(storyKey, excludeStories));
}
storyEntries
.filter(
([exportName]) =>
!(Array.isArray(excludeStories)
? excludeStories.includes(exportName)
: exportName.matches(excludeStories))
)
.forEach(([exportName, exported]) => {
it(exported.story?.name || exported.storyName || exportName, () => {
const Component = defaultDecorateStory(exported, [
...globalDecorators,
...componentDecorators,
]);
smokeTestStory(Component, { ...globalArgs, ...componentArgs, ...exported.args });
});
});
}
const { transform } = require('@babel/core');
module.exports = {
process(src, filename) {
const hackedSrc = `${src}
if (!require.main) {
require('${__dirname}/testCsf').testCsf(module.exports);
}`;
const result = transform(hackedSrc, {
filename,
});
return result ? result.code : src;
},
};
@leipert
Copy link

leipert commented Dec 29, 2020

Hey @tmeasday. I have created this PR to add a new parameter storyFilterFunction to the API of storyshots-core: storybookjs/storybook#13534

I see that in this MR you move away from the regexes storyKindRegex and storyNameRegex and add an includeStories and excludeStories parameter, which can be an array or an regex. I would highly recommend to just expose the filter function to the user, so that they have full control over which stories they want to test. Reasoning as to why can be found in the PR above 😄

By the way in the code above, it seems like excludeStories is used twice, once outside the filter function and once in the filter function.

Thank you for your hard work!

@tmeasday
Copy link
Author

tmeasday commented Jan 4, 2021

Hey @leipert,

Firstly, whoa, what happened to the formatting here? Let me try and fix it.

By the way in the code above, it seems like excludeStories is used twice, once outside the filter function and once in the filter function.

Thanks, good pick up!

I see that in this MR you move away from the regexes storyKindRegex and storyNameRegex and add an includeStories and excludeStories parameter, which can be an array or an regex.

This is just a basic CSF feature I am implementing. (The include/exclude stories is defined in the CSF file). It's not a "feature" of storyshots because SS uses the Storybook's story loading machinery behind the scenes which already does this.

I don't need to add a feature like storyKindRegex or storyNameRegex (or your filter function) because one of the key benefits of doing this the way I've sketched here is that we can lean on jest directly -- each file is treated a separate test suite and you can use the standard jest tools to filter them. In fact at Chromatic we use this code above in combination with CircleCI's test splitting feature to spread these tests over several test runs already.

Although another thing to note is that the tests run significantly faster in any case and splitting may not be required at all!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment