Skip to content

Commit

Permalink
fix: apply edits
Browse files Browse the repository at this point in the history
  • Loading branch information
KuznetsovRoman committed Nov 6, 2024
1 parent 53e02a2 commit 6364eca
Show file tree
Hide file tree
Showing 13 changed files with 121 additions and 54 deletions.
37 changes: 29 additions & 8 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isBoolean, isString, isNumber, isArray, isRegExp } from "lodash";
import { option, root, section, map } from "gemini-configparser";
import { isBoolean, isString, isNumber, isArray, isRegExp, isNull, isPlainObject } from "lodash";
import { option, root, section } from "gemini-configparser";
import type { Parser } from "gemini-configparser";

const assertType = <T>(name: string, validationFn: (v: unknown) => boolean, type: string) => {
Expand All @@ -10,6 +10,20 @@ const assertType = <T>(name: string, validationFn: (v: unknown) => boolean, type
};
};

const assertRecordOfRecords = (value: unknown, name: string): void => {
if (!isNull(value) && !isPlainObject(value)) {
throw new Error(`"${name}" must be an object`);
}

const record = value as Record<string, unknown>;

for (const key of Object.keys(record)) {
if (!isPlainObject(record[key])) {
throw new Error(`"${name}.${key}" must be an object`);
}
}
};

const booleanOption = (name: string, defaultValue = false): Parser<boolean> =>
option({
parseEnv: (val: string) => Boolean(JSON.parse(val)),
Expand Down Expand Up @@ -48,11 +62,22 @@ const stringAndRegExpArrayOption = (name: string, defaultValue: string[]): Parse
},
});

const optionalRecordOfRecordsOption = (
name: string,
defaultValue: Record<string, unknown> = {},
): Parser<Record<string, Record<string, unknown>>> =>
option({
parseEnv: JSON.parse,
parseCli: JSON.parse,
defaultValue,
validate: value => assertRecordOfRecords(value, name),
});

