Skip to content

Commit dea2030

Browse files
feat: add ability to override testplane storyfile configs
1 parent 4a0f84e commit dea2030

File tree

5 files changed

+139
-47
lines changed

5 files changed

+139
-47
lines changed

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

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,88 @@ 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 / file confifs / default 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).toStrictEqual([
63+
{
64+
name: "foo",
65+
absolutePath: "not/existing.js",
66+
extraTests: customTests,
67+
skip: false,
68+
browserIds: ["chrome"],
69+
assertViewOpts: {
70+
ignoreElements: ["foobar"],
71+
tolerance: 10,
72+
ignoreDiffPixelCount: 10,
73+
},
74+
autoScreenshots: false,
75+
autoscreenshotSelector: null,
76+
autoScreenshotStorybookGlobals: { dark: { theme: "dark" } },
77+
},
78+
]);
79+
});
80+
81+
it("should fallback reading story configs from deprecated testplane property", () => {
82+
const stories = [{ name: "foo", absolutePath: "not/existing.js" }] as StorybookStory[];
83+
const requireFn = mkRequireStub_().mockReturnValue({ default: { testplane: { skip: true } }, foo: {} });
84+
85+
const extendedStories = extendStoriesFromStoryFile(stories, { requireFn });
86+
87+
expect(extendedStories).toMatchObject([{ name: "foo", skip: true }]);
88+
});
2989
});

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

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import TypedModule from "module";
22
import type { TestplaneMetaConfig, TestplaneStoryConfig } from "../../types";
3-
import type { StorybookStory, StorybookStoryExtended } from "./types";
3+
import type { StorybookStory, StorybookStoryExtraProperties, StorybookStoryExtended } from "./types";
44

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

@@ -12,45 +12,62 @@ type StoryFile = { default: TestplaneMetaConfig } & Record<string, TestplaneStor
1212

1313
let loggedStoryFileRequireError = Boolean(process.env.TESTPLANE_STORYBOOK_DISABLE_STORY_REQUIRE_WARNING);
1414

15-
export function extendStoriesFromStoryFile(stories: StorybookStory[]): StorybookStoryExtended[] {
15+
export function extendStoriesFromStoryFile(
16+
stories: StorybookStory[],
17+
{ requireFn = require } = {},
18+
): StorybookStoryExtended[] {
1619
const storiesMap = getStoriesMap(stories);
1720
const storyPath = stories[0].absolutePath;
18-
const storyFile = getStoryFile(storyPath);
21+
const storyFile = getStoryFile(storyPath, { requireFn });
1922
const withStoryFileExtendedStories = stories as StorybookStoryExtended[];
2023

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

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

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

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-
});
45+
const storyMapKey = getStoryNameId(storyName);
4346

47+
if (!storiesMap.has(storyMapKey)) {
4448
continue;
4549
}
4650

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

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

52-
story.extraTests = storyFile[storyName].testplane;
53-
}
58+
story.assertViewOpts = Object.assign(
59+
{},
60+
storyExtendedBaseConfig.assertViewOpts,
61+
storyTestplaneDefaultConfigs.assertViewOpts,
62+
storyTestlaneConfigs.assertViewOpts,
63+
);
64+
65+
story.autoScreenshotStorybookGlobals = Object.assign(
66+
{},
67+
storyExtendedBaseConfig.autoScreenshotStorybookGlobals,
68+
storyTestplaneDefaultConfigs.autoScreenshotStorybookGlobals,
69+
storyTestlaneConfigs.autoScreenshotStorybookGlobals,
70+
);
5471
}
5572

5673
return withStoryFileExtendedStories;
@@ -74,13 +91,13 @@ function getStoryNameId(storyName: string): string {
7491
return storyName.replace(nonAsciiWordRegExp, "").toLowerCase();
7592
}
7693

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

8097
let storyFile;
8198

