storytests-cli v2.0.1
Storytests CLI
Framework agnostic CLI Utility to generate test files from matched Storybook files.
Table of Contents
Installation
You can install storytests-cli
using npm or yarn:
npm i storytests-cli --save-dev
# or
yarn add -D storytests-cli
Usage
Prerequisites: Node.js (>10.4.0
).
Initialize a basic configuration by running storytests-cli
with:
npm run storytests init
# or
yarn storytests init
Currently all templates including the default one are preconfigured for React and Storybook@^6.0.0. However this utility is agnostic of framework or Storybook version and you may contribute with your own templates. Existing templates can be used by providing -t, --template
argument and they include a hermione
preset, puppeteer
preset or a playwright
one with respective argument names.
You could also create a config file named storytests.config.js
yourself, names like storytestsrc.cjs
or storytests.conf.js
would also work. Read about configuration in detail.
When configured can be run with:
npm run storytests
# or
yarn storytests
Config file in the project root will be hooked up automatically. If you are using a different location or name for your config file, pass relative path to it with -c, --config
argument.
yarn storytests -c ./.config/storytests.config.js
By default, if an existing test file is found, it will not be rewritten. If you want to rewrite existing test files, pass -r, --rewrite
flag.
You can also display a help message with --help
.
Configuration
storytests-cli
can be configured with the following properties:
strategy: 'component' | 'story';
When set to
'component'
a separate test file will be created for every matched file. When set to'story'
a separate test file will be created for every matched story in a file.testDirectoy: ((component: string, path: string) => string) | string;
Path to the folder where test files will be created relative to the matched file folder. Can be either a function or a string. Relative paths are supported: in this case they will be resolve against matched file directory.
postfixes: string[];
Postfixes for generated test files. For example, to create
hermione
and other generic test files you can specify['hermione', 'test']
as the value.filesGlob: string;
Absolute path glob pattern to match desired story files.
componentPattern: RegExp;
RegExp to match the component name in a Storybook file.
storyPattern: RegExp;
RegExp to match the story names in a Storybook file.
generateTest: ( component: string, story: string | string[], postfix: string, ) => string | false;
A function that gets called for every file with every possible combination of stories/postfixes and should return test file content. Recieves matched component name (the result of the match from
componentPattern
), stories matched fromstoryPattern
in the file or a single story name (ifstrategy
is set to'story'
), as well as the postfix frompostfixes
. This function could also returnfalse
(not any other falsy value though), then no test file for this combination of arguments will be created.generateFileName: ( component: string, story: string | string[], postfix: string, ) => string;
A function that gets called before
generateTest
and should return the file name. Has identical signature togenerateTest
except it should not returnfalse
.validateFileName: (path: string, component: string, stories: string[]) => boolean;
A function that gets called for every unvalidated file when running
cleanup
command.path
parameter stores relative path from test directory (calculated usingtestDirectory
).component
andstories
parameters store matched component names and all matched stories (matches fromcomponentPattern
andstoryPattern
). Should returntrue
if file is valid andfalse
if file shoudl get removed (e.g. a screenshot from a removed story).
Example
Let's imagine we have a simple Button component story:
// button.stories.tsx
// ...
export default {
title: "Components/Button",
component: Button,
} as Meta;
const Template: Story = ({ label, ...args }) => (
<Button {...args}>{label}</Button>
);
// @storytests-ignore
export const Playground = Template.bind({});
export const Primary = Template.bind({});
Primary.args = {
view: "primary",
};
// ...
We want to create hermione
and playwright
test files from this story. Take a look at a sufficient storytests.config.js
.
// storytests.config.js
const path = require('path');
const hermioneTemplate = require('./storytests/hermione.template');
const playwrightTemplate = require('./storytests/playwright.template');
module.exports = {
/**
* Should match `Components/Button`
* ```
* export default {
* title: "Components/Button",
* component: Button,
* } as Meta;
* ```
*/
componentPattern: /(?<=title: ")[a-z/]+/gi,
/**
* Should match `Primary`
* ```
* export const Primary = Template.bind({});
* ```
*
* Should not match `Playground`
* ```
* // @storytests-ignore
* export const Playground = Template.bind({});
* ```
*/
storyPattern: /(?<!\/\/ @storytests-ignore[ \r\n]export const )\b[a-z]+(?= = Template.bind\()/gi,
/**
* Generate a single test file for a single component, not for every story
*/
strategy: 'component',
/**
* Generate test files in the same directory as stories file
*/
testDirectory: './',
/**
* Generate `hermione` and `playwright` (though we can use any names here, they get passed to our hooks)
*/
postfixes: ['hermione', 'playwright'],
/**
* Glob pattern to match story files
*/
filesGlob: path.resolve(__dirname, './src/**/*.stories.tsx'),
/**
* A hook function to generate test file contents
* @param {string} componentPath component name (match from `componentPattern`)
* @param {string[]} stories story names as an array (matches from `storyPattern`, could be empty)
* @param {string} postfix test file postfix
* @returns {string|false} could return false then this file will not be generated
*/
generateTest: (componentPath, stories, postfix) => {
switch (postfix) {
case 'hermione':
return hermioneTemplate(componentPath, stories);
case 'playwright':
return playwrightTemplate(componentPath, stories);
default:
return false;
}
},
/**
* A hook function to generate file name
*/
generateFileName: (componentPath, _stories, postfix) => {
const componentParts = componentPath.split('/');
const component = componentParts[
componentParts.length - 1
].toLowerCase();
const isPlaywright = postfix === 'playwright';
const type = isPlaywright ? 'spec' : postfix;
const extention = isPlaywright ? 'ts' : 'js';
// Even though we specified `playwright` as a postfix in the config we are free to use any names we want
return `${component}.${type}.${extention}`;
},
};
Now when we run yarn storytests
in the project we should see button.hermione.js
and button.spec.ts
generated in the same folder as button.stories.tsx
according to imported template functions which could look like this:
/**
* Generates a hermione test file from template
* @param {string} componentPath component name
* @param {string[]} stories story names as an array
*/
const hermioneTemplate = (componentPath, stories) => {
if (stories.length === 0) {
return false;
}
const kebabCaseComponent = componentPath.toLowerCase().replace(/\//g, '-');
const componentParts = componentPath.split('/');
const component = componentParts[componentParts.length - 1];
const kebabCaseStories = stories.map((story) =>
story.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(),
);
const storyNames = stories.map((story) =>
story.replace(/([a-z])([A-Z])/g, '$1 $2'),
);
return `describe("${component}", function () {
const selector = ".story";
${kebabCaseStories
.map(
(story, index) => `
it("${storyNames[index]}", function () {
return this.browser
.url("iframe.html?id=${kebabCaseComponent}--${story}")
.assertView("${story}", selector);
});`,
)
.join('\n')}
});
`;
};
module.exports = hermioneTemplate;
Resulting button.hermione.js
could look something like this:
describe('Button', function () {
const selector = '.story';
it('Primary', function () {
return this.browser
.url('iframe.html?id=components-button--primary')
.assertView('primary', selector);
});
// ...
});
You can check out the repository with this example more in depth at storytests-cli-example
Acknowledgements
Inspired by Storytests Webpack Plugin by baushonok
License
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago