UNPKG

@sxzz/test-utils

Version:

Collection of common test utils.

205 lines (203 loc) 7.08 kB
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 };