Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/yandex/storytests-cli

Framework agnostic CLI Utility to generate test files from Storybook
https://github.com/yandex/storytests-cli

storybook testing

Last synced: 3 months ago
JSON representation

Framework agnostic CLI Utility to generate test files from Storybook

Awesome Lists containing this project

README

        

# Storytests CLI

Framework agnostic CLI Utility to generate test files from matched [Storybook](https://storybook.js.org/) files.

## Table of Contents

- [Storytests CLI](#storytests-cli)
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [Example](#example)
- [Acknowledgements](#acknowledgements)
- [License](#license)

## Installation

You can install `storytests-cli` using npm or yarn:

```bash
npm i storytests-cli --save-dev
# or
yarn add -D storytests-cli
```

## Usage

Prerequisites: [Node.js](https://nodejs.org/en/) (`>10.4.0`).

Initialize a basic configuration by running `storytests-cli` with:

```bash
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`](https://github.com/gemini-testing/hermione) preset, [`puppeteer`](https://github.com/puppeteer/puppeteer) preset or a [`playwright`](https://playwright.dev) 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](#configuration) in detail.

When configured can be run with:

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

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

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

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

- ```ts
postfixes: string[];
```

Postfixes for generated test files. For example, to create [`hermione`](https://github.com/gemini-testing/hermione) and other generic test files you can specify `['hermione', 'test']` as the value.

- ```ts
filesGlob: string;
```

Absolute path glob pattern to match desired story files.

- ```ts
componentPattern: RegExp;
```

RegExp to match the component name in a Storybook file.

- ```ts
storyPattern: RegExp;
```

RegExp to match the story names in a Storybook file.

- ```ts
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 from `storyPattern` in the file or a single story name (if `strategy` is set to `'story'`), as well as the postfix from `postfixes`. This function could also return `false` (not any other falsy value though), then no test file for this combination of arguments will be created.

- ```ts
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 to `generateTest` except it should not return `false`.

- ```ts
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 using `testDirectory`). `component` and `stories` parameters store matched component names and all matched stories (matches from `componentPattern` and `storyPattern`). Should return `true` if file is valid and `false` if file shoudl get removed (e.g. a screenshot from a removed story).

## Example

Let's imagine we have a simple Button component story:

```jsx
// button.stories.tsx

// ...

export default {
title: "Components/Button",
component: Button,
} as Meta;

const Template: Story = ({ label, ...args }) => (
{label}
);

// @storytests-ignore
export const Playground = Template.bind({});

export const Primary = Template.bind({});
Primary.args = {
view: "primary",
};

// ...

```

We want to create [`hermione`](https://github.com/gemini-testing/hermione) and [`playwright`](https://playwright.dev) test files from this story. Take a look at a sufficient `storytests.config.js`.

````javascript
// 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: /(? {
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:

```javascript
/**
* 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:

```javascript
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`](https://github.com/yakovlev-alexey/storytests-cli-example)

## Acknowledgements

Inspired by [Storytests Webpack Plugin](https://github.com/yandex/storytests-webpack-plugin) by [baushonok](https://github.com/baushonok)

## License

[MPL-2.0](/LICENSE)