Skip to content

Commit 5246ffd

Browse files
Merge pull request #33 from gemini-testing/TESTPLANE-439.story_override_story_configs
feat: add ability to override testplane storyfile configs
2 parents 3f18cc9 + 3a94059 commit 5246ffd

File tree

10 files changed

+1941
-2742
lines changed

10 files changed

+1941
-2742
lines changed

jest.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
12
module.exports = {
23
clearMocks: true,
34
globals: {
45
"ts-jest": {
5-
"tsconfig": "tsconfig.spec.json",
6+
tsconfig: "tsconfig.spec.json",
7+
isolatedModules: true,
68
},
79
},
810
preset: "ts-jest",

package-lock.json

Lines changed: 1616 additions & 2682 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,20 @@
4444
"devDependencies": {
4545
"@gemini-testing/commander": "^2.15.3",
4646
"@types/fs-extra": "^11.0.4",
47-
"@types/jest": "^27.5.2",
47+
"@types/jest": "^29.5.14",
4848
"@types/lodash": "^4.14.172",
4949
"@types/node": "^12.20.19",
5050
"@types/npm-which": "^3.0.3",
5151
"@typescript-eslint/eslint-plugin": "^4.29.3",
5252
"@typescript-eslint/parser": "^4.29.3",
5353
"eslint": "^7.32.0",
5454
"eslint-config-gemini-testing": "^3.0.0",
55-
"jest": "^27.5.1",
55+
"jest": "^29.7.0",
5656
"jest-extended": "^0.11.5",
57-
"prettier": "^2.3.2",
57+
"prettier": "^3.4.2",
5858
"rimraf": "^3.0.2",
5959
"testplane": "^0.1.0-rc.0",
60-
"ts-jest": "^27.1.5",
60+
"ts-jest": "^29.2.5",
6161
"typescript": "^4.3.5"
6262
},
6363
"dependencies": {

src/storybook/story-test-runner/extend-stories.test.ts

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,115 @@ import { extendStoriesFromStoryFile } from "./extend-stories";
22
import { StorybookStory } from "./types";
33

44
describe("storybook/story-test-runner/extend-stories", () => {
5-
it("should log warning failed storyfile import", async () => {
5+
const mkRequireStub_ = (
6+
impl?: Parameters<(typeof jest)["fn"]>[0],
7+
): jest.MockedFunction<(typeof globalThis)["require"]> => {
8+
return jest.fn(impl) as unknown as jest.MockedFunction<(typeof globalThis)["require"]>;
9+
};
10+
11+
it("should log warning failed storyfile import", () => {
612
jest.spyOn(console, "warn").mockImplementation(jest.fn);
13+
const requireFn = mkRequireStub_(() => {
14+
throw new Error("some error message");
15+
});
716
const stories = [{ name: "foo", absolutePath: "not/existing.ts" }] as StorybookStory[];
817

9-
extendStoriesFromStoryFile(stories);
18+
extendStoriesFromStoryFile(stories, { requireFn });
1019

1120
const expectedMsg = [
1221
'"testplane" section is ignored in storyfile "not/existing.ts", because the file could not be read:',
13-
"Error: Cannot find module 'not/existing.ts' from 'src/storybook/story-test-runner/extend-stories.ts' ",
22+
"Error: some error message ",
1423
"There could be other story files. ",
1524
"Set 'TESTPLANE_STORYBOOK_DISABLE_STORY_REQUIRE_WARNING' environment variable to hide this warning",
1625
].join("\n");
1726
expect(console.warn).toBeCalledWith(expectedMsg);
1827
});
1928

20-
it("should fallback, when could not read story file", async () => {
29+
it("should fallback, when could not read story file", () => {
30+
const requireFn = mkRequireStub_(() => {
31+
throw new Error("file does not exist");
32+
});
2133
const stories = [{ name: "foo", absolutePath: "not/existing.js" }] as StorybookStory[];
2234

23-
const extendedStories = extendStoriesFromStoryFile(stories);
35+
const extendedStories = extendStoriesFromStoryFile(stories, { requireFn });
2436

2537
expect(extendedStories[0].skip).toBe(false);
2638
expect(extendedStories[0].assertViewOpts).toEqual({});
2739
expect(extendedStories[0].browserIds).toBe(null);
2840
});
41+
42+
it("should overlay story configs over file configs", () => {
43+
const customTests = {};
44+
const defaultExport = {
45+
testplane: { browserIds: ["firefox"] },
46+
testplaneConfig: {
47+
skip: true,
48+
browserIds: ["chrome"],
49+
assertViewOpts: { tolerance: 10, ignoreDiffPixelCount: 10 },
50+
autoScreenshotStorybookGlobals: { dark: { theme: "dark" } },
51+
},
52+
};
53+
const fooExport = {
54+
testplane: customTests,
55+
testplaneConfig: { skip: false, autoScreenshots: false, assertViewOpts: { ignoreElements: ["foobar"] } },
56+
};
57+
const requireFn = mkRequireStub_().mockReturnValue({ default: defaultExport, foo: fooExport });
58+
const stories = [{ name: "foo", absolutePath: "not/existing.js" }] as StorybookStory[];
59+
60+
const extendedStories = extendStoriesFromStoryFile(stories, { requireFn });
61+
62+
expect(extendedStories).toMatchObject([
63+
{
64+
name: "foo",
65+
absolutePath: "not/existing.js",
66+
extraTests: {},
67+
skip: false,
68+
browserIds: ["chrome"],
69+
assertViewOpts: {
70+
ignoreElements: ["foobar"],
71+
tolerance: 10,
72+
ignoreDiffPixelCount: 10,
73+
},
74+
autoScreenshots: false,
75+
autoScreenshotStorybookGlobals: { dark: { theme: "dark" } },
76+
},
77+
]);
78+
});
79+
80+
it("should overlay story configs over default configs", () => {
81+
const customTests = {};
82+
const fooExport = {
83+
testplane: customTests,
84+
testplaneConfig: { skip: false, autoScreenshots: false, assertViewOpts: { ignoreElements: ["foobar"] } },
85+
};
86+
const requireFn = mkRequireStub_().mockReturnValue({ default: {}, foo: fooExport });
87+
const stories = [{ name: "foo", absolutePath: "not/existing.js" }] as StorybookStory[];
88+
89+
const extendedStories = extendStoriesFromStoryFile(stories, { requireFn });
90+
91+
expect(extendedStories).toMatchObject([
92+
{
93+
name: "foo",
94+
absolutePath: "not/existing.js",
95+
extraTests: {},
96+
skip: false,
97+
browserIds: null,
98+
assertViewOpts: {
99+
ignoreElements: ["foobar"],
100+
},
101+
autoScreenshots: false,
102+
autoscreenshotSelector: null,
103+
autoScreenshotStorybookGlobals: {},
104+
},
105+
]);
106+
});
107+
108+
it("should fallback reading story configs from deprecated testplane property", () => {
109+
const stories = [{ name: "foo", absolutePath: "not/existing.js" }] as StorybookStory[];
110+
const requireFn = mkRequireStub_().mockReturnValue({ default: { testplane: { skip: true } }, foo: {} });
111+
112+
const extendedStories = extendStoriesFromStoryFile(stories, { requireFn });
113+
114+
expect(extendedStories).toMatchObject([{ name: "foo", skip: true }]);
115+
});
29116
});

src/storybook/story-test-runner/extend-stories.ts

Lines changed: 43 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import TypedModule from "module";
2+
import { extractInheritedValue, inheritValue } from "./inheritable-values";
23
import type { TestplaneMetaConfig, TestplaneStoryConfig } from "../../types";
3-
import type { StorybookStory, StorybookStoryExtended } from "./types";
4+
import type { StorybookStory, StorybookStoryExtraProperties, StorybookStoryExtended } from "./types";
45

56
const Module = TypedModule as any; // eslint-disable-line @typescript-eslint/no-explicit-any
67

@@ -12,45 +13,60 @@ type StoryFile = { default: TestplaneMetaConfig } & Record<string, TestplaneStor
1213

1314
let loggedStoryFileRequireError = Boolean(process.env.TESTPLANE_STORYBOOK_DISABLE_STORY_REQUIRE_WARNING);
1415

15-
export function extendStoriesFromStoryFile(stories: StorybookStory[]): StorybookStoryExtended[] {
16+
export function extendStoriesFromStoryFile(
17+
stories: StorybookStory[],
18+
{ requireFn = require } = {},
19+
): StorybookStoryExtended[] {
1620
const storiesMap = getStoriesMap(stories);
1721
const storyPath = stories[0].absolutePath;
18-
const storyFile = getStoryFile(storyPath);
22+
const storyFile = getStoryFile(storyPath, { requireFn });
1923
const withStoryFileExtendedStories = stories as StorybookStoryExtended[];
2024

21-
if (!storyFile) {
22-
return withStoryFileExtendedStories.map(story => {
23-
story.skip = false;
24-
story.assertViewOpts = {};
25-
story.browserIds = null;
26-
story.autoScreenshotStorybookGlobals = {};
25+
const storyExtendedBaseConfig = {
26+
skip: false,
27+
browserIds: null,
28+
assertViewOpts: {},
29+
autoScreenshots: null,
30+
autoscreenshotSelector: null,
31+
autoScreenshotStorybookGlobals: {},
32+
extraTests: null,
33+
} satisfies StorybookStoryExtraProperties;
2734

28-
return story;
29-
});
35+
if (!storyFile) {
36+
return withStoryFileExtendedStories.map(story => Object.assign(story, storyExtendedBaseConfig));
3037
}
3138

32-
for (const storyName in storyFile) {
39+
const storyTestplaneDefaultConfigs = storyFile.default?.testplaneConfig || storyFile.default?.testplane || {};
40+
41+
for (const storyName of Object.keys(storyFile)) {
3342
if (storyName === "default") {
34-
withStoryFileExtendedStories.forEach(story => {
35-
const testplaneStoryOpts = storyFile[storyName].testplane || {};
43+
continue;
44+
}
3645

37-
story.skip = testplaneStoryOpts.skip || false;
38-
story.assertViewOpts = testplaneStoryOpts.assertViewOpts || {};
39-
story.browserIds = testplaneStoryOpts.browserIds || null;
40-
story.autoscreenshotSelector = testplaneStoryOpts.autoscreenshotSelector || null;
41-
story.autoScreenshotStorybookGlobals = testplaneStoryOpts.autoScreenshotStorybookGlobals || {};
42-
});
46+
const storyMapKey = getStoryNameId(storyName);
4347

48+
if (!storiesMap.has(storyMapKey)) {
4449
continue;
4550
}
4651

47-
const storyMapKey = getStoryNameId(storyName);
52+
const storyTestlaneConfigs = storyFile[storyName].testplaneConfig || {};
53+
const story = storiesMap.get(storyMapKey) as StorybookStoryExtended;
4854

49-
if (storiesMap.has(storyMapKey) && storyFile[storyName].testplane) {
50-
const story = storiesMap.get(storyMapKey) as StorybookStoryExtended;
55+
Object.assign(story, storyExtendedBaseConfig, storyTestplaneDefaultConfigs, storyTestlaneConfigs, {
56+
extraTests: storyFile[storyName].testplane || null,
57+
});
5158

52-
story.extraTests = storyFile[storyName].testplane;
53-
}
59+
story.assertViewOpts = extractInheritedValue(
60+
inheritValue(storyTestplaneDefaultConfigs.assertViewOpts, storyTestlaneConfigs.assertViewOpts),
61+
storyExtendedBaseConfig.assertViewOpts,
62+
);
63+
64+
story.autoScreenshotStorybookGlobals =
65+
inheritValue(
66+
storyExtendedBaseConfig.autoScreenshotStorybookGlobals,
67+
storyTestplaneDefaultConfigs.autoScreenshotStorybookGlobals,
68+
storyTestlaneConfigs.autoScreenshotStorybookGlobals,
69+
) || {};
5470
}
5571

5672
return withStoryFileExtendedStories;
@@ -74,13 +90,13 @@ function getStoryNameId(storyName: string): string {
7490
return storyName.replace(nonAsciiWordRegExp, "").toLowerCase();
7591
}
7692

77-
function getStoryFile(storyPath: string): StoryFile | null {
93+
function getStoryFile(storyPath: string, { requireFn = require } = {}): StoryFile | null {
7894
const unmockFn = mockLoaders({ except: storyPath });
7995

8096
let storyFile;
8197

8298
try {
83-
storyFile = require(storyPath); // eslint-disable-line @typescript-eslint/no-var-requires
99+
storyFile = requireFn(storyPath);
84100
} catch (error) {
85101
if (!loggedStoryFileRequireError) {
86102
loggedStoryFileRequireError = true;

src/storybook/story-test-runner/index.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import type { StoryLoadResult } from "./open-story/testplane-open-story";
55
import type { TestplaneOpts } from "../story-to-test";
66
import type { TestFunctionExtendedCtx } from "../../types";
77
import type { StorybookStoryExtended, StorybookStory } from "./types";
8+
import { extractInheritedValue } from "./inheritable-values";
89

910
export function getAbsoluteFilePath(): string {
1011
return __filename;
1112
}
1213

14+
// Those stories must be from a single story file
1315
export function run(stories: StorybookStory[], opts: TestplaneOpts): void {
1416
const withStoryFileDataStories = extendStoriesFromStoryFile(stories);
1517

@@ -21,26 +23,28 @@ function createTestplaneTests(
2123
{ autoScreenshots, autoscreenshotSelector, autoScreenshotStorybookGlobals }: TestplaneOpts,
2224
): void {
2325
nestedDescribe(story, () => {
24-
const rawAutoScreenshotGlobalSets = {
25-
...autoScreenshotStorybookGlobals,
26-
...story.autoScreenshotStorybookGlobals,
27-
};
26+
const rawAutoScreenshotGlobalSets = extractInheritedValue(
27+
story.autoScreenshotStorybookGlobals,
28+
autoScreenshotStorybookGlobals,
29+
);
2830

29-
const screenshotGlobalSetNames = Object.keys(rawAutoScreenshotGlobalSets);
31+
const screenshotGlobalSetNames = Object.keys(rawAutoScreenshotGlobalSets).filter(name =>
32+
Boolean(rawAutoScreenshotGlobalSets[name]),
33+
);
3034

3135
const autoScreenshotGlobalSets = screenshotGlobalSetNames.length
3236
? screenshotGlobalSetNames.map(name => ({ name, globals: rawAutoScreenshotGlobalSets[name] }))
3337
: [{ name: "", globals: {} }];
3438

35-
if (autoScreenshots) {
39+
if (story.autoScreenshots ?? autoScreenshots) {
3640
for (const { name, globals } of autoScreenshotGlobalSets) {
3741
extendedIt(
3842
story,
3943
`Autoscreenshot${name ? ` ${name}` : ""}`,
4044
async function (ctx: TestFunctionExtendedCtx) {
4145
ctx.expect = globalThis.expect;
4246

43-
const result = await openStoryStep(ctx.browser, story, globals);
47+
const result = await openStoryStep(ctx.browser, story, globals as Record<string, unknown>);
4448
const selector = story.autoscreenshotSelector || autoscreenshotSelector || result.rootSelector;
4549

4650
await autoScreenshotStep(ctx.browser, selector);

0 commit comments

Comments
 (0)