diff --git a/.changeset/fix-clear-runtime-folder.md b/.changeset/fix-clear-runtime-folder.md new file mode 100644 index 000000000..b9e260386 --- /dev/null +++ b/.changeset/fix-clear-runtime-folder.md @@ -0,0 +1,10 @@ +--- +"@wdio/visual-service": patch +"@wdio/image-comparison-core": patch +--- + +Fix `clearRuntimeFolder` clearing the actual and diff folders after each spec/feature execution instead of once before all workers start. This caused only the last spec's visual data to be present in the output when running multiple specs. + +# Committers: 1 + +- Wim Selles ([@wswebcreation](https://github.com/wswebcreation)) diff --git a/packages/image-comparison-core/src/base.test.ts b/packages/image-comparison-core/src/base.test.ts index a81b3a123..878f0b952 100644 --- a/packages/image-comparison-core/src/base.test.ts +++ b/packages/image-comparison-core/src/base.test.ts @@ -45,12 +45,9 @@ describe('BaseClass', () => { expect(instance.folders.actualFolder).toContain('functional/screenshots') }) - it('clears runtime folders if clearRuntimeFolder is true', () => { - const options = { - clearRuntimeFolder: true, - } - new BaseClass(options) - - expect(rmSync).toHaveBeenCalledTimes(2) + it('should not clear runtime folders in the constructor - clearing should only happen once in the launcher (issue #683)', () => { + vi.mocked(rmSync).mockClear() + new BaseClass({ clearRuntimeFolder: true }) + expect(rmSync).not.toHaveBeenCalled() }) }) diff --git a/packages/image-comparison-core/src/base.ts b/packages/image-comparison-core/src/base.ts index 4a9650077..4a7a0fd42 100644 --- a/packages/image-comparison-core/src/base.ts +++ b/packages/image-comparison-core/src/base.ts @@ -18,11 +18,6 @@ export default class BaseClass { // Setup folder structure this.folders = this._setupFolders(options) - - // Clear runtime folders if requested - if (options.clearRuntimeFolder) { - this._clearRuntimeFolders() - } } /** @@ -47,9 +42,9 @@ export default class BaseClass { /** * Clear the runtime folders (actual and diff) - * @private + * @protected */ - private _clearRuntimeFolders(): void { + protected _clearRuntimeFolders(): void { log.info('\x1b[33m\n##############################\n!!CLEARING RUNTIME FOLDERS!!\n##############################\x1b[0m') try { diff --git a/packages/visual-service/src/index.ts b/packages/visual-service/src/index.ts index ed6c0f6ae..7bb0c64a1 100644 --- a/packages/visual-service/src/index.ts +++ b/packages/visual-service/src/index.ts @@ -1,6 +1,6 @@ import type { WicElement } from '@wdio/image-comparison-core' import WdioImageComparisonService from './service.js' -import VisualLauncher from './storybook/launcher.js' +import VisualLauncher from './launcher.js' import type { Output, Result, diff --git a/packages/visual-service/src/launcher.ts b/packages/visual-service/src/launcher.ts new file mode 100644 index 000000000..c5c3d0485 --- /dev/null +++ b/packages/visual-service/src/launcher.ts @@ -0,0 +1,32 @@ +import type { Capabilities } from '@wdio/types' +import type { ClassOptions } from '@wdio/image-comparison-core' +import { BaseClass } from '@wdio/image-comparison-core' +import { prepareStorybook, cleanupStorybook } from './storybook/hooks.js' +import generateVisualReport from './reporter.js' + +export default class VisualLauncher extends BaseClass { + #options: ClassOptions + + constructor(options: ClassOptions) { + super(options) + this.#options = options + } + + async onPrepare(config: WebdriverIO.Config, capabilities: Capabilities.TestrunnerCapabilities) { + if (this.#options.clearRuntimeFolder) { + this._clearRuntimeFolders() + } + + await prepareStorybook(config, capabilities, this.#options, this.folders) + } + + async onComplete() { + cleanupStorybook() + + if (this.#options.createJsonReportFiles) { + new generateVisualReport( + { directoryPath: this.folders.actualFolder } + ).generate() + } + } +} diff --git a/packages/visual-service/src/storybook/hooks.ts b/packages/visual-service/src/storybook/hooks.ts new file mode 100644 index 000000000..ae4d6c71f --- /dev/null +++ b/packages/visual-service/src/storybook/hooks.ts @@ -0,0 +1,129 @@ +import { rmdirSync } from 'node:fs' +import logger from '@wdio/logger' +import { SevereServiceError } from 'webdriverio' +import type { Capabilities } from '@wdio/types' +import type { ClassOptions, CheckElementMethodOptions, Folders } from '@wdio/image-comparison-core' +import { + createStorybookCapabilities, + createTestFiles, + getArgvValue, + isCucumberFramework, + isStorybookMode, + parseSkipStories, + scanStorybook, +} from './utils.js' +import { CLIP_SELECTOR, NUM_SHARDS, V6_CLIP_SELECTOR } from '../constants.js' + +const log = logger('@wdio/visual-service') + +export async function prepareStorybook( + config: WebdriverIO.Config, + capabilities: Capabilities.TestrunnerCapabilities, + options: ClassOptions, + folders: Folders, +): Promise { + const isStorybook = isStorybookMode() + const framework = config.framework as string + const isCucumber = isCucumberFramework(framework) + + if (isCucumber && isStorybook) { + throw new SevereServiceError('\n\nRunning Storybook in combination with the cucumber framework adapter is not supported.\nOnly Jasmine and Mocha are supported.\n\n') + } + + if (!isStorybook) { + return + } + + log.info('Running `@wdio/visual-service` in Storybook mode.') + + const { storiesJson, storybookUrl, tempDir } = await scanStorybook(config, options) + process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER = tempDir + process.env.VISUAL_STORYBOOK_URL = storybookUrl + + if (typeof capabilities === 'object' && !Array.isArray(capabilities)) { + throw new SevereServiceError('\n\nRunning Storybook in combination with Multiremote is not supported.\nRemove your `capabilities` property from your config or assign an empty array to it like `capabilities: [],`.\n\n') + } + + capabilities.length = 0 + log.info('Clearing the current capabilities.') + + const compareOptions: CheckElementMethodOptions = { + blockOutSideBar: options.blockOutSideBar, + blockOutStatusBar: options.blockOutStatusBar, + blockOutToolBar: options.blockOutToolBar, + ignoreAlpha: options.ignoreAlpha, + ignoreAntialiasing: options.ignoreAntialiasing, + ignoreColors: options.ignoreColors, + ignoreLess: options.ignoreLess, + ignoreNothing: options.ignoreNothing, + rawMisMatchPercentage: options.rawMisMatchPercentage, + returnAllCompareData: options.returnAllCompareData, + saveAboveTolerance: options.saveAboveTolerance, + scaleImagesToSameSize: options.scaleImagesToSameSize, + } + + // --version + const versionOption = options?.storybook?.version + const versionArgv = getArgvValue('--version', value => Math.floor(parseFloat(value))) + const version = versionOption ?? versionArgv ?? 7 + // --numShards + const maxInstances = config?.maxInstances ?? 1 + const numShardsOption = options?.storybook?.numShards + const numShardsArgv = getArgvValue('--numShards', value => parseInt(value, 10)) + const numShards = Math.min(numShardsOption || numShardsArgv || NUM_SHARDS, maxInstances) + // --clip + const clipOption = options?.storybook?.clip + const clipArgv = getArgvValue('--clip', value => value !== 'false') + const clip = clipOption ?? clipArgv ?? true + // --clipSelector + const clipSelectorOption = options?.storybook?.clipSelector + const clipSelectorArgv = getArgvValue('--clipSelector', value => value) + const clipSelector = (clipSelectorOption ?? clipSelectorArgv) ?? (version === 6 ? V6_CLIP_SELECTOR : CLIP_SELECTOR) + process.env.VISUAL_STORYBOOK_CLIP_SELECTOR = clipSelector + // --skipStories + const skipStoriesOption = options?.storybook?.skipStories + const skipStoriesArgv = getArgvValue('--skipStories', value => value) + const skipStories = skipStoriesOption ?? skipStoriesArgv ?? [] + const parsedSkipStories = parseSkipStories(skipStories) + // --additionalSearchParams + const additionalSearchParamsOption = options?.storybook?.additionalSearchParams + const additionalSearchParamsArgv = getArgvValue('--additionalSearchParams', value => new URLSearchParams(value)) + const additionalSearchParams = additionalSearchParamsOption ?? additionalSearchParamsArgv ?? new URLSearchParams() + const getStoriesBaselinePath = options?.storybook?.getStoriesBaselinePath + + createTestFiles({ + additionalSearchParams, + clip, + clipSelector, + compareOptions, + directoryPath: tempDir, + folders, + framework, + getStoriesBaselinePath, + numShards, + skipStories: parsedSkipStories, + storiesJson, + storybookUrl, + }) + + createStorybookCapabilities(capabilities as WebdriverIO.Capabilities[]) +} + +export function cleanupStorybook(): void { + const tempDir = process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER + if (!tempDir) { + return + } + + log.info(`Cleaning up temporary folder for storybook specs: ${tempDir}`) + try { + rmdirSync(tempDir, { recursive: true }) + log.info(`Temporary folder for storybook specs has been removed: ${tempDir}`) + } catch (err) { + log.error(`Failed to remove temporary folder for storybook specs: ${tempDir} due to: ${(err as Error).message}`) + } + + delete process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER + delete process.env.VISUAL_STORYBOOK_URL + delete process.env.VISUAL_STORYBOOK_CLIP_SELECTOR +} diff --git a/packages/visual-service/src/storybook/launcher.ts b/packages/visual-service/src/storybook/launcher.ts deleted file mode 100644 index acb278e96..000000000 --- a/packages/visual-service/src/storybook/launcher.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { rmdirSync } from 'node:fs' -import logger from '@wdio/logger' -import { SevereServiceError } from 'webdriverio' -import type { Capabilities } from '@wdio/types' -import type { ClassOptions, CheckElementMethodOptions } from '@wdio/image-comparison-core' -import { BaseClass } from '@wdio/image-comparison-core' -import { - createStorybookCapabilities, - createTestFiles, - getArgvValue, - isCucumberFramework, - isStorybookMode, - parseSkipStories, - scanStorybook, -} from './utils.js' -import { CLIP_SELECTOR, NUM_SHARDS, V6_CLIP_SELECTOR } from '../constants.js' -import generateVisualReport from '../reporter.js' - -const log = logger('@wdio/visual-service') - -export default class VisualLauncher extends BaseClass { - #options: ClassOptions - - constructor(options: ClassOptions) { - super(options) - this.#options = options - } - - async onPrepare (config: WebdriverIO.Config, capabilities: Capabilities.TestrunnerCapabilities) { - const isStorybook = isStorybookMode() - const framework = config.framework as string - const isCucumber = isCucumberFramework(framework) - - if (isCucumber && isStorybook) { - throw new SevereServiceError('\n\nRunning Storybook in combination with the cucumber framework adapter is not supported.\nOnly Jasmine and Mocha are supported.\n\n') - } else if (isStorybook) { - log.info('Running `@wdio/visual-service` in Storybook mode.') - - const { storiesJson, storybookUrl, tempDir } = await scanStorybook(config, this.#options) - // Set an environment variable so it can be used in the onComplete hook - process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER = tempDir - // Add the storybook URL to the environment variables - process.env.VISUAL_STORYBOOK_URL = storybookUrl - - // Check the capabilities - // Multiremote capabilities are not supported - if (typeof capabilities === 'object' && !Array.isArray(capabilities)) { - throw new SevereServiceError('\n\nRunning Storybook in combination with Multiremote is not supported.\nRemove your `capabilities` property from your config or assign an empty array to it like `capabilities: [],`.\n\n') - } - - // Clear the capabilities - capabilities.length = 0 - log.info('Clearing the current capabilities.') - - // Get compare options from config - const compareOptions: CheckElementMethodOptions = { - blockOutSideBar: this.#options.blockOutSideBar, - blockOutStatusBar: this.#options.blockOutStatusBar, - blockOutToolBar: this.#options.blockOutToolBar, - ignoreAlpha: this.#options.ignoreAlpha, - ignoreAntialiasing: this.#options.ignoreAntialiasing, - ignoreColors: this.#options.ignoreColors, - ignoreLess: this.#options.ignoreLess, - ignoreNothing: this.#options.ignoreNothing, - rawMisMatchPercentage: this.#options.rawMisMatchPercentage, - returnAllCompareData: this.#options.returnAllCompareData, - saveAboveTolerance: this.#options.saveAboveTolerance, - scaleImagesToSameSize: this.#options.scaleImagesToSameSize, - } - - // Determine some run options - // --version - const versionOption = this.#options?.storybook?.version - const versionArgv = getArgvValue('--version', value => Math.floor(parseFloat(value))) - const version = versionOption ?? versionArgv ?? 7 - // --numShards - const maxInstances = config?.maxInstances ?? 1 - const numShardsOption = this.#options?.storybook?.numShards - const numShardsArgv = getArgvValue('--numShards', value => parseInt(value, 10)) - const numShards = Math.min(numShardsOption || numShardsArgv || NUM_SHARDS, maxInstances) - // --clip - const clipOption = this.#options?.storybook?.clip - const clipArgv = getArgvValue('--clip', value => value !== 'false') - const clip = clipOption ?? clipArgv ?? true - // --clipSelector - const clipSelectorOption = this.#options?.storybook?.clipSelector - const clipSelectorArgv = getArgvValue('--clipSelector', value => value) - // V6 has '#root' as the root element, V7 has '#storybook-root' - const clipSelector = (clipSelectorOption ?? clipSelectorArgv) ?? (version === 6 ? V6_CLIP_SELECTOR : CLIP_SELECTOR) - // Add the clip selector to the environment variables - process.env.VISUAL_STORYBOOK_CLIP_SELECTOR = clipSelector - // --skipStories - const skipStoriesOption = this.#options?.storybook?.skipStories - const skipStoriesArgv = getArgvValue('--skipStories', value => value) - const skipStories = skipStoriesOption ?? skipStoriesArgv ?? [] - const parsedSkipStories = parseSkipStories(skipStories) - // --additionalSearchParams - const additionalSearchParamsOption = this.#options?.storybook?.additionalSearchParams - const additionalSearchParamsArgv = getArgvValue('--additionalSearchParams', value => new URLSearchParams(value)) - const additionalSearchParams = additionalSearchParamsOption ?? additionalSearchParamsArgv ?? new URLSearchParams() - const getStoriesBaselinePath = this.#options?.storybook?.getStoriesBaselinePath - - // Create the test files - createTestFiles({ - additionalSearchParams, - clip, - clipSelector, - compareOptions, - directoryPath: tempDir, - folders: this.folders, - framework, - getStoriesBaselinePath, - numShards, - skipStories: parsedSkipStories, - storiesJson, - storybookUrl, - }) - - // Create the capabilities - createStorybookCapabilities(capabilities as WebdriverIO.Capabilities[]) - } - } - - async onComplete () { - const tempDir = process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER - if (tempDir) { - log.info(`Cleaning up temporary folder for storybook specs: ${tempDir}`) - try { - rmdirSync(tempDir, { recursive: true }) - log.info(`Temporary folder for storybook specs has been removed: ${tempDir}`) - } catch (err) { - log.error(`Failed to remove temporary folder for storybook specs: ${tempDir} due to: ${(err as Error).message}`) - } - - // Remove the environment variables - delete process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER - delete process.env.VISUAL_STORYBOOK_URL - delete process.env.VISUAL_STORYBOOK_CLIP_SELECTOR - } - - if (this.#options.createJsonReportFiles){ - new generateVisualReport( - { directoryPath: this.folders.actualFolder } - ).generate() - } - } -} diff --git a/packages/visual-service/tests/launcher.test.ts b/packages/visual-service/tests/launcher.test.ts new file mode 100644 index 000000000..c2cda7af4 --- /dev/null +++ b/packages/visual-service/tests/launcher.test.ts @@ -0,0 +1,105 @@ +import { rmSync } from 'node:fs' +import { join } from 'node:path' +import type { Services } from '@wdio/types' +import { afterEach, describe, expect, it, vi } from 'vitest' +import VisualLauncher from '../src/launcher.js' +import * as storybookHooks from '../src/storybook/hooks.js' + +vi.mock('@wdio/logger', () => import(join(process.cwd(), '__mocks__', '@wdio/logger'))) +vi.mock('fs') + +vi.mock('../src/storybook/hooks.js', () => ({ + prepareStorybook: vi.fn(), + cleanupStorybook: vi.fn(), +})) + +vi.mock('../src/reporter.js', () => ({ + default: vi.fn().mockImplementation(() => ({ generate: vi.fn() })), +})) + +describe('VisualLauncher', () => { + afterEach(() => { + vi.clearAllMocks() + }) + + describe('onPrepare - clearRuntimeFolder (issue #683)', () => { + it('should clear runtime folders once in onPrepare when clearRuntimeFolder is true', async () => { + const rmSyncMock = vi.mocked(rmSync) + rmSyncMock.mockClear() + + const launcher = new VisualLauncher({ clearRuntimeFolder: true }) + rmSyncMock.mockClear() + + const config = { runner: 'local', framework: 'mocha' } as WebdriverIO.Config + await launcher.onPrepare!(config, [{}]) + + expect(rmSyncMock).toHaveBeenCalledTimes(2) + }) + + it('should not clear runtime folders in onPrepare when clearRuntimeFolder is false', async () => { + const rmSyncMock = vi.mocked(rmSync) + rmSyncMock.mockClear() + + const launcher = new VisualLauncher({ clearRuntimeFolder: false }) + rmSyncMock.mockClear() + + const config = { runner: 'local', framework: 'mocha' } as WebdriverIO.Config + await launcher.onPrepare!(config, [{}]) + + expect(rmSyncMock).not.toHaveBeenCalled() + }) + }) + + describe('onPrepare - storybook delegation', () => { + it('should delegate to prepareStorybook', async () => { + const launcher = new VisualLauncher({}) + const config = { runner: 'local', framework: 'mocha' } as WebdriverIO.Config + const caps = [{}] + + await launcher.onPrepare!(config, caps) + + expect(vi.mocked(storybookHooks.prepareStorybook)).toHaveBeenCalledWith( + config, + caps, + expect.any(Object), + expect.objectContaining({ + actualFolder: expect.any(String), + baselineFolder: expect.any(String), + diffFolder: expect.any(String), + }), + ) + }) + }) + + describe('onComplete', () => { + it('should delegate to cleanupStorybook', async () => { + const launcher = new VisualLauncher({}) as Services.ServiceInstance + + // @ts-ignore + await launcher.onComplete!() + + expect(vi.mocked(storybookHooks.cleanupStorybook)).toHaveBeenCalledOnce() + }) + + it('should generate visual report when createJsonReportFiles is true', async () => { + const generateVisualReport = (await import('../src/reporter.js')).default + const launcher = new VisualLauncher({ createJsonReportFiles: true }) as Services.ServiceInstance + + // @ts-ignore + await launcher.onComplete!() + + expect(generateVisualReport).toHaveBeenCalled() + }) + + it('should not generate visual report when createJsonReportFiles is false', async () => { + const generateVisualReport = (await import('../src/reporter.js')).default + vi.mocked(generateVisualReport).mockClear() + const launcher = new VisualLauncher({ createJsonReportFiles: false }) as Services.ServiceInstance + + // @ts-ignore + await launcher.onComplete!() + + expect(generateVisualReport).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/visual-service/tests/storybook/__snapshots__/hooks.test.ts.snap b/packages/visual-service/tests/storybook/__snapshots__/hooks.test.ts.snap new file mode 100644 index 000000000..34c6b037d --- /dev/null +++ b/packages/visual-service/tests/storybook/__snapshots__/hooks.test.ts.snap @@ -0,0 +1,23 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Storybook hooks > prepareStorybook > should process all default data 1`] = `"Running \`@wdio/visual-service\` in Storybook mode."`; + +exports[`Storybook hooks > prepareStorybook > should process all default data 2`] = `"Clearing the current capabilities."`; + +exports[`Storybook hooks > prepareStorybook > should throw an error for storybook and cucumber 1`] = ` +" + +Running Storybook in combination with the cucumber framework adapter is not supported. +Only Jasmine and Mocha are supported. + +" +`; + +exports[`Storybook hooks > prepareStorybook > should throw an error for storybook multiremote 1`] = ` +" + +Running Storybook in combination with Multiremote is not supported. +Remove your \`capabilities\` property from your config or assign an empty array to it like \`capabilities: [],\`. + +" +`; diff --git a/packages/visual-service/tests/storybook/__snapshots__/launcher.test.ts.snap b/packages/visual-service/tests/storybook/__snapshots__/launcher.test.ts.snap deleted file mode 100644 index c3f66b177..000000000 --- a/packages/visual-service/tests/storybook/__snapshots__/launcher.test.ts.snap +++ /dev/null @@ -1,23 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Visual Launcher for Storybook > onPrepare > should process all default data 1`] = `"Running \`@wdio/visual-service\` in Storybook mode."`; - -exports[`Visual Launcher for Storybook > onPrepare > should process all default data 2`] = `"Clearing the current capabilities."`; - -exports[`Visual Launcher for Storybook > onPrepare > should throw an error for storybook and cucumber 1`] = ` -" - -Running Storybook in combination with the cucumber framework adapter is not supported. -Only Jasmine and Mocha are supported. - -" -`; - -exports[`Visual Launcher for Storybook > onPrepare > should throw an error for storybook multiremote 1`] = ` -" - -Running Storybook in combination with Multiremote is not supported. -Remove your \`capabilities\` property from your config or assign an empty array to it like \`capabilities: [],\`. - -" -`; diff --git a/packages/visual-service/tests/storybook/launcher.test.ts b/packages/visual-service/tests/storybook/hooks.test.ts similarity index 72% rename from packages/visual-service/tests/storybook/launcher.test.ts rename to packages/visual-service/tests/storybook/hooks.test.ts index 257957997..6c04769f8 100644 --- a/packages/visual-service/tests/storybook/launcher.test.ts +++ b/packages/visual-service/tests/storybook/hooks.test.ts @@ -1,17 +1,17 @@ import { rmdirSync } from 'node:fs' import { join } from 'node:path' import logger from '@wdio/logger' -import type { Capabilities, Services } from '@wdio/types' +import type { Capabilities } from '@wdio/types' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import VisualLauncher from '../../src/storybook/launcher.js' -import type { ClassOptions } from '@wdio/image-comparison-core' +import type { ClassOptions, Folders } from '@wdio/image-comparison-core' +import { prepareStorybook, cleanupStorybook } from '../../src/storybook/hooks.js' import * as storybookUtils from '../../src/storybook/utils.js' const log = logger('test') vi.mock('@wdio/logger', () => import(join(process.cwd(), '__mocks__', '@wdio/logger'))) vi.mock('fs') -vi.mock('../../src/storybook/utils.js', ()=>({ +vi.mock('../../src/storybook/utils.js', () => ({ isStorybookMode: vi.fn(() => true), isCucumberFramework: vi.fn(() => false), scanStorybook: vi.fn(() => ({ @@ -25,12 +25,12 @@ vi.mock('../../src/storybook/utils.js', ()=>({ createStorybookCapabilities: vi.fn(), })) -describe('Visual Launcher for Storybook', () => { - describe('onPrepare', () => { - let options: ClassOptions, - caps: Capabilities.RequestedStandaloneCapabilities[], - config: WebdriverIO.Config, - Launcher: Services.ServiceInstance +describe('Storybook hooks', () => { + describe('prepareStorybook', () => { + let options: ClassOptions + let caps: Capabilities.RequestedStandaloneCapabilities[] + let config: WebdriverIO.Config + let folders: Folders beforeEach(() => { options = {} @@ -39,19 +39,19 @@ describe('Visual Launcher for Storybook', () => { runner: 'local', framework: 'mocha' } as WebdriverIO.Config - Launcher = new VisualLauncher(options) + folders = { + actualFolder: '.tmp/actual', + baselineFolder: '.tmp/baseline', + diffFolder: '.tmp/diff', + } vi.clearAllMocks() }) it('should process all default data', async () => { - if (!Launcher.onPrepare) { - throw new Error('onPrepare method is not defined on Launcher') - } - const logInfoMock = vi.spyOn(log, 'info') - await Launcher!.onPrepare(config, caps) + await prepareStorybook(config, caps, options, folders) expect(vi.mocked(storybookUtils.isStorybookMode)).toHaveBeenCalledOnce() expect(vi.mocked(storybookUtils.isCucumberFramework)).toHaveBeenCalledOnce() @@ -64,9 +64,6 @@ describe('Visual Launcher for Storybook', () => { }) it('should process all process.argv data', async () => { - if (!Launcher.onPrepare) { - throw new Error('onPrepare method is not defined on Launcher') - } vi.mocked(storybookUtils.getArgvValue) .mockReturnValueOnce(6) // --version .mockReturnValueOnce(2) // --numShards @@ -75,36 +72,28 @@ describe('Visual Launcher for Storybook', () => { .mockReturnValueOnce(['foo-bar-foo']) // --skipStories .mockReturnValueOnce('foo=bar') // --additionalSearchParams - await Launcher.onPrepare(config, caps) + await prepareStorybook(config, caps, options, folders) expect(vi.mocked(storybookUtils.getArgvValue)).toHaveBeenCalledTimes(6) expect(vi.mocked(storybookUtils.parseSkipStories)).toHaveBeenCalledWith(['foo-bar-foo']) }) it('should process all options data', async () => { - if (!Launcher.onPrepare) { - throw new Error('onPrepare method is not defined on Launcher') - } - options.storybook = { version: 7, numShards: 16, clip: false, clipSelector: 'clipSelector', skipStories: 'skipStories', additionalSearchParams: new URLSearchParams({ foo: 'bar' }) } - await Launcher.onPrepare(config, caps) + await prepareStorybook(config, caps, options, folders) expect(vi.mocked(storybookUtils.getArgvValue)).toHaveBeenCalledTimes(6) expect(vi.mocked(storybookUtils.parseSkipStories)).toHaveBeenCalledWith('skipStories') }) it('should throw an error for storybook and cucumber', async () => { - if (!Launcher.onPrepare) { - throw new Error('onPrepare method is not defined on Launcher') - } - const logInfoMock = vi.spyOn(log, 'info') vi.mocked(storybookUtils.isCucumberFramework).mockReturnValueOnce(true) let error try { - await Launcher.onPrepare(config, caps) + await prepareStorybook(config, caps, options, folders) } catch (e) { error = e } @@ -114,7 +103,6 @@ describe('Visual Launcher for Storybook', () => { expect(vi.mocked(storybookUtils.isStorybookMode)).toHaveBeenCalledOnce() expect(vi.mocked(storybookUtils.isCucumberFramework)).toHaveBeenCalledOnce() expect(logInfoMock).not.toHaveBeenCalled() - // Ensure other mocks were not called expect(vi.mocked(storybookUtils.getArgvValue)).not.toHaveBeenCalled() expect(vi.mocked(storybookUtils.parseSkipStories)).not.toHaveBeenCalled() expect(vi.mocked(storybookUtils.createTestFiles)).not.toHaveBeenCalled() @@ -122,10 +110,6 @@ describe('Visual Launcher for Storybook', () => { }) it('should throw an error for storybook multiremote', async () => { - if (!Launcher.onPrepare) { - throw new Error('onPrepare method is not defined on Launcher') - } - const logInfoMock = vi.spyOn(log, 'info') const multiremoteCaps = { myChromeBrowser: { @@ -139,9 +123,10 @@ describe('Visual Launcher for Storybook', () => { } } } + let error try { - await Launcher.onPrepare(config, multiremoteCaps) + await prepareStorybook(config, multiremoteCaps, options, folders) } catch (e) { error = e } @@ -149,68 +134,61 @@ describe('Visual Launcher for Storybook', () => { expect(error).toBeDefined() expect((error as Error).message).toMatchSnapshot() expect(logInfoMock).toHaveBeenCalledOnce() - // Ensure other mocks were not called expect(vi.mocked(storybookUtils.getArgvValue)).not.toHaveBeenCalled() expect(vi.mocked(storybookUtils.parseSkipStories)).not.toHaveBeenCalled() expect(vi.mocked(storybookUtils.createTestFiles)).not.toHaveBeenCalled() expect(vi.mocked(storybookUtils.createStorybookCapabilities)).not.toHaveBeenCalled() }) - }) - describe('onComplete', () => { - let Launcher: Services.ServiceInstance + it('should do nothing when not in storybook mode', async () => { + vi.mocked(storybookUtils.isStorybookMode).mockReturnValueOnce(false) + + await prepareStorybook(config, caps, options, folders) + + expect(vi.mocked(storybookUtils.scanStorybook)).not.toHaveBeenCalled() + expect(vi.mocked(storybookUtils.createTestFiles)).not.toHaveBeenCalled() + expect(vi.mocked(storybookUtils.createStorybookCapabilities)).not.toHaveBeenCalled() + }) + }) + describe('cleanupStorybook', () => { beforeEach(() => { vi.clearAllMocks() - Launcher = new VisualLauncher({}) }) afterEach(() => { - // Clean up environment variables delete process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER }) - it('should remove temporary folder and log success', async () => { - if (!Launcher.onComplete) { - throw new Error('onComplete method is not defined on Launcher') - } + it('should remove temporary folder and log success', () => { process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER = 'path/to/tempDir' const logInfoSpy = vi.spyOn(log, 'info') const rmdirSyncMock = vi.mocked(rmdirSync) - // @ts-ignore - await Launcher.onComplete() + cleanupStorybook() expect(rmdirSyncMock).toHaveBeenCalledWith('path/to/tempDir', { recursive: true }) expect(logInfoSpy).toHaveBeenCalledWith(expect.stringContaining('Temporary folder for storybook specs has been removed')) expect(process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER).toBeUndefined() }) - it('should log error if temporary folder removal fails', async () => { - if (!Launcher.onComplete) { - throw new Error('onComplete method is not defined on Launcher') - } + it('should log error if temporary folder removal fails', () => { process.env.VISUAL_STORYBOOK_TEMP_SPEC_FOLDER = 'path/to/tempDir' const logErrorSpy = vi.spyOn(log, 'error') vi.mocked(rmdirSync).mockImplementationOnce(() => { throw new Error('Deletion Failed') }) - // @ts-ignore - await Launcher.onComplete() + cleanupStorybook() expect(logErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to remove temporary folder for storybook specs')) }) - it('should do nothing if temp directory is not set', async () => { - if (!Launcher.onComplete) { - throw new Error('onComplete method is not defined on Launcher') - } + it('should do nothing if temp directory is not set', () => { const rmdirSyncMock = vi.mocked(rmdirSync) const logInfoSpy = vi.spyOn(log, 'info') - // @ts-ignore - await Launcher.onComplete() + cleanupStorybook() expect(rmdirSyncMock).not.toHaveBeenCalled() expect(logInfoSpy).not.toHaveBeenCalled()