@lewiswright/vitest-plugin-vis
Version:
Vitest visual testing plugin
136 lines (135 loc) • 6.28 kB
JavaScript
import { join, relative, resolve } from 'pathe';
import { pick } from 'type-plus';
import { BASELINE_DIR, DIFF_DIR, RESULT_DIR } from "../shared/constants.js";
import { getProjectName } from "./commands/browser_command_context.js";
import { file } from "./file.js";
import { getSnapshotSubpath, resolveSnapshotRootDir } from "./snapshot_path.js";
import { ctx } from "./vis_context.ctx.js";
export function createVisContext() {
let visOptionsRecord = {};
let state = {};
const context = {
setOptions(projectName, options = {}) {
visOptionsRecord[projectName ?? '__default'] = options;
},
__test__getOptions(projectName) {
return visOptionsRecord[projectName];
},
__test__reset() {
visOptionsRecord = {};
state = {};
},
__test__getState(context) {
return state[getProjectId(context)];
},
/**
* 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 setupSuite(browserContext) {
const projectId = getProjectId(browserContext);
const visOptions = getVisOptions(visOptionsRecord, browserContext);
if (!state[projectId]) {
state[projectId] = setupState(browserContext, visOptions);
}
const projectState = await state[projectId];
const { suiteId, suite } = createSuite(projectState, browserContext.testPath, visOptions);
projectState.suites[suiteId] = suite;
await Promise.allSettled([ctx.rimraf(suite.diffDir), ctx.rimraf(suite.resultDir)]);
return pick(projectState, 'subjectDataTestId');
},
async getSnapshotInfo(browserContext, name, isAutoSnapshot, options) {
const suiteInfo = await context.getSuiteInfo(browserContext, name);
const snapshotFilename = context.getSnapshotFilename(browserContext, suiteInfo, options?.snapshotFileId, isAutoSnapshot);
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(getVisOptions(visOptionsRecord, browserContext), 'comparisonMethod', 'diffOptions', 'failureThreshold', 'failureThresholdType', 'timeout', 'animations'),
baselinePath,
resultPath,
diffPath,
};
},
async getTaskCount(browserContext, taskId) {
return (await context.getSuiteInfo(browserContext, taskId)).task.count;
},
async hasImageSnapshot(browserContext, taskId, snapshotFileId, isAutoSnapshot) {
const info = await context.getSuiteInfo(browserContext, taskId);
return file.existFile(resolve(info.projectRoot, info.baselineDir, context.getSnapshotFilename(browserContext, info, snapshotFileId, isAutoSnapshot)));
},
getSnapshotFilename(browserContext, info, snapshotFileId, isAutoSnapshot) {
if (snapshotFileId)
return `${snapshotFileId}.png`;
const customizeSnapshotId = getVisOptions(visOptionsRecord, browserContext).customizeSnapshotId ?? (({ id, index }) => `${id}-${index}`);
return `${customizeSnapshotId({
id: info.taskId,
index: info.task.count,
isAutoSnapshot,
})}.png`;
},
async getSuiteInfo(browserContext, taskId) {
const projectId = getProjectId(browserContext);
const projectState = await state[projectId];
const visOptions = getVisOptions(visOptionsRecord, browserContext);
const suiteId = getSuiteId(projectState, browserContext.testPath, visOptions);
const suite = projectState.suites[suiteId];
const task = (suite.tasks[taskId] = suite.tasks[taskId] ?? { count: 1 });
return {
projectRoot: projectState.projectRoot,
suiteId,
taskId,
baselineDir: suite.baselineDir,
resultDir: suite.resultDir,
diffDir: suite.diffDir,
task,
};
},
};
return context;
}
async function setupState(browserContext, visOptions) {
const snapshotRootDir = resolveSnapshotRootDir(browserContext, visOptions);
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),
subjectDataTestId: visOptions.subjectDataTestId,
suites: {},
};
await Promise.allSettled([ctx.rimraf(join(state.snapshotDiffDir)), ctx.rimraf(join(state.snapshotResultDir))]);
return state;
}
export function createSuite(state, testPath, options) {
const suiteId = getSuiteId(state, testPath, options);
return {
suiteId,
suite: {
baselineDir: join(state.snapshotBaselineDir, suiteId),
resultDir: join(state.snapshotResultDir, suiteId),
diffDir: join(state.snapshotDiffDir, suiteId),
tasks: {},
},
};
}
export function getSuiteId(state, testPath, options) {
return getSnapshotSubpath(relative(state.projectRoot, testPath), options);
}
function getVisOptions(visOptionsRecord, context) {
return visOptionsRecord[getProjectName(context) ?? '__default'] ?? {};
}
function getProjectRoot(context) {
return context.project.config.root;
}
function getProjectId(context) {
return `${context.project.config.root}/${context.project.config.name}`;
}