Skip to content

Commit

Permalink
Merge pull request #485 from storybookjs/yann/fix-tags-filtering
Browse files Browse the repository at this point in the history
Fix: Combine tags correctly when transforming story files
  • Loading branch information
yannbf authored Jun 21, 2024
2 parents fa76d30 + a54e616 commit 1076614
Show file tree
Hide file tree
Showing 9 changed files with 425 additions and 18 deletions.
8 changes: 7 additions & 1 deletion .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Preview } from '@storybook/react';
import { isTestRunner } from './is-test-runner';

const withSkippableTests = (StoryFn, { parameters }) => {
Expand All @@ -8,4 +9,9 @@ const withSkippableTests = (StoryFn, { parameters }) => {
return StoryFn();
};

export const decorators = [withSkippableTests];
const preview: Preview = {
tags: ['global-tag'],
decorators: [withSkippableTests],
};

export default preview;
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ 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.
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. By default, the test-runner includes every story with the `"test"` tag. This tag is included by default in Storybook 8 for all stories, unless the user tells otherwise via [tag negation](https://storybook.js.org/docs/writing-stories/tags#removing-tags).

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:

Expand Down
14 changes: 11 additions & 3 deletions src/csf/transformCsf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { loadCsf } from '@storybook/csf-tools';
import * as t from '@babel/types';
import generate from '@babel/generator';
import { toId, storyNameFromExport } from '@storybook/csf';
import { toId, storyNameFromExport, combineTags } from '@storybook/csf';
import dedent from 'ts-dedent';

import { getTagOptions } from '../util/getTagOptions';
Expand Down Expand Up @@ -108,7 +108,8 @@ export const transformCsf = (
beforeEachPrefixer,
insertTestIfEmpty,
makeTitle,
}: TransformOptions
previewAnnotations = { tags: [] },
}: TransformOptions & { previewAnnotations?: Record<string, any> }
) => {
const { includeTags, excludeTags, skipTags } = getTagOptions();

Expand All @@ -126,7 +127,14 @@ export const transformCsf = (
acc[key].play = annotations.play;
}

acc[key].tags = csf._stories[key].tags || csf.meta?.tags || [];
acc[key].tags = combineTags(
'test',
'dev',
...previewAnnotations.tags,
...(csf.meta?.tags || []),
...(csf._stories[key].tags || [])
);

return acc;
},
{}
Expand Down
280 changes: 277 additions & 3 deletions src/playwright/transformPlaywright.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('Playwright', () => {
delete process.env.STORYBOOK_INCLUDE_TAGS;
delete process.env.STORYBOOK_EXCLUDE_TAGS;
delete process.env.STORYBOOK_SKIP_TAGS;
delete process.env.STORYBOOK_PREVIEW_TAGS;
});

