{"id":18682887,"url":"https://github.com/yandex/storytests-cli","last_synced_at":"2025-08-24T13:34:23.220Z","repository":{"id":44923041,"uuid":"372505318","full_name":"yandex/storytests-cli","owner":"yandex","description":"Framework agnostic CLI Utility to generate test files from Storybook","archived":false,"fork":false,"pushed_at":"2022-01-18T09:20:31.000Z","size":384,"stargazers_count":10,"open_issues_count":0,"forks_count":2,"subscribers_count":10,"default_branch":"main","last_synced_at":"2025-03-26T00:01:51.516Z","etag":null,"topics":["storybook","testing"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/storytests-cli","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yandex.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-05-31T12:50:54.000Z","updated_at":"2025-02-28T09:26:59.000Z","dependencies_parsed_at":"2022-09-19T08:31:23.033Z","dependency_job_id":null,"html_url":"https://github.com/yandex/storytests-cli","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yandex%2Fstorytests-cli","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yandex%2Fstorytests-cli/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yandex%2Fstorytests-cli/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yandex%2Fstorytests-cli/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yandex","download_url":"https://codeload.github.com/yandex/storytests-cli/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248517042,"owners_count":21117369,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["storybook","testing"],"created_at":"2024-11-07T10:13:06.483Z","updated_at":"2025-04-12T04:30:54.620Z","avatar_url":"https://github.com/yandex.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Storytests CLI\n\nFramework agnostic CLI Utility to generate test files from matched [Storybook](https://storybook.js.org/) files.\n\n## Table of Contents\n\n-   [Storytests CLI](#storytests-cli)\n-   [Table of Contents](#table-of-contents)\n-   [Installation](#installation)\n-   [Usage](#usage)\n-   [Configuration](#configuration)\n-   [Example](#example)\n-   [Acknowledgements](#acknowledgements)\n-   [License](#license)\n\n## Installation\n\nYou can install `storytests-cli` using npm or yarn:\n\n```bash\nnpm i storytests-cli --save-dev\n# or\nyarn add -D storytests-cli\n```\n\n## Usage\n\nPrerequisites: [Node.js](https://nodejs.org/en/) (`\u003e10.4.0`).\n\nInitialize a basic configuration by running `storytests-cli` with:\n\n```bash\nnpm run storytests init\n# or\nyarn storytests init\n```\n\nCurrently 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.\n\nYou 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.\n\nWhen configured can be run with:\n\n```bash\nnpm run storytests\n# or\nyarn storytests\n```\n\nConfig 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.\n\n```bash\nyarn storytests -c ./.config/storytests.config.js\n```\n\nBy 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.\n\nYou can also display a help message with `--help`.\n\n## Configuration\n\n`storytests-cli` can be configured with the following properties:\n\n-   ```ts\n    strategy: 'component' | 'story';\n    ```\n\n    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.\n\n-   ```ts\n    testDirectoy: ((component: string, path: string) =\u003e string) | string;\n    ```\n\n    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.\n\n-   ```ts\n    postfixes: string[];\n    ```\n\n    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.\n\n-   ```ts\n    filesGlob: string;\n    ```\n\n    Absolute path glob pattern to match desired story files.\n\n-   ```ts\n    componentPattern: RegExp;\n    ```\n\n    RegExp to match the component name in a Storybook file.\n\n-   ```ts\n    storyPattern: RegExp;\n    ```\n\n    RegExp to match the story names in a Storybook file.\n\n-   ```ts\n    generateTest: (\n        component: string,\n        story: string | string[],\n        postfix: string,\n    ) =\u003e string | false;\n    ```\n\n    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.\n\n-   ```ts\n    generateFileName: (\n        component: string,\n        story: string | string[],\n        postfix: string,\n    ) =\u003e string;\n    ```\n\n    A function that gets called before `generateTest` and should return the file name. Has identical signature to `generateTest` except it should not return `false`.\n\n-   ```ts\n    validateFileName: (path: string, component: string, stories: string[]) =\u003e\n        boolean;\n    ```\n\n    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).\n\n## Example\n\nLet's imagine we have a simple Button component story:\n\n```jsx\n// button.stories.tsx\n\n// ...\n\nexport default {\n  title: \"Components/Button\",\n  component: Button,\n} as Meta;\n\nconst Template: Story = ({ label, ...args }) =\u003e (\n  \u003cButton {...args}\u003e{label}\u003c/Button\u003e\n);\n\n// @storytests-ignore\nexport const Playground = Template.bind({});\n\nexport const Primary = Template.bind({});\nPrimary.args = {\n  view: \"primary\",\n};\n\n// ...\n\n```\n\nWe 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`.\n\n````javascript\n// storytests.config.js\n\nconst path = require('path');\n\nconst hermioneTemplate = require('./storytests/hermione.template');\nconst playwrightTemplate = require('./storytests/playwright.template');\n\nmodule.exports = {\n    /**\n     * Should match `Components/Button`\n     * ```\n     * export default {\n     *   title: \"Components/Button\",\n     *   component: Button,\n     * } as Meta;\n     * ```\n     */\n    componentPattern: /(?\u003c=title: \")[a-z/]+/gi,\n\n    /**\n     * Should match `Primary`\n     * ```\n     * export const Primary = Template.bind({});\n     * ```\n     *\n     * Should not match `Playground`\n     * ```\n     * // @storytests-ignore\n     * export const Playground = Template.bind({});\n     * ```\n     */\n    storyPattern: /(?\u003c!\\/\\/ @storytests-ignore[ \\r\\n]export const )\\b[a-z]+(?= = Template.bind\\()/gi,\n\n    /**\n     * Generate a single test file for a single component, not for every story\n     */\n    strategy: 'component',\n\n    /**\n     * Generate test files in the same directory as stories file\n     */\n    testDirectory: './',\n\n    /**\n     * Generate `hermione` and `playwright` (though we can use any names here, they get passed to our hooks)\n     */\n    postfixes: ['hermione', 'playwright'],\n\n    /**\n     * Glob pattern to match story files\n     */\n    filesGlob: path.resolve(__dirname, './src/**/*.stories.tsx'),\n\n    /**\n     * A hook function to generate test file contents\n     * @param {string} componentPath component name (match from `componentPattern`)\n     * @param {string[]} stories story names as an array (matches from `storyPattern`, could be empty)\n     * @param {string} postfix test file postfix\n     * @returns {string|false} could return false then this file will not be generated\n     */\n    generateTest: (componentPath, stories, postfix) =\u003e {\n        switch (postfix) {\n            case 'hermione':\n                return hermioneTemplate(componentPath, stories);\n            case 'playwright':\n                return playwrightTemplate(componentPath, stories);\n            default:\n                return false;\n        }\n    },\n\n    /**\n     * A hook function to generate file name\n     */\n    generateFileName: (componentPath, _stories, postfix) =\u003e {\n        const componentParts = componentPath.split('/');\n\n        const component = componentParts[\n            componentParts.length - 1\n        ].toLowerCase();\n\n        const isPlaywright = postfix === 'playwright';\n\n        const type = isPlaywright ? 'spec' : postfix;\n\n        const extention = isPlaywright ? 'ts' : 'js';\n\n        // Even though we specified `playwright` as a postfix in the config we are free to use any names we want\n        return `${component}.${type}.${extention}`;\n    },\n};\n````\n\nNow 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:\n\n```javascript\n/**\n * Generates a hermione test file from template\n * @param {string} componentPath component name\n * @param {string[]} stories story names as an array\n */\nconst hermioneTemplate = (componentPath, stories) =\u003e {\n    if (stories.length === 0) {\n        return false;\n    }\n\n    const kebabCaseComponent = componentPath.toLowerCase().replace(/\\//g, '-');\n    const componentParts = componentPath.split('/');\n    const component = componentParts[componentParts.length - 1];\n    const kebabCaseStories = stories.map((story) =\u003e\n        story.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(),\n    );\n    const storyNames = stories.map((story) =\u003e\n        story.replace(/([a-z])([A-Z])/g, '$1 $2'),\n    );\n\n    return `describe(\"${component}\", function () {\n  const selector = \".story\";\n        ${kebabCaseStories\n            .map(\n                (story, index) =\u003e `\n  it(\"${storyNames[index]}\", function () {\n    return this.browser\n      .url(\"iframe.html?id=${kebabCaseComponent}--${story}\")\n      .assertView(\"${story}\", selector);\n  });`,\n            )\n            .join('\\n')}\n});\n`;\n};\n\nmodule.exports = hermioneTemplate;\n```\n\nResulting `button.hermione.js` could look something like this:\n\n```javascript\ndescribe('Button', function () {\n    const selector = '.story';\n\n    it('Primary', function () {\n        return this.browser\n            .url('iframe.html?id=components-button--primary')\n            .assertView('primary', selector);\n    });\n\n    // ...\n});\n```\n\nYou can check out the repository with this example more in depth at [`storytests-cli-example`](https://github.com/yakovlev-alexey/storytests-cli-example)\n\n## Acknowledgements\n\nInspired by [Storytests Webpack Plugin](https://github.com/yandex/storytests-webpack-plugin) by [baushonok](https://github.com/baushonok)\n\n## License\n\n[MPL-2.0](/LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyandex%2Fstorytests-cli","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyandex%2Fstorytests-cli","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyandex%2Fstorytests-cli/lists"}