Skip to content

Commit

Permalink
add support for test filtering via tags
Browse files Browse the repository at this point in the history
  • Loading branch information
yannbf committed Nov 10, 2023
1 parent 33f2385 commit 223f137
Show file tree
Hide file tree
Showing 16 changed files with 1,682 additions and 561 deletions.
5 changes: 5 additions & 0 deletions .storybook/test-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ const customSnapshotsDir = `${process.cwd()}/${snapshotsDir}`;
const skipSnapshots = process.env.SKIP_SNAPSHOTS === 'true';

const config: TestRunnerConfig = {
tags: {
exclude: ['exclude'],
include: [],
skip: ['skip'],
},
setup() {
expect.extend({ toMatchImageSnapshot });
},
Expand Down
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Storybook test runner turns all of your stories into executable tests.
- [Ejecting configuration](#ejecting-configuration)
- [Jest-playwright options](#jest-playwright-options)
- [Jest options](#jest-options)
- [Filtering tests (experimental)](#filtering-tests-experimental)
- [Test reporters](#test-reporters)
- [Running against a deployed Storybook](#running-against-a-deployed-storybook)
- [Index.json mode](#indexjson-mode)
Expand All @@ -32,6 +33,7 @@ Storybook test runner turns all of your stories into executable tests.
- [Render lifecycle](#render-lifecycle)
- [prepare](#prepare)
- [getHttpHeaders](#gethttpheaders)
- [tags](#tags)
- [Utility functions](#utility-functions)
- [getStoryContext](#getstorycontext)
- [waitForPageReady](#waitforpageready)
Expand Down Expand Up @@ -159,6 +161,9 @@ Usage: test-storybook [options]
| `--ci` | Instead of the regular behavior of storing a new snapshot automatically, it will fail the test and require Jest to be run with `--updateSnapshot`. <br/>`test-storybook --ci` |
| `--shard [shardIndex/shardCount]` | Splits your test suite across different machines to run in CI. <br/>`test-storybook --shard=1/3` |
| `--failOnConsole` | Makes tests fail on browser console errors<br/>`test-storybook --failOnConsole` |
| `--includeTags` | Only test stories that match the specified tags, comma separated<br/>`test-storybook --includeTags="test-only"` |
| `--excludeTags` | Do not test stories that match the specified tags, comma separated<br/>`test-storybook --excludeTags="broken-story,todo"` |
| `--skipTags` | Skip test stories that match the specified tags, comma separated<br/>`test-storybook --skipTags="design"` |

## Ejecting configuration

Expand All @@ -181,7 +186,7 @@ The Storybook test runner comes with Jest installed as an internal dependency. Y
| ------------------- | ------------------ |
| ^0.6.2 | ^26.6.3 or ^27.0.0 |
| ^0.7.0 | ^28.0.0 |
| ^0.14.0-next.2 | ^29.0.0 |
| ^0.14.0 | ^29.0.0 |

> If you're already using a compatible version of Jest, the test runner will use it, instead of installing a duplicate version in your node_modules folder.
Expand All @@ -206,6 +211,33 @@ module.exports = {
};
```

## Filtering tests (experimental)

You might want to skip certain stories in the test-runner, run tests only against a subset of stories, or exclude certain stories entirely from your tests. This is possible via the `tags` annotation.

This annotation can be part of a story, therefore only applying to it, or the component meta (the default export), which applies to all stories in the file:

```ts
const meta = {
component: Button,
tags: ['design', 'test-only'],
};
export default meta;

// will inherit tags from meta with value ['design', 'test-only']
export const Primary = {};

export const Secondary = {
// will override tags to be just ['skip']
tags: ['skip'],
};
```

> **Note**
> You can't import constants from another file and use them to define tags in your stories. The tags in your stories or meta **have to be ** defined inline, as an array of strings. This is a limitation due to Storybook's static analysis.
Once your stories have your own custom tags, you can filter them via the [tags property](#tags) in your test-runner configuration file. You can also use the CLI flags `--includeTags`, `--excludeTags` or `--skipTags` for the same purpose. The CLI flags will take precedence over the tags in the test-runner config, therefore overriding them.

## Test reporters

The test runner uses default Jest reporters, but you can add additional reporters by ejecting the configuration as explained above and overriding (or merging with) the `reporters` property.
Expand Down Expand Up @@ -622,6 +654,23 @@ module.exports = {
};
```

#### tags

The `tags` property contains three options: `include | exclude | skip`, each accepting an array of strings:

```js
// .storybook/test-runner.js
module.exports = {
tags: {
include: [], // string array, e.g. ['test-only']
exclude: [], // string array, e.g. ['design', 'docs-only']
skip: [], // string array, e.g. ['design']
},
};
```

`tags` are used for filtering your tests. Learn more [here](#filtering-tests-experimental).

### Utility functions

For more specific use cases, the test runner provides utility functions that could be useful to you.
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@
"@babel/preset-typescript": "^7.18.6",
"@jest/types": "^29.6.3",
"@storybook/addon-coverage": "^0.0.9",
"@storybook/addon-essentials": "^7.3.0",
"@storybook/addon-interactions": "^7.3.0",
"@storybook/addon-essentials": "^7.5.3",
"@storybook/addon-interactions": "^7.5.3",
"@storybook/jest": "^0.2.2",
"@storybook/react": "^7.3.0",
"@storybook/react-vite": "^7.3.0",
"@storybook/react": "^7.5.3",
"@storybook/react-vite": "^7.5.3",
"@storybook/testing-library": "^0.2.0",
"@types/jest": "^29.0.0",
"@types/node": "^16.4.1",
Expand All @@ -78,7 +78,7 @@
"react-dom": "^17.0.1",
"rimraf": "^3.0.2",
"semver": "^7.5.4",
"storybook": "^7.3.0",
"storybook": "^7.5.3",
"ts-jest": "^29.0.0",
"tsup": "^6.5.0",
"typescript": "~4.9.4",
Expand Down
71 changes: 60 additions & 11 deletions src/csf/transformCsf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as t from '@babel/types';
import generate from '@babel/generator';
import { toId, storyNameFromExport } from '@storybook/csf';
import dedent from 'ts-dedent';
import { getTagOptions } from '../util/getTagOptions';

export interface TestContext {
storyExport?: t.Identifier;
Expand All @@ -21,6 +22,9 @@ interface TransformOptions {
testPrefixer?: TestPrefixer;
insertTestIfEmpty?: boolean;
makeTitle?: (userTitle: string) => string;
includeTags?: string[];
excludeTags?: string[];
skipTags?: string[];
}

const prefixFunction = (
Expand All @@ -42,15 +46,22 @@ const prefixFunction = (
return stmt.expression;
};

const makePlayTest = (
key: string,
title: string,
metaOrStoryPlay: t.Node,
testPrefix?: TestPrefixer
): t.Statement[] => {
const makePlayTest = ({
key,
metaOrStoryPlay,
title,
testPrefix,
shouldSkip,
}: {
key: string;
title: string;
metaOrStoryPlay: t.Node;
testPrefix?: TestPrefixer;
shouldSkip?: boolean;
}): t.Statement[] => {
return [
t.expressionStatement(
t.callExpression(t.identifier('it'), [
t.callExpression(shouldSkip ? t.identifier('it.skip') : t.identifier('it'), [
t.stringLiteral(!!metaOrStoryPlay ? 'play-test' : 'smoke-test'),
prefixFunction(key, title, metaOrStoryPlay as t.Expression, testPrefix),
])
Expand Down Expand Up @@ -81,6 +92,18 @@ const makeBeforeEach = (beforeEachPrefixer: FilePrefixer) => {
const makeArray = (templateResult: TemplateResult) =>
Array.isArray(templateResult) ? templateResult : [templateResult];

// copied from csf-tools, as it's not exported
function parseTags(prop: t.Node) {
if (!t.isArrayExpression(prop)) {
throw new Error('CSF: Expected tags array');
}

return prop.elements.map((e) => {
if (t.isStringLiteral(e)) return e.value;
throw new Error(`CSF: Expected tag to be string literal`);
}) as string[];
}

export const transformCsf = (
code: string,
{
Expand All @@ -91,23 +114,49 @@ export const transformCsf = (
makeTitle,
}: TransformOptions = {}
) => {
const { includeTags, excludeTags, skipTags } = getTagOptions();

const csf = loadCsf(code, { makeTitle });
csf.parse();

const storyExports = Object.keys(csf._stories);
const title = csf.meta.title;

const storyPlays = storyExports.reduce((acc, key) => {
const storyAnnotations = storyExports.reduce((acc, key) => {
const annotations = csf._storyAnnotations[key];
acc[key] = {};
if (annotations?.play) {
acc[key] = annotations.play;
acc[key].play = annotations.play;
}
acc[key].tags = annotations.tags ? parseTags(annotations.tags) : csf.meta.tags || [];
return acc;
}, {} as Record<string, t.Node>);
}, {} as Record<string, { play?: t.Node; tags?: string[] }>);

const playTests = storyExports
.filter((key) => {
// If includeTags is passed, check if the story has any of them - else include by default
const isIncluded =
includeTags.length === 0 ||
includeTags.some((tag) => storyAnnotations[key].tags.includes(tag));

// If excludeTags is passed, check if the story does not have any of them
const isNotExcluded = excludeTags.every((tag) => !storyAnnotations[key].tags.includes(tag));

return isIncluded && isNotExcluded;
})
.map((key: string) => {
let tests: t.Statement[] = [];
tests = [...tests, ...makePlayTest(key, title, storyPlays[key], testPrefixer)];
const shouldSkip = skipTags.some((tag) => storyAnnotations[key].tags.includes(tag));
tests = [
...tests,
...makePlayTest({
key,
title,
metaOrStoryPlay: storyAnnotations[key].play,
testPrefix: testPrefixer,
shouldSkip,
}),
];

if (tests.length) {
return makeDescribe(key, tests);
Expand Down
8 changes: 8 additions & 0 deletions src/playwright/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ export interface TestRunnerConfig {
* If you override the default prepare behavior, even though this is powerful, you will be responsible for properly preparing the browser. Future changes to the default prepare function will not get included in your project, so you will have to keep an eye out for changes in upcoming releases.
*/
prepare?: PrepareSetter;
/**
* Tags to include, exclude, or skip. These tags are defined as annotations in your story or meta.
*/
tags: {
include: string[];
exclude: string[];
skip: string[];
};
}

export const setPreRender = (preRender: TestHook) => {
Expand Down
2 changes: 1 addition & 1 deletion src/playwright/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { transformSync as swcTransform } from '@swc/core';
import { transformPlaywright } from './transformPlaywright';

export const process = (src: string, filename: string, config: any) => {
export const process = (src: string, filename: string) => {
const csfTest = transformPlaywright(src, filename);

const result = swcTransform(csfTest, {
Expand Down
Loading

0 comments on commit 223f137

Please sign in to comment.