-
-
Save tmeasday/c0de43edf8f91171fd7ec8bd5c90c064 to your computer and use it in GitHub Desktop.
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; | |
}, | |
}; |
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!
Hey @tmeasday. I have created this PR to add a new parameter
storyFilterFunction
to the API of storyshots-core: storybookjs/storybook#13534I see that in this MR you move away from the regexes
storyKindRegex
andstoryNameRegex
and add anincludeStories
andexcludeStories
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!