From 9b77c29202573a081290976537fb72c3aec9575b Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 3 Feb 2022 15:43:07 +0100 Subject: [PATCH] feat: add CLI wrapper --- README.md | 17 ++++++ bin/test-storybook.js | 12 +++-- package.json | 3 +- src/playwright/transformPlaywright.test.ts | 4 +- src/playwright/transformPlaywright.ts | 15 ++---- src/util/cli.test.ts | 44 +++++++++++++++ src/util/cli.ts | 63 ++++++++++++++++++++++ src/util/helpers.ts | 41 ++++++++++++++ yarn.lock | 5 ++ 9 files changed, 189 insertions(+), 15 deletions(-) create mode 100644 src/util/cli.test.ts create mode 100644 src/util/cli.ts create mode 100644 src/util/helpers.ts diff --git a/README.md b/README.md index 9ec64030..0df996b5 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,23 @@ yarn test-storybook > TARGET_URL=http://localhost:9009 yarn test-storybook > ``` +## CLI Options + +```plaintext +Usage: test-storybook [options] +``` + +| Options | Description | +| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--help` | Output usage information
`test-storybook --help` | +| `-s`, `--stories-json` | Run in stories json mode (requires a compatible Storybook)
`test-storybook --stories-json` | +| `-c`, `--config-dir [dir-name]` | Directory where to load Storybook configurations from
`test-storybook -c .storybook` | +| `--watch` | Run in watch mode
`test-storybook --watch` | +| `--maxWorkers [amount]` | Specifies the maximum number of workers the worker-pool will spawn for running tests
`test-storybook --maxWorkers=2` | +| `--no-cache` | Disable the cache
`test-storybook --no-cache` | +| `--clearCache` | Deletes the Jest cache directory and then exits without running tests
`test-storybook --clearCache` | +| `--verbose` | Display individual test results with the test suite hierarchy
`test-storybook --verbose` | + ## Configuration The test runner is based on [Jest](https://jestjs.io/) and will accept the [CLI options](https://jestjs.io/docs/cli) that Jest does, like `--watch`, `--maxWorkers`, etc. diff --git a/bin/test-storybook.js b/bin/test-storybook.js index e0e4b0ec..7ae8f9cc 100755 --- a/bin/test-storybook.js +++ b/bin/test-storybook.js @@ -6,6 +6,7 @@ const fetch = require('node-fetch'); const fs = require('fs'); const path = require('path'); const tempy = require('tempy'); +const { getCliOptions, getStorybookMain } = require('../dist/cjs/util/cli'); const { transformPlaywrightJson } = require('../dist/cjs/playwright/transformPlaywrightJson'); // Do this as the first thing so that any code reading it knows the right env. @@ -103,15 +104,20 @@ async function fetchStoriesJson(url) { const main = async () => { const targetURL = sanitizeURL(process.env.TARGET_URL || `http://localhost:6006`); await checkStorybook(targetURL); - let args = process.argv.filter((arg) => arg !== '--stories-json'); - if (args.length !== process.argv.length) { + const { jestOptions, runnerOptions } = getCliOptions() + + if (runnerOptions.storiesJson) { storiesJsonTmpDir = await fetchStoriesJson(targetURL); process.env.TEST_ROOT = storiesJsonTmpDir; process.env.TEST_MATCH = '**/*.test.js'; } - await executeJestPlaywright(args); + // check if main.js exists, throw an error if not + getStorybookMain(runnerOptions.configDir); + process.env.STORYBOOK_CONFIG_DIR = runnerOptions.configDir; + + await executeJestPlaywright(jestOptions); }; main().catch((e) => console.log(`[test-storybook] ${e}`)); diff --git a/package.json b/package.json index 738c9557..5ca20aa3 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "buildTsc": "tsc --declaration --emitDeclarationOnly --outDir ./dist/ts", "prebuild": "yarn clean", "build": "concurrently \"yarn buildBabel\" \"yarn buildTsc\"", - "build:watch": "concurrently \"yarn buildBabel:esm -- --watch\" \"yarn buildTsc -- --watch\"", + "build:watch": "concurrently \"yarn buildBabel:cjs -- --watch\" \"yarn buildTsc -- --watch\"", "test": "jest", "storybook": "start-storybook -p 6006", "start": "concurrently \"yarn build:watch\" \"yarn storybook -- --no-manager-cache --quiet\"", @@ -106,6 +106,7 @@ "dependencies": { "@storybook/csf": "0.0.2--canary.87bc651.0", "@storybook/csf-tools": "^6.4.14", + "commander": "^9.0.0", "jest-playwright-preset": "^1.7.0", "node-fetch": "^2", "playwright": "^1.14.0", diff --git a/src/playwright/transformPlaywright.test.ts b/src/playwright/transformPlaywright.test.ts index 91d68e95..c93e791a 100644 --- a/src/playwright/transformPlaywright.test.ts +++ b/src/playwright/transformPlaywright.test.ts @@ -1,10 +1,12 @@ import dedent from 'ts-dedent'; import path from 'path'; import * as coreCommon from '@storybook/core-common'; +import * as cli from '../util/cli'; import { transformPlaywright } from './transformPlaywright'; jest.mock('@storybook/core-common'); +jest.mock('../util/cli'); expect.addSnapshotSerializer({ print: (val: any) => val.trim(), @@ -15,7 +17,7 @@ describe('Playwright', () => { beforeEach(() => { const relativeSpy = jest.spyOn(path, 'relative'); relativeSpy.mockReturnValueOnce('stories/basic/Header.stories.js'); - jest.spyOn(coreCommon, 'serverRequire').mockImplementation(() => ({ + jest.spyOn(cli, 'getStorybookMain').mockImplementation(() => ({ stories: [ { directory: '../stories/basic', diff --git a/src/playwright/transformPlaywright.ts b/src/playwright/transformPlaywright.ts index 06e52362..fdf46c8d 100644 --- a/src/playwright/transformPlaywright.ts +++ b/src/playwright/transformPlaywright.ts @@ -1,8 +1,9 @@ -import { resolve, join, relative } from 'path'; +import { resolve, relative } from 'path'; import template from '@babel/template'; -import { serverRequire, normalizeStories } from '@storybook/core-common'; +import { normalizeStories } from '@storybook/core-common'; import { autoTitle } from '@storybook/store'; +import { getStorybookMain } from '../util/cli'; import { transformCsf } from '../csf/transformCsf'; export const testPrefixer = template( @@ -24,16 +25,10 @@ export const testPrefixer = template( ); const getDefaultTitle = (filename: string) => { - // we'll need to figure this out for different cases - // e.g. --config-dir - const configDir = resolve('.storybook'); const workingDir = resolve(); + const configDir = process.env.STORYBOOK_CONFIG_DIR; - const main = serverRequire(join(configDir, 'main')); - - if (!main) { - throw new Error(`Could not load main.js in ${configDir}`); - } + const main = getStorybookMain(configDir); const normalizedStoriesEntries = normalizeStories(main.stories, { configDir, diff --git a/src/util/cli.test.ts b/src/util/cli.test.ts new file mode 100644 index 00000000..ab03ed60 --- /dev/null +++ b/src/util/cli.test.ts @@ -0,0 +1,44 @@ +import * as coreCommon from '@storybook/core-common'; + +import * as cliHelper from './helpers'; +import { getCliOptions, getStorybookMain, defaultRunnerOptions } from './cli'; + +jest.mock('@storybook/core-common'); + +describe('CLI', () => { + describe('getCliOptions', () => { + it('returns default options if no extra option is passed', () => { + const opts = getCliOptions(); + expect(opts.runnerOptions).toMatchObject(defaultRunnerOptions); + }); + + it('returns custom options if passed', () => { + const customConfig = { configDir: 'custom', storiesJson: true }; + jest.spyOn(cliHelper, 'getParsedCliOptions').mockReturnValue(customConfig); + const opts = getCliOptions(); + expect(opts.runnerOptions).toMatchObject(customConfig); + }); + }); + + describe('getStorybookMain', () => { + it('should throw an error if no configuration is found', () => { + expect(() => getStorybookMain('.storybook')).toThrow(); + }); + + it('should return mainjs', () => { + const mockedMain = { + stories: [ + { + directory: '../stories/basic', + titlePrefix: 'Example', + }, + ], + }; + + jest.spyOn(coreCommon, 'serverRequire').mockImplementation(() => mockedMain); + + const res = getStorybookMain('.storybook'); + expect(res).toMatchObject(mockedMain); + }); + }); +}); diff --git a/src/util/cli.ts b/src/util/cli.ts new file mode 100644 index 00000000..f8a97851 --- /dev/null +++ b/src/util/cli.ts @@ -0,0 +1,63 @@ +import { join, resolve } from 'path'; +import { serverRequire, StorybookConfig } from '@storybook/core-common'; +import { getParsedCliOptions } from './helpers'; + +type CliOptions = { + runnerOptions: { + storiesJson: boolean; + configDir: string; + }; + jestOptions: string[]; +}; + +type StorybookRunnerCommand = keyof CliOptions['runnerOptions']; + +const STORYBOOK_RUNNER_COMMANDS: StorybookRunnerCommand[] = ['storiesJson', 'configDir']; + +export const defaultRunnerOptions: CliOptions['runnerOptions'] = { + configDir: '.storybook', + storiesJson: false, +}; + +let storybookMainConfig: StorybookConfig; + +export const getCliOptions = () => { + const allOptions = getParsedCliOptions(); + + const defaultOptions: CliOptions = { + runnerOptions: { ...defaultRunnerOptions }, + jestOptions: process.argv.splice(0, 2), + }; + + return Object.keys(allOptions).reduce((acc, key: any) => { + if (STORYBOOK_RUNNER_COMMANDS.includes(key)) { + //@ts-ignore + acc.runnerOptions[key] = allOptions[key]; + } else { + if (allOptions[key] === true) { + acc.jestOptions.push(`--${key}`); + } else if (allOptions[key] === false) { + acc.jestOptions.push(`--no-${key}`); + } else { + acc.jestOptions.push(`--${key}`, allOptions[key]); + } + } + + return acc; + }, defaultOptions); +}; + +export const getStorybookMain = (configDir: string) => { + if (storybookMainConfig) { + return storybookMainConfig; + } + + storybookMainConfig = serverRequire(join(resolve(configDir), 'main')); + if (!storybookMainConfig) { + throw new Error( + `Could not load main.js in ${configDir}. Is the config directory correct? You can change it by using --config-dir ` + ); + } + + return storybookMainConfig; +}; diff --git a/src/util/helpers.ts b/src/util/helpers.ts new file mode 100644 index 00000000..5c2398bd --- /dev/null +++ b/src/util/helpers.ts @@ -0,0 +1,41 @@ +export const getParsedCliOptions = () => { + const { program } = require('commander'); + + program + .option('-s, --stories-json', 'Run in stories json mode (requires a compatible Storybook)') + .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') + .option('--watch', 'Run in watch mode') + .option( + '--maxWorkers ', + 'Specifies the maximum number of workers the worker-pool will spawn for running tests' + ) + .option('--no-cache', 'Disable the cache') + .option('--clearCache', 'Deletes the Jest cache directory and then exits without running tests') + .option('--verbose', 'Display individual test results with the test suite hierarchy'); + + program.exitOverride(); + + try { + program.parse(); + } catch (err) { + switch (err.code) { + case 'commander.unknownOption': { + program.outputHelp(); + console.warn( + `\nIf you'd like this option to be supported, please open an issue at https://github.com/storybookjs/test-runner/issues/new\n` + ); + process.exit(1); + } + + case 'commander.helpDisplayed': { + process.exit(0); + } + + default: { + throw err; + } + } + } + + return program.opts(); +}; diff --git a/yarn.lock b/yarn.lock index 3dd214f7..e1d74d8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5124,6 +5124,11 @@ commander@^8.2.0: resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== +commander@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.0.0.tgz#86d58f24ee98126568936bd1d3574e0308a99a40" + integrity sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw== + common-path-prefix@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0"