Lock decision edge cases with business state tests
The dashboard risk is now mostly contradictory state combinations rather than styling. This extracts calendar action grouping into a pure utility and adds a lightweight TypeScript business-state runner so key decision states can be asserted without adding a test dependency. Constraint: No new dependencies; use the existing TypeScript package for a local runner. Rejected: Only rely on Next build/typecheck | it cannot catch product-language regressions such as fallback AI being labeled complete. Confidence: high Scope-risk: moderate Reversibility: clean Tested: npm run test:business Tested: npm run build Not-tested: Browser-rendered mobile fold interaction snapshots.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import vm from "node:vm";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
const moduleCache = new Map();
|
||||
|
||||
function toWindowsSafePath(value) {
|
||||
return process.platform === "win32" && value.startsWith("/")
|
||||
? value.slice(1)
|
||||
: value;
|
||||
}
|
||||
|
||||
function resolveModule(specifier, fromFile) {
|
||||
if (specifier.startsWith("@/")) {
|
||||
return resolveFile(path.join(projectRoot, specifier.slice(2)));
|
||||
}
|
||||
if (specifier.startsWith(".")) {
|
||||
return resolveFile(path.resolve(path.dirname(fromFile), specifier));
|
||||
}
|
||||
return specifier;
|
||||
}
|
||||
|
||||
function resolveFile(basePath) {
|
||||
const candidates = [
|
||||
basePath,
|
||||
`${basePath}.ts`,
|
||||
`${basePath}.tsx`,
|
||||
`${basePath}.js`,
|
||||
`${basePath}.jsx`,
|
||||
path.join(basePath, "index.ts"),
|
||||
path.join(basePath, "index.tsx"),
|
||||
path.join(basePath, "index.js"),
|
||||
];
|
||||
const match = candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
|
||||
if (!match) {
|
||||
throw new Error(`Cannot resolve module file: ${basePath}`);
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
function loadLocalModule(filename) {
|
||||
const normalized = path.normalize(filename);
|
||||
if (moduleCache.has(normalized)) return moduleCache.get(normalized).exports;
|
||||
|
||||
const source = fs.readFileSync(normalized, "utf8");
|
||||
const transpiled = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
esModuleInterop: true,
|
||||
jsx: ts.JsxEmit.ReactJSX,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
},
|
||||
fileName: normalized,
|
||||
}).outputText;
|
||||
|
||||
const module = { exports: {} };
|
||||
moduleCache.set(normalized, module);
|
||||
|
||||
const localRequire = (specifier) => {
|
||||
const resolved = resolveModule(specifier, normalized);
|
||||
if (typeof resolved === "string" && path.isAbsolute(resolved)) {
|
||||
return loadLocalModule(resolved);
|
||||
}
|
||||
return nodeRequire(specifier);
|
||||
};
|
||||
|
||||
const wrapper = `(function (exports, require, module, __filename, __dirname) {\n${transpiled}\n})`;
|
||||
const compiled = vm.runInThisContext(wrapper, { filename: normalized });
|
||||
compiled(module.exports, localRequire, module, normalized, path.dirname(normalized));
|
||||
return module.exports;
|
||||
}
|
||||
|
||||
function collectTests(dir) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
return entries.flatMap((entry) => {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) return collectTests(fullPath);
|
||||
return /\.test\.tsx?$/.test(entry.name) ? [fullPath] : [];
|
||||
});
|
||||
}
|
||||
|
||||
const testsRoot = path.join(projectRoot, "components", "dashboard", "scan-terminal", "__tests__");
|
||||
const testFiles = fs.existsSync(testsRoot) ? collectTests(testsRoot).sort() : [];
|
||||
|
||||
if (!testFiles.length) {
|
||||
throw new Error(`No business state tests found under ${testsRoot}`);
|
||||
}
|
||||
|
||||
let passed = 0;
|
||||
for (const file of testFiles) {
|
||||
const exported = loadLocalModule(toWindowsSafePath(file));
|
||||
const run = exported.runTests || exported.default;
|
||||
if (typeof run !== "function") {
|
||||
throw new Error(`${file} must export runTests()`);
|
||||
}
|
||||
await run();
|
||||
passed += 1;
|
||||
console.log(`✓ ${path.relative(projectRoot, file)}`);
|
||||
}
|
||||
|
||||
console.log(`Business state tests passed: ${passed}/${testFiles.length}`);
|
||||
Reference in New Issue
Block a user