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 05b5365
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 63 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
58 changes: 30 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,34 @@ 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
? story.autoScreenshotStorybookGlobals
: autoScreenshotStorybookGlobals;

const screenshotGlobalSetNames = Object.keys(rawAutoScreenshotGlobalSets);

const autoScreenshotGlobalSets = screenshotGlobalSetNames.length
? screenshotGlobalSetNames.map(name => ({ name, globals: rawAutoScreenshotGlobalSets[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 +64,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
79 changes: 65 additions & 14 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,8 +13,17 @@ interface HTMLElement {

export type StorybookWindow = Window &
typeof globalThis & {
__HERMIONE_OPEN_STORY__: (storyId: string, remountOnly: boolean, done: (result: string) => void) => void;
__STORYBOOK_ADDONS_CHANNEL__: EventEmitter;
__HERMIONE_OPEN_STORY__: (
storyId: string,
storybookGlobals: Record<string, unknown>,
remountOnly: boolean,
done: (result: string) => void,
) => void;
__TESTPLANE_STORYBOOK_INITIAL_GLOBALS__?: Record<string, unknown>;
__STORYBOOK_PREVIEW__?: { storeInitializationPromise?: Promise<void> };
__STORYBOOK_ADDONS_CHANNEL__: EventEmitter & {
data?: { setGlobals?: Array<{ globals: Record<string, unknown> }> };
};
__STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER_STATE__: Record<
string,
{
Expand All @@ -34,10 +43,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 +68,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 All @@ -81,6 +96,7 @@ function openStoryScript(storyId: string, shouldRemount: boolean, done: (result:
channel.off("storyMissing", onStoryMissing);
channel.off("storyThrewException", onStoryThrewException);
channel.off("storyErrored", onStoryErrored);
channel.off("globalsUpdated", onGlobalsUpdated);

done(JSON.stringify(value));
}
Expand Down Expand Up @@ -140,23 +156,58 @@ function openStoryScript(storyId: string, shouldRemount: boolean, done: (result:
doneJson(result);
}

channel.once("playFunctionThrewException", onPlayFunctionThrewException);
channel.once("storyRendered", onStoryRendered);
channel.once("storyMissing", onStoryMissing);
channel.once("storyThrewException", onStoryThrewException);
channel.once("storyErrored", onStoryErrored);
function onGlobalsUpdated(): void {
channel.once("playFunctionThrewException", onPlayFunctionThrewException);
channel.once("storyRendered", onStoryRendered);
channel.once("storyMissing", onStoryMissing);
channel.once("storyThrewException", onStoryThrewException);
channel.once("storyErrored", onStoryErrored);

if (shouldRemount) {
channel.emit("setCurrentStory", { storyId: "" });
}

channel.emit("setCurrentStory", { storyId });
}

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

if (shouldRemount) {
channel.emit("setCurrentStory", { storyId: "" });
doneJson(result);
}

channel.emit("setCurrentStory", { storyId });
const storybookPreview = (window as StorybookWindow).__STORYBOOK_PREVIEW__;
const isStorybookPreviewAvailable = storybookPreview && storybookPreview.storeInitializationPromise;
const shouldUpdateStorybookGlobals = storybookGlobals && isStorybookPreviewAvailable;

if (shouldUpdateStorybookGlobals) {
(storybookPreview.storeInitializationPromise as Promise<void>).then(function () {
let defaultGlobals = (window as StorybookWindow).__TESTPLANE_STORYBOOK_INITIAL_GLOBALS__;

if (!defaultGlobals) {
const setGlobalCalls = (window as StorybookWindow).__STORYBOOK_ADDONS_CHANNEL__.data?.setGlobals;
const initValue = (setGlobalCalls && setGlobalCalls[0].globals) || {};

defaultGlobals = (window as StorybookWindow).__TESTPLANE_STORYBOOK_INITIAL_GLOBALS__ = initValue;
}

channel.emit("updateGlobals", { globals: Object.assign({}, defaultGlobals, storybookGlobals) });
channel.once("globalsUpdated", onGlobalsUpdated);
});
} else {
onGlobalsUpdated();
}
});
}

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
Loading

0 comments on commit 05b5365

Please sign in to comment.