2.0.1 • Published 2 years ago

storytests-cli v2.0.1

Weekly downloads
-
License
MPL-2.0
Repository
github
Last release
2 years ago

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

  • 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.

  • 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:

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

MPL-2.0

2.0.1

2 years ago

2.0.0

2 years ago

1.5.0

3 years ago

1.4.0

3 years ago

1.3.1

3 years ago

1.3.0

3 years ago

1.2.2

3 years ago

1.2.1

3 years ago

1.2.0

3 years ago

1.1.0

3 years ago

1.0.2

3 years ago

1.0.1

3 years ago

1.0.0

3 years ago

0.0.1

3 years ago