export interface PluginConfig {
enabled: boolean;
storybookConfigDir: string;
autoScreenshots: boolean;
customAutoScreenshots: Record<string, { globals: Record<string, unknown>}>;
autoScreenshotStorybookGlobals: Record<string, Record<string, unknown>>;
localport: number;
remoteStorybookUrl: string;
browserIds: Array<string | RegExp>;
Expand All @@ -69,11 +94,7 @@ export function parseConfig(options: PluginPartialConfig): PluginConfig {
enabled: booleanOption("enabled", true),
storybookConfigDir: stringOption("storybookConfigDir", ".storybook"),
autoScreenshots: booleanOption("autoScreenshots", true),
customAutoScreenshots: map(section({
globals: section({
theme: stringOption("theme", ""),
}),
})),
autoScreenshotStorybookGlobals: optionalRecordOfRecordsOption("autoScreenshotStorybookGlobals", {}),
localport: numberOption("localport", 6006),
remoteStorybookUrl: stringOption("remoteStorybookUrl", ""),
browserIds: stringAndRegExpArrayOption("browserIds", []),
Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ function onTestplaneMaster(testplane: Testplane, config: PluginConfig): void {

const stories = await getStories(storybookUrl);

const storyTestFiles = await buildStoryTestFiles(stories, { autoScreenshots: config.autoScreenshots, customAutoScreenshots: config.customAutoScreenshots });
const storyTestFiles = await buildStoryTestFiles(stories, {
autoScreenshots: config.autoScreenshots,
autoScreenshotStorybookGlobals: config.autoScreenshotStorybookGlobals,
});

patchTestplaneBaseUrl(testplane.config, iframeUrl);
disableTestplaneIsolation(testplane.config, config.browserIds);
Expand Down
4 changes: 3 additions & 1 deletion src/storybook/story-test-runner/extend-stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ describe("storybook/story-test-runner/extend-stories", () => {

const expectedMsg = [
'"testplane" section is ignored in storyfile "not/existing.ts", because the file could not be read:',
"Error: Cannot find module 'not/existing.ts' from 'src/storybook/story-test-runner/extend-stories.ts'",
"Error: Cannot find module 'not/existing.ts' from 'src/storybook/story-test-runner/extend-stories.ts' ",
"There could be other story files. ",
"Set 'TESTPLANE_STORYBOOK_DISABLE_STORY_REQUIRE_WARNING' environment variable to hide this warning",
].join("\n");
expect(console.warn).toBeCalledWith(expectedMsg);
});
Expand Down
3 changes: 3 additions & 0 deletions src/storybook/story-test-runner/extend-stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function extendStoriesFromStoryFile(stories: StorybookStory[]): Storybook
story.assertViewOpts = testplaneStoryOpts.assertViewOpts || {};
story.browserIds = testplaneStoryOpts.browserIds || null;
story.autoscreenshotSelector = testplaneStoryOpts.autoscreenshotSelector || null;
story.autoScreenshotStorybookGlobals = testplaneStoryOpts.autoScreenshotStorybookGlobals || {};
});

continue;
Expand Down Expand Up @@ -86,6 +87,8 @@ function getStoryFile(storyPath: string): StoryFile | null {
const warningMessage = [
`"testplane" section is ignored in storyfile "${storyPath}",`,
`because the file could not be read:\n${error}`,
"\nThere could be other story files.",
"\nSet 'TESTPLANE_STORYBOOK_DISABLE_STORY_REQUIRE_WARNING' environment variable to hide this warning",
].join(" ");

console.warn(warningMessage);
Expand Down
56 changes: 28 additions & 28 deletions src/storybook/story-test-runner/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { extendStoriesFromStoryFile } from "./extend-stories";
import { nestedDescribe, extendedIt } from "./test-decorators";
import { openStory, StorybookWindow } from "./open-story";
import { openStory } from "./open-story";
import type { StoryLoadResult } from "./open-story/testplane-open-story";
import type { TestplaneOpts } from "../story-to-test";
import type { TestFunctionExtendedCtx } from "../../types";
Expand All @@ -16,27 +16,32 @@ export function run(stories: StorybookStory[], opts: TestplaneOpts): void {
withStoryFileDataStories.forEach(story => createTestplaneTests(story, opts));
}

function createTestplaneTests(story: StorybookStoryExtended, { autoScreenshots, customAutoScreenshots }: TestplaneOpts): void {
function createTestplaneTests(
story: StorybookStoryExtended,
{ autoScreenshots, autoScreenshotStorybookGlobals }: TestplaneOpts,
): void {
nestedDescribe(story, () => {
const rawAutoScreenshotGlobalSets = Object.keys(story.autoScreenshotStorybookGlobals).length
? Object.keys(story.autoScreenshotStorybookGlobals)
: Object.keys(autoScreenshotStorybookGlobals);

const autoScreenshotGlobalSets = rawAutoScreenshotGlobalSets.length
? rawAutoScreenshotGlobalSets.map(name => ({ name, globals: story.autoScreenshotStorybookGlobals[name] }))
: [{ name: "", globals: {} }];

if (autoScreenshots) {
if (customAutoScreenshots) {
for (const testName in customAutoScreenshots) {
extendedIt(story, testName, async function (ctx: TestFunctionExtendedCtx) {
for (const { name, globals } of autoScreenshotGlobalSets) {
extendedIt(
story,
`Autoscreenshot${name ? ` ${name}` : ""}`,
async function (ctx: TestFunctionExtendedCtx) {
ctx.expect = globalThis.expect;

const result = await openStoryStep(ctx.browser, story);
await setGlobalsStep(ctx.browser, customAutoScreenshots[testName].globals);
await autoScreenshotStep(ctx.browser, story.autoscreenshotSelector || result.rootSelector);
});
}
} else {
extendedIt(story, "Autoscreenshot", async function (ctx: TestFunctionExtendedCtx) {
ctx.expect = globalThis.expect;

const result = await openStoryStep(ctx.browser, story);
const result = await openStoryStep(ctx.browser, story, globals);

await autoScreenshotStep(ctx.browser, story.autoscreenshotSelector || result.rootSelector);
});
await autoScreenshotStep(ctx.browser, story.autoscreenshotSelector || result.rootSelector);
},
);
}
}

Expand All @@ -57,17 +62,12 @@ function createTestplaneTests(story: StorybookStoryExtended, { autoScreenshots,
});
}

async function openStoryStep(browser: WebdriverIO.Browser, story: StorybookStoryExtended): Promise<StoryLoadResult> {
return browser.runStep("@testplane/storybook: open story", () => openStory(browser, story));
}

async function setGlobalsStep(browser: WebdriverIO.Browser, globals: Record<string, unknown>): Promise<void> {
return browser.runStep("@testplane/storybook: set globals", () => {
return browser.execute(async (globals) => {
const channel = (window as StorybookWindow).__STORYBOOK_ADDONS_CHANNEL__;
channel.emit("updateGlobals", { globals });
}, globals);
});
async function openStoryStep(
browser: WebdriverIO.Browser,
story: StorybookStoryExtended,
storybookGlobals: Record<string, unknown> = {},
): Promise<StoryLoadResult> {
return browser.runStep("@testplane/storybook: open story", () => openStory(browser, story, storybookGlobals));
}

async function autoScreenshotStep(browser: WebdriverIO.Browser, rootSelector: string): Promise<void> {
Expand Down
8 changes: 6 additions & 2 deletions src/storybook/story-test-runner/open-story/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ export type { StorybookWindow } from "./testplane-open-story";
import type { ExecutionContextExtended, StorybookStoryExtended } from "../types";
import type { StoryLoadResult } from "./testplane-open-story";

export async function openStory(browser: WebdriverIO.Browser, story: StorybookStoryExtended): Promise<StoryLoadResult> {
export async function openStory(
browser: WebdriverIO.Browser,
story: StorybookStoryExtended,
storybookGlobals: Record<string, unknown>,
): Promise<StoryLoadResult> {
const browserConfig = await browser.getConfig();
const currentBrowserUrl = await browser.getUrl();

Expand All @@ -25,7 +29,7 @@ export async function openStory(browser: WebdriverIO.Browser, story: StorybookSt
await extendBrowserMeta(browser, story);

return browser.runStep("wait story load", async (): Promise<StoryLoadResult> => {
const storyLoadResult = await testplaneOpenStory.execute(browser, story.id, shouldRemount);
const storyLoadResult = await testplaneOpenStory.execute(browser, story.id, storybookGlobals, shouldRemount);

if (storyLoadResult.loadError) {
throw new Error(storyLoadResult.loadError);
Expand Down
36 changes: 31 additions & 5 deletions src/storybook/story-test-runner/open-story/testplane-open-story.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ interface HTMLElement {

export type StorybookWindow = Window &
typeof globalThis & {
__HERMIONE_OPEN_STORY__: (storyId: string, remountOnly: boolean, done: (result: string) => void) => void;
__HERMIONE_OPEN_STORY__: (
storyId: string,
storybookGlobals: Record<string, unknown>,
remountOnly: boolean,
done: (result: string) => void,
) => void;
__STORYBOOK_ADDONS_CHANNEL__: EventEmitter;
__STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER_STATE__: Record<
string,
Expand All @@ -34,10 +39,11 @@ export async function inject(browser: WebdriverIO.Browser): Promise<void> {
export async function execute(
browser: WebdriverIO.Browser,
storyId: string,
storybookGlobals: Record<string, unknown>,
shouldRemount: boolean,
): Promise<StoryLoadResult> {
const getResult = (): Promise<StoryLoadResult> =>
browser.executeAsync(executeOpenStoryScript, storyId, shouldRemount).then(JSON.parse);
browser.executeAsync(executeOpenStoryScript, storyId, storybookGlobals, shouldRemount).then(JSON.parse);

const result: StoryLoadResult = await getResult();

Expand All @@ -58,7 +64,12 @@ export async function execute(

export default { inject, execute };

function openStoryScript(storyId: string, shouldRemount: boolean, done: (result: string) => void): void {
function openStoryScript(
storyId: string,
storybookGlobals: Record<string, unknown>,
shouldRemount: boolean,
done: (result: string) => void,
): void {
function onPageLoad(fn: () => void): void {
if (document.readyState === "complete") {
fn();
Expand Down Expand Up @@ -140,12 +151,22 @@ function openStoryScript(storyId: string, shouldRemount: boolean, done: (result:
doneJson(result);
}

if (!channel) {
result.loadError = "Couldn't find storybook channel. Looks like the opened page is not storybook preview";

doneJson(result);
}

channel.once("playFunctionThrewException", onPlayFunctionThrewException);
channel.once("storyRendered", onStoryRendered);
channel.once("storyMissing", onStoryMissing);
channel.once("storyThrewException", onStoryThrewException);
channel.once("storyErrored", onStoryErrored);

if (Object.keys(storybookGlobals).length) {
channel.emit("updateGlobals", storybookGlobals);
}

if (shouldRemount) {
channel.emit("setCurrentStory", { storyId: "" });
}
Expand All @@ -154,9 +175,14 @@ function openStoryScript(storyId: string, shouldRemount: boolean, done: (result:
});
}

function executeOpenStoryScript(storyId: string, remountOnly: boolean, done: (result: string) => void): void {
function executeOpenStoryScript(
storyId: string,
storybookGlobals: Record<string, unknown>,
remountOnly: boolean,
done: (result: string) => void,
): void {
if ((window as StorybookWindow).__HERMIONE_OPEN_STORY__) {
(window as StorybookWindow).__HERMIONE_OPEN_STORY__(storyId, remountOnly, done);
(window as StorybookWindow).__HERMIONE_OPEN_STORY__(storyId, storybookGlobals, remountOnly, done);
} else {
done(JSON.stringify({ notInjected: true }));
}
Expand Down
1 change: 1 addition & 0 deletions src/storybook/story-test-runner/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface StorybookStoryExtended extends StorybookStory {
browserIds: Array<string | RegExp> | null;
extraTests?: Record<string, TestplaneTestFunction>;
autoscreenshotSelector: string | null;
autoScreenshotStorybookGlobals: Record<string, Record<string, unknown>>;
}

export type ExecutionContextExtended = WebdriverIO.Browser["executionContext"] & {
Expand Down
16 changes: 11 additions & 5 deletions src/storybook/story-to-test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ describe("storybook/story-to-test", () => {
it("should save tests in tmpdir", async () => {
const story = { importPath: "./story/path/story.js" } as StorybookStoryExtended;

const storyTestFiles = await buildStoryTestFiles([story], { autoScreenshots: true });
const storyTestFiles = await buildStoryTestFiles([story], {
autoScreenshots: true,
autoScreenshotStorybookGlobals: {},
});

expect(storyTestFiles).toEqual(["/tmpdir/testplane-storybook-autogenerated/story/path/story.js.testplane.js"]);
});

it("should empty tests dir before writing tests", async () => {
await buildStoryTestFiles([], { autoScreenshots: true });
await buildStoryTestFiles([], { autoScreenshots: true, autoScreenshotStorybookGlobals: {} });

expect(fs.emptyDir).toBeCalled();
});
Expand All @@ -28,17 +31,20 @@ describe("storybook/story-to-test", () => {
const storyFirst = { importPath: "./story/path/story-first.js" } as StorybookStoryExtended;
const storySecond = { importPath: "./story/path/story-second.js" } as StorybookStoryExtended;

const storyTestFiles = await buildStoryTestFiles([storyFirst, storySecond], { autoScreenshots: true });
const storyTestFiles = await buildStoryTestFiles([storyFirst, storySecond], {
autoScreenshots: true,
autoScreenshotStorybookGlobals: { foo: { bar: "baz" } },
});

expect(writeStoryTestsFile).toBeCalledWith({
testFile: "./story/path/story-first.js.testplane.js",
opts: { autoScreenshots: true },
opts: { autoScreenshots: true, autoScreenshotStorybookGlobals: { foo: { bar: "baz" } } },
stories: [storyFirst],
});

expect(writeStoryTestsFile).toBeCalledWith({
testFile: "./story/path/story-second.js.testplane.js",
opts: { autoScreenshots: true },
opts: { autoScreenshots: true, autoScreenshotStorybookGlobals: { foo: { bar: "baz" } } },
stories: [storySecond],
});

Expand Down
2 changes: 1 addition & 1 deletion src/storybook/story-to-test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { StorybookStoryExtended } from "../get-stories";

export interface TestplaneOpts {
autoScreenshots: boolean;
customAutoScreenshots: Record<string, { globals: Record<string, unknown>}>;
autoScreenshotStorybookGlobals: Record<string, Record<string, unknown>>;
}

const testplaneTestNameSuffix = ".testplane.js";
Expand Down
4 changes: 2 additions & 2 deletions src/storybook/story-to-test/write-tests-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ jest.mock("fs-extra", () => ({

describe("storybook/story-to-test/write-tests-file", () => {
it("should write test file with correct content", async () => {
const opts = { autoScreenshots: true };
const opts = { autoScreenshots: true, autoScreenshotStorybookGlobals: { foo: { bar: "baz" } } };
const stories = [{ id: "foo" }, { id: "bar" }] as StorybookStoryExtended[];
const testFile = "/absolute/test/path/file.testplane.js";
const expectedContents = `
const stories = [{"id":"foo"},{"id":"bar"}];
const storyTestRunnerPath = "/absolute/story/runner/path";
const testplaneOpts = {"autoScreenshots":true};
const testplaneOpts = {"autoScreenshots":true,"autoScreenshotStorybookGlobals":{"foo":{"bar":"baz"}}};
require(storyTestRunnerPath).run(stories, testplaneOpts);
`;
Expand Down
2 changes: 1 addition & 1 deletion src/storybook/story-to-test/write-tests-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { StorybookStoryExtended } from "../get-stories";

export interface TestplaneOpts {
autoScreenshots: boolean;
customAutoScreenshots: Record<string, { globals: Record<string, unknown>}>;
autoScreenshotStorybookGlobals: Record<string, Record<string, unknown>>;
}

interface TestFileContent {
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type TestplaneMetaConfig<T = void> = Combined<
assertViewOpts?: AssertViewOpts;
browserIds?: Array<string | RegExp>;
autoscreenshotSelector?: string;
autoScreenshotStorybookGlobals?: Record<string, Record<string, unknown>>;
}>,
T
>;
Expand Down

0 comments on commit 6364eca

Please sign in to comment.