vitest-plugin-vis
Version:
Vitest visual testing plugin
412 lines (390 loc) • 14.6 kB
JavaScript
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