From 53e02a28861251bc4eedbd4d471fab81874d1c06 Mon Sep 17 00:00:00 2001 From: Voytenok Artur Date: Mon, 21 Oct 2024 16:19:24 +0300 Subject: [PATCH 1/2] feat: ability to generate multiple sets of autotests --- src/config.ts | 8 ++++- src/index.ts | 2 +- src/storybook/story-test-runner/index.ts | 35 +++++++++++++++---- .../story-test-runner/open-story/index.ts | 1 + .../open-story/testplane-open-story.ts | 2 +- src/storybook/story-to-test/index.ts | 1 + .../story-to-test/write-tests-file.ts | 1 + 7 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/config.ts b/src/config.ts index c3f63ce..c9b22da 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,5 @@ import { isBoolean, isString, isNumber, isArray, isRegExp } from "lodash"; -import { option, root, section } from "gemini-configparser"; +import { option, root, section, map } from "gemini-configparser"; import type { Parser } from "gemini-configparser"; const assertType = (name: string, validationFn: (v: unknown) => boolean, type: string) => { @@ -52,6 +52,7 @@ export interface PluginConfig { enabled: boolean; storybookConfigDir: string; autoScreenshots: boolean; + customAutoScreenshots: Record}>; localport: number; remoteStorybookUrl: string; browserIds: Array; @@ -68,6 +69,11 @@ 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", ""), + }), + })), localport: numberOption("localport", 6006), remoteStorybookUrl: stringOption("remoteStorybookUrl", ""), browserIds: stringAndRegExpArrayOption("browserIds", []), diff --git a/src/index.ts b/src/index.ts index face646..ab8fef5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -78,7 +78,7 @@ function onTestplaneMaster(testplane: Testplane, config: PluginConfig): void { const stories = await getStories(storybookUrl); - const storyTestFiles = await buildStoryTestFiles(stories, { autoScreenshots: config.autoScreenshots }); + const storyTestFiles = await buildStoryTestFiles(stories, { autoScreenshots: config.autoScreenshots, customAutoScreenshots: config.customAutoScreenshots }); patchTestplaneBaseUrl(testplane.config, iframeUrl); disableTestplaneIsolation(testplane.config, config.browserIds); diff --git a/src/storybook/story-test-runner/index.ts b/src/storybook/story-test-runner/index.ts index 5298423..72df38b 100644 --- a/src/storybook/story-test-runner/index.ts +++ b/src/storybook/story-test-runner/index.ts @@ -1,6 +1,6 @@ import { extendStoriesFromStoryFile } from "./extend-stories"; import { nestedDescribe, extendedIt } from "./test-decorators"; -import { openStory } from "./open-story"; +import { openStory, StorybookWindow } from "./open-story"; import type { StoryLoadResult } from "./open-story/testplane-open-story"; import type { TestplaneOpts } from "../story-to-test"; import type { TestFunctionExtendedCtx } from "../../types"; @@ -16,16 +16,28 @@ export function run(stories: StorybookStory[], opts: TestplaneOpts): void { withStoryFileDataStories.forEach(story => createTestplaneTests(story, opts)); } -function createTestplaneTests(story: StorybookStoryExtended, { autoScreenshots }: TestplaneOpts): void { +function createTestplaneTests(story: StorybookStoryExtended, { autoScreenshots, customAutoScreenshots }: TestplaneOpts): void { nestedDescribe(story, () => { if (autoScreenshots) { - extendedIt(story, "Autoscreenshot", async function (ctx: TestFunctionExtendedCtx) { - ctx.expect = globalThis.expect; + if (customAutoScreenshots) { + for (const testName in customAutoScreenshots) { + extendedIt(story, testName, async function (ctx: TestFunctionExtendedCtx) { + ctx.expect = globalThis.expect; - const result = await openStoryStep(ctx.browser, story); + 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); - await autoScreenshotStep(ctx.browser, story.autoscreenshotSelector || result.rootSelector); - }); + await autoScreenshotStep(ctx.browser, story.autoscreenshotSelector || result.rootSelector); + }); + } } if (story.extraTests) { @@ -49,6 +61,15 @@ async function openStoryStep(browser: WebdriverIO.Browser, story: StorybookStory return browser.runStep("@testplane/storybook: open story", () => openStory(browser, story)); } +async function setGlobalsStep(browser: WebdriverIO.Browser, globals: Record): Promise { + 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 autoScreenshotStep(browser: WebdriverIO.Browser, rootSelector: string): Promise { await browser.runStep("@testplane/storybook: autoscreenshot", () => browser.assertView("plain", rootSelector)); } diff --git a/src/storybook/story-test-runner/open-story/index.ts b/src/storybook/story-test-runner/open-story/index.ts index f803f87..0617ead 100644 --- a/src/storybook/story-test-runner/open-story/index.ts +++ b/src/storybook/story-test-runner/open-story/index.ts @@ -1,5 +1,6 @@ import { PlayFunctionError } from "../play-function-error"; import testplaneOpenStory from "./testplane-open-story"; +export type { StorybookWindow } from "./testplane-open-story"; import type { ExecutionContextExtended, StorybookStoryExtended } from "../types"; import type { StoryLoadResult } from "./testplane-open-story"; diff --git a/src/storybook/story-test-runner/open-story/testplane-open-story.ts b/src/storybook/story-test-runner/open-story/testplane-open-story.ts index 293f8a7..32a8e84 100644 --- a/src/storybook/story-test-runner/open-story/testplane-open-story.ts +++ b/src/storybook/story-test-runner/open-story/testplane-open-story.ts @@ -11,7 +11,7 @@ interface HTMLElement { innerText: string; } -type StorybookWindow = Window & +export type StorybookWindow = Window & typeof globalThis & { __HERMIONE_OPEN_STORY__: (storyId: string, remountOnly: boolean, done: (result: string) => void) => void; __STORYBOOK_ADDONS_CHANNEL__: EventEmitter; diff --git a/src/storybook/story-to-test/index.ts b/src/storybook/story-to-test/index.ts index 2fb9f6a..487af0d 100644 --- a/src/storybook/story-to-test/index.ts +++ b/src/storybook/story-to-test/index.ts @@ -9,6 +9,7 @@ import type { StorybookStoryExtended } from "../get-stories"; export interface TestplaneOpts { autoScreenshots: boolean; + customAutoScreenshots: Record}>; } const testplaneTestNameSuffix = ".testplane.js"; diff --git a/src/storybook/story-to-test/write-tests-file.ts b/src/storybook/story-to-test/write-tests-file.ts index 02fe880..caf5783 100644 --- a/src/storybook/story-to-test/write-tests-file.ts +++ b/src/storybook/story-to-test/write-tests-file.ts @@ -5,6 +5,7 @@ import { StorybookStoryExtended } from "../get-stories"; export interface TestplaneOpts { autoScreenshots: boolean; + customAutoScreenshots: Record}>; } interface TestFileContent { From 63eb93c8d1980f5ea57468c07da31777ab10c945 Mon Sep 17 00:00:00 2001 From: Roman Kuznetsov Date: Wed, 6 Nov 2024 04:16:49 +0300 Subject: [PATCH 2/2] fix: apply edits --- README.md | 46 ++++++++--- src/config.ts | 37 +++++++-- src/index.ts | 5 +- .../story-test-runner/extend-stories.test.ts | 4 +- .../story-test-runner/extend-stories.ts | 4 + src/storybook/story-test-runner/index.ts | 59 +++++++------- .../story-test-runner/open-story/index.ts | 8 +- .../open-story/testplane-open-story.ts | 80 +++++++++++++++---- src/storybook/story-test-runner/types.ts | 1 + src/storybook/story-to-test/index.test.ts | 16 ++-- src/storybook/story-to-test/index.ts | 2 +- .../story-to-test/write-tests-file.test.ts | 4 +- .../story-to-test/write-tests-file.ts | 2 +- src/types.ts | 1 + 14 files changed, 196 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 24f692f..eb017bc 100644 --- a/README.md +++ b/README.md @@ -52,25 +52,45 @@ With this minimal config, you will be able to run `npx testplane --storybook` to Full plugin config: -| **Parameter** | **Type** | **Default value** | **Description** | -| ------------------ | ----------------------- | ---------------------- | --------------------------------------------------------------------------- | -| enabled | Boolean | true | Enable / disable the plugin | -| storybookConfigDir | String | ".storybook" | Path to the storybook configuration directory | -| autoScreenshots | Boolean | true | Enable / disable auto-screenshot tests | -| localport | Number | 6006 | Port to launch storybook dev server on | -| remoteStorybookUrl | String | "" | URL of the remote Storybook. If specified, local storybook dev sever would not be launched | -| browserIds | Array | [] | Array of `browserId` to run storybook tests on. By default, all of browsers, specified in Testplane config would be used | +| **Parameter** | **Type** | **Default value** | **Description** | +| ---------------------------------- | --------------------------------------- | ---------------------- | --------------------------------------------------------------------------- | +| enabled | Boolean | true | Enable / disable the plugin | +| storybookConfigDir | String | ".storybook" | Path to the storybook configuration directory | +| autoScreenshots | Boolean | true | Enable / disable auto-screenshot tests | +| autoScreenshotStorybookGlobals | Record> | {} | Run multiple auto-screenshot tests with different [storybook globals](https://storybook.js.org/docs/7/essentials/toolbars-and-globals#globals) | +| localport | Number | 6006 | Port to launch storybook dev server on | +| remoteStorybookUrl | String | "" | URL of the remote Storybook. If specified, local storybook dev sever would not be launched | +| browserIds | Array | [] | Array of `browserId` to run storybook tests on. By default, all of browsers, specified in Testplane config would be used | > ⚠️ *Storybook tests performance greatly depends on [Testplane testsPerSession](https://github.com/gemini-testing/testplane#testspersession) parameter, as these tests speeds up on reusing existing sessions, so setting values around 20+ is preferred* > ⚠️ *These tests ignore [Testplane isolation](https://github.com/gemini-testing/testplane#isolation). It would be turned off unconditionally* +#### autoScreenshotStorybookGlobals + +For example, with `autoScreenshotStorybookGlobals` set to: + +```json +{ + "default": {}, + "light theme": { + "theme": "light" + }, + "dark theme": { + "theme": "dark" + } +} +``` + +3 autoscreenshot tests will be generated for each story, each test having its corresponding storybook globals value: +- `... Autoscreenshot default` +- `... Autoscreenshot light theme` +- `... Autoscreenshot dark theme` + ## Advanced usage If you have `ts-node` in your project, you can write your Testplane tests right inside of storybook story files: -> ⚠️ *Storybook story files must have `.js` or `.ts` extension for this to work* - ```ts import type { StoryObj } from "@storybook/react"; import type { WithTestplane } from "@testplane/storybook" @@ -103,6 +123,12 @@ const meta: WithTestplane> = { skip: false, // if true, skips all Testplane tests from this story file autoscreenshotSelector: ".my-selector", // Custom selector to auto-screenshot elements browserIds: ["chrome"], // Testplane browsers to run tests from this story file + autoScreenshotStorybookGlobals: { + // override default autoScreenshotStorybookGlobals options from plugin config + // tests for default autoScreenshotStorybookGlobals from plugin config won't be generated + "es locale": { locale: "es" }, + "fr locale": { locale: "fr" } + }, assertViewOpts: { // override default assertView options for tests from this file ignoreDiffPixelCount: 5 } diff --git a/src/config.ts b/src/config.ts index c9b22da..9e7021b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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 = (name: string, validationFn: (v: unknown) => boolean, type: string) => { @@ -10,6 +10,20 @@ const assertType = (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; + + 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 => option({ parseEnv: (val: string) => Boolean(JSON.parse(val)), @@ -48,11 +62,22 @@ const stringAndRegExpArrayOption = (name: string, defaultValue: string[]): Parse }, }); +const optionalRecordOfRecordsOption = ( + name: string, + defaultValue: Record = {}, +): Parser>> => + option({ + parseEnv: JSON.parse, + parseCli: JSON.parse, + defaultValue, + validate: value => assertRecordOfRecords(value, name), + }); + export interface PluginConfig { enabled: boolean; storybookConfigDir: string; autoScreenshots: boolean; - customAutoScreenshots: Record}>; + autoScreenshotStorybookGlobals: Record>; localport: number; remoteStorybookUrl: string; browserIds: Array; @@ -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", []), diff --git a/src/index.ts b/src/index.ts index ab8fef5..3c4fa7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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); diff --git a/src/storybook/story-test-runner/extend-stories.test.ts b/src/storybook/story-test-runner/extend-stories.test.ts index ba33a55..cfd3acf 100644 --- a/src/storybook/story-test-runner/extend-stories.test.ts +++ b/src/storybook/story-test-runner/extend-stories.test.ts @@ -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); }); diff --git a/src/storybook/story-test-runner/extend-stories.ts b/src/storybook/story-test-runner/extend-stories.ts index 7240cb9..b0522b5 100644 --- a/src/storybook/story-test-runner/extend-stories.ts +++ b/src/storybook/story-test-runner/extend-stories.ts @@ -23,6 +23,7 @@ export function extendStoriesFromStoryFile(stories: StorybookStory[]): Storybook story.skip = false; story.assertViewOpts = {}; story.browserIds = null; + story.autoScreenshotStorybookGlobals = {}; return story; }); @@ -37,6 +38,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; @@ -86,6 +88,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); diff --git a/src/storybook/story-test-runner/index.ts b/src/storybook/story-test-runner/index.ts index 72df38b..dfbbd5b 100644 --- a/src/storybook/story-test-runner/index.ts +++ b/src/storybook/story-test-runner/index.ts @@ -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"; @@ -16,27 +16,35 @@ 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 = { + ...autoScreenshotStorybookGlobals, + ...story.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); + }, + ); } } @@ -57,17 +65,12 @@ function createTestplaneTests(story: StorybookStoryExtended, { autoScreenshots, }); } -async function openStoryStep(browser: WebdriverIO.Browser, story: StorybookStoryExtended): Promise { - return browser.runStep("@testplane/storybook: open story", () => openStory(browser, story)); -} - -async function setGlobalsStep(browser: WebdriverIO.Browser, globals: Record): Promise { - 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 = {}, +): Promise { + return browser.runStep("@testplane/storybook: open story", () => openStory(browser, story, storybookGlobals)); } async function autoScreenshotStep(browser: WebdriverIO.Browser, rootSelector: string): Promise { diff --git a/src/storybook/story-test-runner/open-story/index.ts b/src/storybook/story-test-runner/open-story/index.ts index 0617ead..b922b0b 100644 --- a/src/storybook/story-test-runner/open-story/index.ts +++ b/src/storybook/story-test-runner/open-story/index.ts @@ -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 { +export async function openStory( + browser: WebdriverIO.Browser, + story: StorybookStoryExtended, + storybookGlobals: Record, +): Promise { const browserConfig = await browser.getConfig(); const currentBrowserUrl = await browser.getUrl(); @@ -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 => { - 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); diff --git a/src/storybook/story-test-runner/open-story/testplane-open-story.ts b/src/storybook/story-test-runner/open-story/testplane-open-story.ts index 32a8e84..1347683 100644 --- a/src/storybook/story-test-runner/open-story/testplane-open-story.ts +++ b/src/storybook/story-test-runner/open-story/testplane-open-story.ts @@ -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, + remountOnly: boolean, + done: (result: string) => void, + ) => void; + __TESTPLANE_STORYBOOK_INITIAL_GLOBALS__?: Record; + __STORYBOOK_PREVIEW__?: { storeInitializationPromise?: Promise }; + __STORYBOOK_ADDONS_CHANNEL__: EventEmitter & { + data?: { setGlobals?: Array<{ globals: Record }> }; + }; __STORYBOOK_ADDON_INTERACTIONS_INSTRUMENTER_STATE__: Record< string, { @@ -34,10 +43,11 @@ export async function inject(browser: WebdriverIO.Browser): Promise { export async function execute( browser: WebdriverIO.Browser, storyId: string, + storybookGlobals: Record, shouldRemount: boolean, ): Promise { const getResult = (): Promise => - browser.executeAsync(executeOpenStoryScript, storyId, shouldRemount).then(JSON.parse); + browser.executeAsync(executeOpenStoryScript, storyId, storybookGlobals, shouldRemount).then(JSON.parse); const result: StoryLoadResult = await getResult(); @@ -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, + shouldRemount: boolean, + done: (result: string) => void, +): void { function onPageLoad(fn: () => void): void { if (document.readyState === "complete") { fn(); @@ -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)); } @@ -140,23 +156,59 @@ 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: "" }); + if (shouldRemount) { + channel.emit("setCurrentStory", { storyId: "" }); + } + + 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"; + + doneJson(result); + } + + const storybookPreview = (window as StorybookWindow).__STORYBOOK_PREVIEW__; + const isStorybookPreviewAvailable = storybookPreview && storybookPreview.storeInitializationPromise; + const shouldUpdateStorybookGlobals = storybookGlobals && isStorybookPreviewAvailable; + + if (shouldUpdateStorybookGlobals) { + (storybookPreview.storeInitializationPromise as Promise).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.once("globalsUpdated", onGlobalsUpdated); + + channel.emit("updateGlobals", { globals: Object.assign({}, defaultGlobals, storybookGlobals) }); + }); + } else { + onGlobalsUpdated(); + } }); } -function executeOpenStoryScript(storyId: string, remountOnly: boolean, done: (result: string) => void): void { +function executeOpenStoryScript( + storyId: string, + storybookGlobals: Record, + 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 })); } diff --git a/src/storybook/story-test-runner/types.ts b/src/storybook/story-test-runner/types.ts index f4ce3c0..e48a7ae 100644 --- a/src/storybook/story-test-runner/types.ts +++ b/src/storybook/story-test-runner/types.ts @@ -8,6 +8,7 @@ export interface StorybookStoryExtended extends StorybookStory { browserIds: Array | null; extraTests?: Record; autoscreenshotSelector: string | null; + autoScreenshotStorybookGlobals: Record>; } export type ExecutionContextExtended = WebdriverIO.Browser["executionContext"] & { diff --git a/src/storybook/story-to-test/index.test.ts b/src/storybook/story-to-test/index.test.ts index 4005b9c..eeb02cb 100644 --- a/src/storybook/story-to-test/index.test.ts +++ b/src/storybook/story-to-test/index.test.ts @@ -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(); }); @@ -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], }); diff --git a/src/storybook/story-to-test/index.ts b/src/storybook/story-to-test/index.ts index 487af0d..6a228ee 100644 --- a/src/storybook/story-to-test/index.ts +++ b/src/storybook/story-to-test/index.ts @@ -9,7 +9,7 @@ import type { StorybookStoryExtended } from "../get-stories"; export interface TestplaneOpts { autoScreenshots: boolean; - customAutoScreenshots: Record}>; + autoScreenshotStorybookGlobals: Record>; } const testplaneTestNameSuffix = ".testplane.js"; diff --git a/src/storybook/story-to-test/write-tests-file.test.ts b/src/storybook/story-to-test/write-tests-file.test.ts index ca1fbb3..3d9a2de 100644 --- a/src/storybook/story-to-test/write-tests-file.test.ts +++ b/src/storybook/story-to-test/write-tests-file.test.ts @@ -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); `; diff --git a/src/storybook/story-to-test/write-tests-file.ts b/src/storybook/story-to-test/write-tests-file.ts index caf5783..0945f64 100644 --- a/src/storybook/story-to-test/write-tests-file.ts +++ b/src/storybook/story-to-test/write-tests-file.ts @@ -5,7 +5,7 @@ import { StorybookStoryExtended } from "../get-stories"; export interface TestplaneOpts { autoScreenshots: boolean; - customAutoScreenshots: Record}>; + autoScreenshotStorybookGlobals: Record>; } interface TestFileContent { diff --git a/src/types.ts b/src/types.ts index 7df3030..31babe6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,6 +23,7 @@ export type TestplaneMetaConfig = Combined< assertViewOpts?: AssertViewOpts; browserIds?: Array; autoscreenshotSelector?: string; + autoScreenshotStorybookGlobals?: Record>; }>, T >;