UNPKG

vitest-plugin-vis

Version:
412 lines (390 loc) 14.6 kB
import { t as assertSnapshotKeyWithoutDash } from "./asserts--HSSlgPD.mjs"; import { i as RESULT_DIR, n as DIFF_DIR, r as NAME, t as BASELINE_DIR } from "./constants-CTBaz8Ml.mjs"; import { i as trimCommonFolder, n as getSnapshotSubpath, r as resolveSnapshotRootDir } from "./snapshot_path-7GliOSH_.mjs"; import { t as isBase64String } from "./base64-CHSENEAO.mjs"; import { basename, dirname, join, relative, resolve } from "pathe"; import { pick } from "type-plus"; import { glob } from "glob"; import { mkdirp } from "mkdirp"; import { readFile, stat, writeFile } from "node:fs/promises"; import ci from "is-ci"; import { rimraf } from "rimraf"; //#region src/server/externals/file.ts const file = { existFile(filePath) { return stat(filePath).then((s) => s.isFile()).catch(() => false); }, glob(pattern, options) { return glob(pattern, options); }, mkdirp, async tryReadFile(filePath) { if (!filePath) return void 0; return readFile(filePath).catch(() => void 0); }, writeFile }; //#endregion //#region src/server/project.ts function getProjectName(context) { const { runner } = context.project; const name = runner.config.name; if (typeof name === "string") return name; } function getProjectRoot(context) { return context.project.config.root; } //#endregion //#region src/server/vis_options.ts const DEFAULT_PROJECT_NAME = "__default"; const visOptions = {}; function setVisOption(userConfig, options) { assertSnapshotKeyWithoutDash(options?.snapshotKey); const name = userConfig.test?.name; const id = typeof name === "string" ? name : typeof name === "object" ? name?.label : DEFAULT_PROJECT_NAME; visOptions[id] = options; } function getVisOption(context) { return visOptions[getProjectName(context) ?? DEFAULT_PROJECT_NAME] ?? {}; } //#endregion //#region src/server/vis_server_context.deps.ts /* v8 ignore start */ const deps = { rimraf, getSnapshotPlatform() { return ci ? process.platform : "local"; } }; /* v8 ignore end */ //#endregion //#region src/server/suite.ts const suites = {}; /** * Setup suite is called on each test file's beforeAll hook. * Test files include vitest test files and storybook story files. * It needs to make sure there is no race condition between the test files. */ async function setupSuite(browserContext) { const suiteId = getSuiteId(browserContext); const visOptions$1 = getVisOption(browserContext); if (!suites[suiteId]) suites[suiteId] = createSuite(browserContext, visOptions$1); const suite = await suites[suiteId]; const { taskSubpath, baselineDir, diffDir, resultDir, tasks } = createModule(suite, browserContext.testPath, visOptions$1); suite.modules[taskSubpath] = { baselineDir, diffDir, resultDir, tasks }; await Promise.allSettled([deps.rimraf(diffDir), deps.rimraf(resultDir)]); return { comparisonMethod: visOptions$1.comparisonMethod, diffOptions: visOptions$1.diffOptions, failureThreshold: visOptions$1.failureThreshold, failureThresholdType: visOptions$1.failureThresholdType, snapshotKey: visOptions$1.snapshotKey, subject: visOptions$1.subject, timeout: visOptions$1.timeout }; } /** * Suite ID also contains the project name to make it unique * across different projects. */ function getSuiteId(context) { return `${getProjectName(context)}/${context.project.config.name}`; } async function createSuite(browserContext, visOptions$1) { const snapshotRootDir = resolveSnapshotRootDir(browserContext, visOptions$1); const projectRoot = getProjectRoot(browserContext); const state = { projectRoot, testTimeout: browserContext.project.config.testTimeout, hookTimeout: browserContext.project.config.hookTimeout, snapshotRootDir, snapshotBaselineDir: join(snapshotRootDir, BASELINE_DIR), snapshotResultDir: join(snapshotRootDir, RESULT_DIR), snapshotDiffDir: join(snapshotRootDir, DIFF_DIR), snapshotRootPath: join(projectRoot, snapshotRootDir), subject: visOptions$1.subject, modules: {} }; await Promise.allSettled([deps.rimraf(join(state.snapshotDiffDir)), deps.rimraf(join(state.snapshotResultDir))]); return state; } function createModule(state, testPath, options) { const taskSubpath = getTaskSubpath(state, testPath, options); return { taskSubpath, baselineDir: join(state.snapshotBaselineDir, taskSubpath), resultDir: join(state.snapshotResultDir, taskSubpath), diffDir: join(state.snapshotDiffDir, taskSubpath), tasks: {} }; } function getTaskSubpath(state, testPath, options) { return getSnapshotSubpath(relative(state.projectRoot, testPath), options); } function getSuite(context) { return suites[getSuiteId(context)]; } //#endregion //#region src/server/vis_server_context.logic.ts function createVisServerContext() { const context = { async getSnapshotResults(browserContext, taskId) { const suiteInfo = await context.getSuiteInfo(browserContext, taskId); const baselines = await file.glob(join(suiteInfo.projectRoot, suiteInfo.baselineDir, `${suiteInfo.taskId}-*.png`)); const results = await file.glob(join(suiteInfo.projectRoot, suiteInfo.resultDir, `${suiteInfo.taskId}-*.png`)); const diffs = await file.glob(join(suiteInfo.projectRoot, suiteInfo.diffDir, `${suiteInfo.taskId}-*.png`)); const r = []; await Promise.all(baselines.map(async (baselinePath) => { const filename = basename(baselinePath); const resultPath = results.find((r$1) => r$1.endsWith(filename)); const diffPath = diffs.find((d) => d.endsWith(filename)); const baselineBuffer = await file.tryReadFile(baselinePath); const resultBuffer = await file.tryReadFile(resultPath); const diffBuffer = await file.tryReadFile(diffPath); r.push({ filename, baseline: baselineBuffer?.toString("base64"), result: resultBuffer?.toString("base64"), diff: diffBuffer?.toString("base64") }); })); return r; }, async getSnapshotInfo(browserContext, taskId, options) { const suiteInfo = await context.getSuiteInfo(browserContext, taskId); const snapshotFilename = context.getSnapshotFilename(browserContext, suiteInfo, options?.snapshotKey); const { baselineDir, resultDir, diffDir, task } = suiteInfo; task.count = task.count + 1; const baselinePath = join(baselineDir, snapshotFilename); const resultPath = join(resultDir, snapshotFilename); const diffPath = join(diffDir, snapshotFilename); return { ...pick(getVisOption(browserContext), "comparisonMethod", "diffOptions", "failureThreshold", "failureThresholdType", "timeout"), baselinePath, resultPath, diffPath }; }, async getTaskCount(browserContext, taskId) { return (await context.getSuiteInfo(browserContext, taskId)).task.count; }, async hasImageSnapshot(browserContext, taskId, snapshotKey) { const info = await context.getSuiteInfo(browserContext, taskId); return file.existFile(resolve(info.projectRoot, info.baselineDir, context.getSnapshotFilename(browserContext, info, snapshotKey))); }, getSnapshotFilename(browserContext, info, snapshotKey) { if (snapshotKey) return `${info.taskId}-${snapshotKey}.png`; const visOptions$1 = getVisOption(browserContext); if (typeof visOptions$1.snapshotKey === "string") return `${info.taskId}-${visOptions$1.snapshotKey}.png`; return `${info.taskId}-${info.task.count}.png`; }, async getSuiteInfo(browserContext, taskId) { const projectState = await getSuite(browserContext); const visOptions$1 = getVisOption(browserContext); const moduleId = getTaskSubpath(projectState, browserContext.testPath, visOptions$1); const m = projectState.modules[moduleId]; const task = m.tasks[taskId] = m.tasks[taskId] ?? { count: 1 }; return { projectRoot: projectState.projectRoot, suiteId: moduleId, taskId, baselineDir: m.baselineDir, resultDir: m.resultDir, diffDir: m.diffDir, task }; } }; return context; } //#endregion //#region src/server/vis_server_context.ts const visServerContext = createVisServerContext(); //#endregion //#region src/server/commands/_assertions.ts function assertTestPathDefined(context, commandName) { if (!context.testPath) throw new Error(`'commands.${commandName}' requires testPath to be defined`); } //#endregion //#region src/server/commands/has_image_snapshot.ts const hasImageSnapshot = async (context, taskId, snapshotKey) => { assertTestPathDefined(context, "hasImageSnapshot"); return visServerContext.hasImageSnapshot(context, taskId, snapshotKey); }; //#endregion //#region src/server/commands/image_snapshot_next_index.ts const imageSnapshotNextIndex = async (context, taskId) => { assertTestPathDefined(context, "imageSnapshotNextIndex"); return visServerContext.getTaskCount(context, taskId); }; //#endregion //#region src/server/commands/load_image_snapshot_results.ts const loadImageSnapshotResults = async (context, taskId) => { assertTestPathDefined(context, "loadImageSnapshotResults"); return visServerContext.getSnapshotResults(context, taskId); }; //#endregion //#region src/server/browser_provider/playwright.ts function playwright(context) { const { page, iframe } = context; return { async takeScreenshot(projectRoot, relativeFilePath, selector, options) { return iframe.locator(selector).screenshot({ path: resolve(projectRoot, relativeFilePath), ...options }); }, async takePageScreenshot(projectRoot, relativeFilePath, options) { return page.screenshot({ path: resolve(projectRoot, relativeFilePath), ...options }); } }; } //#endregion //#region src/server/browser_provider/webdriverio.ts function webdriverio(context) { const { browser } = context; return { async takeScreenshot(_projectRoot, relativeFilePath, selector) { return (await browser.$(`${selector}`)).saveScreenshot(relativeFilePath); }, async takePageScreenshot(_projectRoot, relativeFilePath, options) { return browser.saveScreenshot(relativeFilePath, options); } }; } //#endregion //#region src/server/browser_provider/browser_api.ts function browserApi(context) { if (context.provider.name === "playwright") return playwright(context); if (context.provider.name === "webdriverio") return webdriverio(context); throw new Error(`Unsupported provider: ${context.provider.name}`); } //#endregion //#region src/server/snapshot_writer.ts function createSnapshotWriter({ mkdirp: mkdirp$1, writeFile: writeFile$1 } = file) { return { async writeBase64(filePath, data) { await mkdirp$1(dirname(filePath)); await writeFile$1(filePath, data, { encoding: "base64" }); }, async writeBuffer(filePath, data) { await mkdirp$1(dirname(filePath)); await writeFile$1(filePath, data); } }; } const snapshotWriter = createSnapshotWriter(); //#endregion //#region src/server/snapshot.ts async function takeSnapshot(context, projectRoot, relativeFilePath, subject, options) { if (isBase64String(subject)) { await snapshotWriter.writeBase64(resolve(projectRoot, relativeFilePath), subject); return Buffer.from(subject, "base64"); } return takeSnapshotByBrowser(context, projectRoot, relativeFilePath, subject, options); } async function takeSnapshotByBrowser(context, projectRoot, relativeFilePath, subject, options) { await mkdirp(dirname(resolve(projectRoot, relativeFilePath))); return browserApi(context).takeScreenshot(projectRoot, relativeFilePath, subject ?? "body", { timeout: options?.timeout }); } async function takePageSnapshot(context, projectRoot, relativeFilePath, options) { await mkdirp(dirname(resolve(projectRoot, relativeFilePath))); return browserApi(context).takePageScreenshot(projectRoot, relativeFilePath, { timeout: options?.timeout, fullPage: options?.fullPage }); } //#endregion //#region src/server/commands/prepare_image_snapshot_comparison.ts const prepareImageSnapshotComparison = async (context, taskId, subject, options) => { assertTestPathDefined(context, "prepareImageSnapshotComparison"); if (!options) options = {}; options.timeout = options.timeout ?? 3e4; const projectRoot = getProjectRoot(context); const info = await visServerContext.getSnapshotInfo(context, taskId, options); const baselineBuffer = await file.tryReadFile(resolve(projectRoot, info.baselinePath)); if (!baselineBuffer) { if (isBase64String(subject)) await snapshotWriter.writeBase64(resolve(projectRoot, info.baselinePath), subject); else await takeSnapshotByBrowser(context, projectRoot, info.baselinePath, subject, options); return; } const resultBuffer = await takeSnapshot(context, projectRoot, info.resultPath, subject, options); return { ...info, projectRoot, baseline: baselineBuffer.toString("base64"), result: resultBuffer.toString("base64") }; }; //#endregion //#region src/server/commands/prepare_page_image_snapshot_comparison.ts const preparePageImageSnapshotComparison = async (context, taskId, options) => { assertTestPathDefined(context, "preparePageImageSnapshotComparison"); if (!options) options = {}; options.timeout = options.timeout ?? 3e4; const projectRoot = getProjectRoot(context); const info = await visServerContext.getSnapshotInfo(context, taskId, options); const baselineBuffer = await file.tryReadFile(resolve(projectRoot, info.baselinePath)); if (!baselineBuffer) { await takePageSnapshot(context, projectRoot, info.baselinePath, options); return; } const resultBuffer = await takePageSnapshot(context, projectRoot, info.resultPath, options); return { ...info, projectRoot, baseline: baselineBuffer.toString("base64"), result: resultBuffer.toString("base64") }; }; //#endregion //#region src/server/commands/setup_vis_suite.ts const setupVisSuite = async (context) => { assertTestPathDefined(context, "setupVisSuite"); return setupSuite(context); }; //#endregion //#region src/server/commands/commands.ts const commands = { setupVisSuite, imageSnapshotNextIndex, hasImageSnapshot, preparePageImageSnapshotComparison, prepareImageSnapshotComparison, loadImageSnapshotResults }; //#endregion //#region src/config/vis.ts /** * Create a Vite plugin for visual testing. * * If options are not provided, the plugin will use the default options, * which enables the `auto` preset. */ function vis(options = { preset: "auto" }) { return { name: NAME, config(userConfig) { setVisOption(userConfig, options); const preset = options?.preset; return { test: { browser: { name: void 0, commands }, setupFiles: preset && preset !== "none" && preset !== "custom" ? [`vitest-plugin-vis/presets/${preset}`] : [] } }; } }; } //#endregion export { trimCommonFolder, vis }; //# sourceMappingURL=config.mjs.map