UNPKG

creevey

Version:

Cross-browser screenshot testing tool for Storybook with fancy UI Runner

130 lines (115 loc) 4.76 kB
import { createHash } from 'crypto'; import _ from 'lodash'; import type { TestData, CreeveyStoryParams, StoriesRaw, SkipOptions, ServerTest, StoryInput, CreeveyTestFunction, CreeveyTestContext, } from '../types.js'; import { isDefined } from '../types.js'; import { shouldSkip } from './utils.js'; function storyTestFabric(delay?: number, testFn?: CreeveyTestFunction) { return async function storyTest(context: CreeveyTestContext) { if (delay) await new Promise((resolve) => setTimeout(resolve, delay)); await (testFn ? testFn(context) : context.screenshots.length > 0 ? context.matchImages( context.screenshots.reduce( (screenshots, { imageName, screenshot }, index) => ({ ...screenshots, [imageName ?? `screenshot_${index}`]: screenshot, }), {}, ), ) : context.matchImage(await context.takeScreenshot())); }; } function createCreeveyTest( browser: string, storyMeta: StoryInput, skipOptions?: SkipOptions, testName?: string, ): TestData { const { title, name, id: storyId } = storyMeta; const testPath = [title, name, testName, browser].filter(isDefined); const skip = skipOptions ? shouldSkip(browser, { title, name }, skipOptions, testName) : false; const id = createHash('sha1').update(testPath.join('/')).digest('hex'); return { id, skip, browser, testName, storyPath: [...title.split('/').map((x) => x.trim()), name], storyId, }; } function convertStories(browserName: string, stories: StoriesRaw | StoryInput[]): Partial<Record<string, ServerTest>> { const tests: Record<string, ServerTest> = {}; (Array.isArray(stories) ? stories : Object.values(stories)).forEach((storyMeta) => { // TODO Skip docsOnly stories for now if (storyMeta.parameters.docsOnly) return; const { delay: delayParam, tests: storyTests, skip } = (storyMeta.parameters.creevey ?? {}) as CreeveyStoryParams; const delay = typeof delayParam == 'number' ? delayParam : delayParam?.for.includes(browserName) ? delayParam.ms : 0; // typeof tests === "undefined" => rootSuite -> kindSuite -> storyTest -> [browsers.png] // typeof tests === "function" => rootSuite -> kindSuite -> storyTest -> browser -> [images.png] // typeof tests === "object" => rootSuite -> kindSuite -> storySuite -> test -> [browsers.png] // typeof tests === "object" => rootSuite -> kindSuite -> storySuite -> test -> browser -> [images.png] if (!storyTests) { const test = createCreeveyTest(browserName, storyMeta, skip); tests[test.id] = { ...test, storyId: storyMeta.id, story: storyMeta, fn: storyTestFabric(delay) }; return; } Object.entries(storyTests).forEach(([testName, testFn]) => { const test = createCreeveyTest(browserName, storyMeta, skip, testName); tests[test.id] = { ...test, storyId: storyMeta.id, story: storyMeta, fn: storyTestFabric(delay, testFn) }; }); }); return tests; } export async function loadTestsFromStories( browsers: string[], provider: (storiesListener: (stories: Map<string, StoryInput[]>) => void) => Promise<StoriesRaw>, update?: (testsDiff: Partial<Record<string, ServerTest>>) => void, ): Promise<Partial<Record<string, ServerTest>>> { const testIdsByFiles = new Map<string, string[]>(); const stories = await provider((storiesByFiles) => { const testsDiff: Partial<Record<string, ServerTest>> = {}; const tests: Partial<Record<string, ServerTest>> = {}; browsers.forEach((browser) => { Array.from(storiesByFiles.entries()).forEach(([filename, stories]) => { Object.assign(tests, convertStories(browser, stories)); const changed = Object.keys(tests); const removed = testIdsByFiles.get(filename)?.filter((testId) => !tests[testId]) ?? []; if (changed.length == 0) testIdsByFiles.delete(filename); else testIdsByFiles.set(filename, changed); Object.assign(testsDiff, tests); removed.forEach((testId) => (testsDiff[testId] = undefined)); }); }); update?.(testsDiff); }); const tests = browsers.reduce( (tests: Partial<Record<string, ServerTest>>, browser) => Object.assign(tests, convertStories(browser, stories)), {}, ); Object.values(tests) .filter(isDefined) .forEach( ({ id, story: { parameters: { fileName }, }, }) => // TODO Don't use filename as a key, due possible collisions if two require.context with same structure of modules are defined testIdsByFiles.set(fileName as string, [...(testIdsByFiles.get(fileName as string) ?? []), id]), ); return tests; }