describe('tag filtering mechanism', () => {
Expand Down Expand Up @@ -324,22 +325,26 @@ describe('Playwright', () => {
`);
});
it('should work in conjunction with includeTags, excludeTags and skipTags', () => {
process.env.STORYBOOK_INCLUDE_TAGS = 'play,design';
process.env.STORYBOOK_INCLUDE_TAGS = 'play,design,global-tag';
process.env.STORYBOOK_SKIP_TAGS = 'skip';
process.env.STORYBOOK_EXCLUDE_TAGS = 'exclude';
process.env.STORYBOOK_PREVIEW_TAGS = 'global-tag';

// Should result in:
// - A being excluded
// - B being included, but skipped
// - C being included
// - D being excluded
// - D being included
// - E being excluded
expect(
transformPlaywright(
dedent`
export default { title: 'foo/bar', component: Button };
export const A = { tags: ['play', 'exclude'] };
export const B = { tags: ['play', 'skip'] };
export const C = { tags: ['design'] };
export const D = { };
export const D = { tags: ['global-tag'] };
export const E = { };
`,
filename
)
Expand Down Expand Up @@ -436,6 +441,275 @@ describe('Playwright', () => {
}
});
});
describe("D", () => {
it("smoke-test", async () => {
const testFn = async () => {
const context = {
id: "example-foo-bar--d",
title: "Example/foo/bar",
name: "D"
};
if (globalThis.__sbPreVisit) {
await globalThis.__sbPreVisit(page, context);
}
const result = await page.evaluate(({
id,
hasPlayFn
}) => __test(id, hasPlayFn), {
id: "example-foo-bar--d"
});
if (globalThis.__sbPostVisit) {
await globalThis.__sbPostVisit(page, context);
}
if (globalThis.__sbCollectCoverage) {
const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
if (!isCoverageSetupCorrectly) {
throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
The code in this story is not instrumented, which means the coverage setup is likely not correct.
More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
}
await jestPlaywright.saveCoverage(page);
}
return result;
};
try {
await testFn();
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"D"}". Retrying...\`);
await jestPlaywright.resetPage();
await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
await testFn();
} else {
throw err;
}
}
});
});
describe("E", () => {
it("smoke-test", async () => {
const testFn = async () => {
const context = {
id: "example-foo-bar--e",
title: "Example/foo/bar",
name: "E"
};
if (globalThis.__sbPreVisit) {
await globalThis.__sbPreVisit(page, context);
}
const result = await page.evaluate(({
id,
hasPlayFn
}) => __test(id, hasPlayFn), {
id: "example-foo-bar--e"
});
if (globalThis.__sbPostVisit) {
await globalThis.__sbPostVisit(page, context);
}
if (globalThis.__sbCollectCoverage) {
const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
if (!isCoverageSetupCorrectly) {
throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
The code in this story is not instrumented, which means the coverage setup is likely not correct.
More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
}
await jestPlaywright.saveCoverage(page);
}
return result;
};
try {
await testFn();
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"E"}". Retrying...\`);
await jestPlaywright.resetPage();
await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
await testFn();
} else {
throw err;
}
}
});
});
});
}
`);
});
it('should work with tag negation', () => {
process.env.STORYBOOK_INCLUDE_TAGS = 'play,test';
process.env.STORYBOOK_PREVIEW_TAGS = '!test';
// Should result in:
// - A being included
// - B being excluded because it has no play nor test tag (removed by negation in preview tags)
// - C being included because it has test tag (overwritten via story tags)
expect(
transformPlaywright(
dedent`
export default { title: 'foo/bar', component: Button, tags: ['play'] };
export const A = { };
export const B = { tags: ['!play'] };
export const C = { tags: ['!play', 'test'] };
`,
filename
)
).toMatchInlineSnapshot(`
if (!require.main) {
describe("Example/foo/bar", () => {
describe("A", () => {
it("smoke-test", async () => {
const testFn = async () => {
const context = {
id: "example-foo-bar--a",
title: "Example/foo/bar",
name: "A"
};
if (globalThis.__sbPreVisit) {
await globalThis.__sbPreVisit(page, context);
}
const result = await page.evaluate(({
id,
hasPlayFn
}) => __test(id, hasPlayFn), {
id: "example-foo-bar--a"
});
if (globalThis.__sbPostVisit) {
await globalThis.__sbPostVisit(page, context);
}
if (globalThis.__sbCollectCoverage) {
const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
if (!isCoverageSetupCorrectly) {
throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
The code in this story is not instrumented, which means the coverage setup is likely not correct.
More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
}
await jestPlaywright.saveCoverage(page);
}
return result;
};
try {
await testFn();
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"A"}". Retrying...\`);
await jestPlaywright.resetPage();
await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
await testFn();
} else {
throw err;
}
}
});
});
describe("C", () => {
it("smoke-test", async () => {
const testFn = async () => {
const context = {
id: "example-foo-bar--c",
title: "Example/foo/bar",
name: "C"
};
if (globalThis.__sbPreVisit) {
await globalThis.__sbPreVisit(page, context);
}
const result = await page.evaluate(({
id,
hasPlayFn
}) => __test(id, hasPlayFn), {
id: "example-foo-bar--c"
});
if (globalThis.__sbPostVisit) {
await globalThis.__sbPostVisit(page, context);
}
if (globalThis.__sbCollectCoverage) {
const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
if (!isCoverageSetupCorrectly) {
throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
The code in this story is not instrumented, which means the coverage setup is likely not correct.
More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
}
await jestPlaywright.saveCoverage(page);
}
return result;
};
try {
await testFn();
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"C"}". Retrying...\`);
await jestPlaywright.resetPage();
await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
await testFn();
} else {
throw err;
}
}
});
});
});
}
`);
});
it('should include "test" tag by default', () => {
// Should result in:
// - A being included
// - B being excluded
expect(
transformPlaywright(
dedent`
export default { title: 'foo/bar', component: Button };
export const A = { };
export const B = { tags: ['!test'] };
`,
filename
)
).toMatchInlineSnapshot(`
if (!require.main) {
describe("Example/foo/bar", () => {
describe("A", () => {
it("smoke-test", async () => {
const testFn = async () => {
const context = {
id: "example-foo-bar--a",
title: "Example/foo/bar",
name: "A"
};
if (globalThis.__sbPreVisit) {
await globalThis.__sbPreVisit(page, context);
}
const result = await page.evaluate(({
id,
hasPlayFn
}) => __test(id, hasPlayFn), {
id: "example-foo-bar--a"
});
if (globalThis.__sbPostVisit) {
await globalThis.__sbPostVisit(page, context);
}
if (globalThis.__sbCollectCoverage) {
const isCoverageSetupCorrectly = await page.evaluate(() => '__coverage__' in window);
if (!isCoverageSetupCorrectly) {
throw new Error(\`[Test runner] An error occurred when evaluating code coverage:
The code in this story is not instrumented, which means the coverage setup is likely not correct.
More info: https://github.com/storybookjs/test-runner#setting-up-code-coverage\`);
}
await jestPlaywright.saveCoverage(page);
}
return result;
};
try {
await testFn();
} catch (err) {
if (err.toString().includes('Execution context was destroyed')) {
console.log(\`An error occurred in the following story, most likely because of a navigation: "\${"Example/foo/bar"}/\${"A"}". Retrying...\`);
await jestPlaywright.resetPage();
await globalThis.__sbSetupPage(globalThis.page, globalThis.context);
await testFn();
} else {
throw err;
}
}
});
});
});
}
`);
Expand Down
2 changes: 2 additions & 0 deletions src/playwright/transformPlaywright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,13 @@ const makeTitleFactory = (filename: string) => {
};

export const transformPlaywright = (src: string, filename: string) => {
const tags = process.env.STORYBOOK_PREVIEW_TAGS?.split(',') ?? [];
const transformOptions = {
testPrefixer,
insertTestIfEmpty: true,
clearBody: true,
makeTitle: makeTitleFactory(filename),
previewAnnotations: { tags },
};

const result = transformCsf(src, transformOptions);
Expand Down
Loading

0 comments on commit 1076614

Please sign in to comment.