8299
try {
83-
storyFile = require(storyPath); // eslint-disable-line @typescript-eslint/no-var-requires
100+
storyFile = requireFn(storyPath);
84101
} catch (error) {
85102
if (!loggedStoryFileRequireError) {
86103
loggedStoryFileRequireError = true;

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export function getAbsoluteFilePath(): string {
1010
return __filename;
1111
}
1212

13+
// Those stories must be from a single story file
1314
export function run(stories: StorybookStory[], opts: TestplaneOpts): void {
1415
const withStoryFileDataStories = extendStoriesFromStoryFile(stories);
1516

@@ -32,7 +33,7 @@ function createTestplaneTests(
3233
? screenshotGlobalSetNames.map(name => ({ name, globals: rawAutoScreenshotGlobalSets[name] }))
3334
: [{ name: "", globals: {} }];
3435

35-
if (autoScreenshots) {
36+
if (story.autoScreenshots ?? autoScreenshots) {
3637
for (const { name, globals } of autoScreenshotGlobalSets) {
3738
extendedIt(
3839
story,

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ import type { AssertViewOpts } from "testplane";
22
import type { TestplaneTestFunction } from "../../types";
33
import type { StorybookStoryExtended as StorybookStory } from "../get-stories";
44

5-
export interface StorybookStoryExtended extends StorybookStory {
5+
export interface StorybookStoryExtraProperties {
66
skip: boolean;
77
assertViewOpts: AssertViewOpts;
88
browserIds: Array<string | RegExp> | null;
9-
extraTests?: Record<string, TestplaneTestFunction>;
9+
extraTests: Record<string, TestplaneTestFunction> | null;
10+
autoScreenshots: boolean | null;
1011
autoscreenshotSelector: string | null;
1112
autoScreenshotStorybookGlobals: Record<string, Record<string, unknown>>;
1213
}
1314

15+
export interface StorybookStoryExtended extends StorybookStory, StorybookStoryExtraProperties {}
16+
1417
export type ExecutionContextExtended = WebdriverIO.Browser["executionContext"] & {
1518
"@testplane/storybook-assertView-opts": AssertViewOpts;
1619
};

src/types.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,34 @@ interface StorybookMetaConfig {
1111
component?: unknown;
1212
}
1313

14-
interface CustomField<K> {
15-
testplane?: K;
16-
}
17-
1814
type Combined<N, B = void> = B extends void ? N : N & B;
1915

16+
type TestplaneStoryFileConfig = {
17+
skip?: boolean;
18+
assertViewOpts?: AssertViewOpts;
19+
browserIds?: Array<string | RegExp>;
20+
autoScreenshots?: boolean;
21+
autoscreenshotSelector?: string;
22+
autoScreenshotStorybookGlobals?: Record<string, Record<string, unknown>>;
23+
};
24+
2025
export type TestplaneMetaConfig<T = void> = Combined<
21-
CustomField<{
22-
skip?: boolean;
23-
assertViewOpts?: AssertViewOpts;
24-
browserIds?: Array<string | RegExp>;
25-
autoscreenshotSelector?: string;
26-
autoScreenshotStorybookGlobals?: Record<string, Record<string, unknown>>;
27-
}>,
26+
{
27+
/**
28+
* @deprecated Use "testplaneConfig" instead of "testplane"
29+
*/
30+
testplane?: TestplaneStoryFileConfig;
31+
testplaneConfig?: TestplaneStoryFileConfig;
32+
},
2833
T
2934
>;
3035

31-
export type TestplaneStoryConfig<T = void> = Combined<CustomField<Record<string, TestplaneTestFunction>>, T>;
36+
export type TestplaneStoryConfig<T = void> = Combined<
37+
{
38+
testplane?: Record<string, TestplaneTestFunction>;
39+
testplaneConfig?: TestplaneStoryFileConfig;
40+
},
41+
T
42+
>;
3243

3344
export type WithTestplane<T = void> = T extends StorybookMetaConfig ? TestplaneMetaConfig<T> : TestplaneStoryConfig<T>;

0 commit comments

Comments
 (0)