@sxzz/test-utils
Version:
Collection of common test utils.
205 lines (203 loc) • 7.08 kB
JavaScript
import path from "node:path";
import process from "node:process";
import { glob } from "tinyglobby";
import { normalizePath as normalizePath$1 } from "unplugin-utils";
import { readFile } from "node:fs/promises";
//#region src/fixture.ts
let isSkip;
function testFixturesSkip(fn) {
isSkip = fn;
}
async function testFixtures(globsOrFiles, cb, { params, promise, concurrent, snapshot = true, ...globOptions } = {}) {
const { describe, test } = await import("vitest");
let files;
if (typeof globsOrFiles === "string" || Array.isArray(globsOrFiles)) files = Object.fromEntries((await glob(globsOrFiles, globOptions)).map((file) => [file, void 0]));
else files = globsOrFiles;
for (const [id, code] of Object.entries(files)) makeTests(id, code, [[normalizePath$1(id)], ...params || []])();
function makeTests(id, code, params, args = {}) {
const [currParams, ...restParams] = params;
const [name, values = [void 0]] = currParams;
if (restParams.length > 0) return () => {
for (const value of values) {
const currArgs = {
...args,
[name]: value
};
describe(getName(name, value), makeTests(id, code, restParams, currArgs));
}
};
return () => {
for (const value of values) {
const testName = getName(name, value);
let testFn = test.skipIf(isSkip?.(testName));
if (concurrent) testFn = testFn.concurrent;
testFn(testName, async ({ expect }) => {
const currArgs = {
...args,
[name]: value
};
const execute = () => cb(currArgs, path.resolve(globOptions.cwd || process.cwd(), id), code);
if (snapshot) if (id.includes("error")) if (promise) await expect(execute()).rejects.toThrowErrorMatchingSnapshot();
else expect(execute).toThrowErrorMatchingSnapshot();
else if (promise) await expect(execute().catch((error) => {
console.warn(error);
throw error;
})).resolves.toMatchSnapshot();
else expect(execute()).toMatchSnapshot();
else await execute();
});
}
};
}
}
function getName(name, value) {
return value === void 0 ? name : `${name} = ${String(value)}`;
}
//#endregion
//#region src/path.ts
function normalizePath(path) {
return path.replaceAll("\\", "/");
}
function replacePath(source, search, replacement) {
return source.replaceAll(search, replacement).replaceAll(normalizePath(search), replacement);
}
//#endregion
//#region src/snapshot.ts
function outputToSnapshot(chunks) {
const cwd = process.cwd();
return chunks.map((file) => {
let filename, content;
if ("path" in file) {
filename = file.path;
content = file.text;
} else {
filename = file.fileName;
content = file.type === "chunk" ? file.code : typeof file.source === "string" ? file.source : "[BINARY]";
}
return `// ${normalizePath(filename)}\n${replacePath(content, cwd, "[CWD]")}`;
}).toSorted().join("\n");
}
async function expectFilesSnapshot(sourceDir, snapshotFile, { pattern = "**/*", expect } = {}) {
if (!expect) expect = await import("vitest").then((m) => m.expect);
const cwd = process.cwd();
const files = (await glob(pattern, { cwd: sourceDir })).toSorted();
const fileMap = Object.fromEntries(await Promise.all(files.map(async (filename) => [normalizePath(filename), replacePath(await readFile(path.resolve(sourceDir, filename), "utf8"), cwd, "[CWD]")])));
const snapshot = Object.entries(fileMap).map(([filename, contents]) => {
return `## ${filename}
\`\`\`${path.extname(filename).slice(1)}
${contents}
\`\`\`
`;
}).join("\n");
await expect(snapshot).toMatchFileSnapshot(snapshotFile);
return {
files,
fileMap,
snapshot
};
}
//#endregion
//#region src/rolldown.ts
async function rolldownBuild(input, plugins = [], inputOptions = {}, outputOptions = {}) {
const { rolldown } = await import("rolldown");
const chunks = (await (await rolldown({
input,
treeshake: false,
onwarn(warning, defaultHandler) {
if (["UNUSED_EXTERNAL_IMPORT", "UNRESOLVED_IMPORT"].includes(warning.code)) return;
defaultHandler(warning);
},
...inputOptions,
plugins: [plugins, inputOptions.plugins],
checks: {
pluginTimings: false,
...inputOptions.checks
}
})).generate({
format: "esm",
sourcemap: false,
...outputOptions
})).output;
return {
chunks,
snapshot: outputToSnapshot(chunks)
};
}
//#endregion
//#region src/rollup.ts
async function rollupBuild(input, plugins = [], inputOptions = {}, outputOptions = {}) {
const { rollup } = await import("rollup");
const chunks = (await (await rollup({
input,
treeshake: false,
onwarn(warning, defaultHandler) {
if (["UNUSED_EXTERNAL_IMPORT", "UNRESOLVED_IMPORT"].includes(warning.code)) return;
defaultHandler(warning);
},
...inputOptions,
plugins: [plugins, inputOptions.plugins]
})).generate({
format: "esm",
sourcemap: false,
...outputOptions
})).output;
return {
chunks,
snapshot: outputToSnapshot(chunks)
};
}
const RollupToStringPlugin = () => {
return {
name: "to-string",
transform: (code) => `export default \`${code.replaceAll("`", "\\`")}\``
};
};
const RollupEscapeNullCharacterPlugin = () => {
return {
name: "escape-null-character",
generateBundle(options, bundle) {
for (const filename of Object.keys(bundle)) {
const b = bundle[filename];
if (b.type !== "chunk") continue;
if (b.code.includes("\0")) b.code = b.code.replaceAll("\0", "[NULL]");
}
}
};
};
//#endregion
//#region src/string.ts
function removeSpaces(s) {
return s.trim().replaceAll(/[\n\r]/g, "").replaceAll(/\s+/g, " ");
}
//#endregion
//#region src/volar.ts
async function createTsMacroProgram(rootNames, plugins, compilerOptions = {}) {
const ts = await import("typescript");
const { getLanguagePlugins } = await import("@ts-macro/language-plugin");
const { proxyCreateProgram } = await import("@volar/typescript");
const host = ts.createCompilerHost(compilerOptions);
return proxyCreateProgram(ts, ts.createProgram, (ts, options) => getLanguagePlugins(ts, options.options, { plugins }))({
options: compilerOptions,
host,
rootNames
});
}
async function createVueProgram(rootNames, compilerOptions = {}, vueCompilerOptions = {}) {
const ts = await import("typescript");
const { createVueLanguagePlugin, CompilerOptionsResolver } = await import("@vue/language-core");
const { proxyCreateProgram } = await import("@volar/typescript");
const host = ts.createCompilerHost(compilerOptions);
return proxyCreateProgram(ts, ts.createProgram, (ts, options) => {
const rootDir = options.options.$rootDir || process.cwd();
const resolver = new CompilerOptionsResolver(ts, ts.sys.readFile);
resolver.addConfig(vueCompilerOptions, rootDir);
const vueOptions = resolver.build();
return { languagePlugins: [createVueLanguagePlugin(ts, options.options, vueOptions, (id) => id)] };
})({
options: compilerOptions,
host,
rootNames
});
}
//#endregion
export { RollupEscapeNullCharacterPlugin, RollupToStringPlugin, createTsMacroProgram, createVueProgram, expectFilesSnapshot, normalizePath, outputToSnapshot, removeSpaces, replacePath, rolldownBuild, rollupBuild, testFixtures, testFixturesSkip };