Files
rose-ash/hosts/javascript/run_tests.js
giles 4b21efc43c
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 9s
JS test harness: 375/469 spec tests pass with full build
- Add --full flag for full-spec build (includes continuations + types)
- Add types module to JS SPEC_MODULES
- 375 tests pass on JavaScript, 94 remaining failures are:
  29 type platform stubs, 14 render format, 6 continuation aliases,
  5 type system platform, 4 string primitive aliases
- Full test build: hosts/javascript/cli.py --extensions continuations
  --spec-modules types --output sx-full-test.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 10:46:50 +00:00

210 lines
7.1 KiB
JavaScript

#!/usr/bin/env node
/**
* Run SX spec tests in Node.js using the bootstrapped evaluator.
*
* Usage:
* node hosts/javascript/run_tests.js # all spec tests
* node hosts/javascript/run_tests.js test-primitives # specific test
*/
const fs = require("fs");
const path = require("path");
// Provide globals that sx-browser.js expects
global.window = global;
global.addEventListener = () => {};
global.self = global;
global.document = {
createElement: () => ({ style: {}, setAttribute: () => {}, appendChild: () => {} }),
head: { appendChild: () => {} },
body: { appendChild: () => {} },
querySelector: () => null,
querySelectorAll: () => [],
createTextNode: (s) => ({ textContent: s }),
addEventListener: () => {},
};
global.localStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
global.CustomEvent = class CustomEvent { constructor(n, o) { this.type = n; this.detail = (o||{}).detail||{}; } };
global.MutationObserver = class { observe() {} disconnect() {} };
global.requestIdleCallback = (fn) => setTimeout(fn, 0);
global.matchMedia = () => ({ matches: false });
global.navigator = { serviceWorker: { register: () => Promise.resolve() } };
global.location = { href: "", pathname: "/", hostname: "localhost" };
global.history = { pushState: () => {}, replaceState: () => {} };
global.fetch = () => Promise.resolve({ ok: true, text: () => Promise.resolve("") });
global.setTimeout = setTimeout;
global.clearTimeout = clearTimeout;
global.console = console;
// Load the bootstrapped evaluator
// Use --full flag to load a full-spec build (if available)
const fullBuild = process.argv.includes("--full");
const jsPath = fullBuild
? path.join(__dirname, "..", "..", "shared", "static", "scripts", "sx-full-test.js")
: path.join(__dirname, "..", "..", "shared", "static", "scripts", "sx-browser.js");
if (fullBuild && !fs.existsSync(jsPath)) {
console.error("Full test build not found. Run: python3 hosts/javascript/cli.py --extensions continuations --spec-modules types --output shared/static/scripts/sx-full-test.js");
process.exit(1);
}
const Sx = require(jsPath);
if (!Sx || !Sx.parse) {
console.error("Failed to load Sx evaluator");
process.exit(1);
}
// Test infrastructure
let passCount = 0;
let failCount = 0;
const suiteStack = [];
// Build env with all primitives + spec functions
const env = Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {};
// Additional test helpers needed by spec tests
env["sx-parse"] = function(s) { return Sx.parse(s); };
env["sx-parse-one"] = function(s) { const r = Sx.parse(s); return r && r.length > 0 ? r[0] : null; };
env["test-env"] = function() { return Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {}; };
env["cek-eval"] = function(s) {
const parsed = Sx.parse(s);
if (!parsed || parsed.length === 0) return null;
return Sx.eval(parsed[0], Sx.getEnv ? Object.assign({}, Sx.getEnv()) : {});
};
env["eval-expr-cek"] = function(expr, e) { return Sx.eval(expr, e || env); };
env["env-get"] = function(e, k) { return e && e[k] !== undefined ? e[k] : null; };
env["env-has?"] = function(e, k) { return e && k in e; };
env["env-set!"] = function(e, k, v) { if (e) e[k] = v; return v; };
env["env-extend"] = function(e) { return Object.assign({}, e); };
env["env-merge"] = function(a, b) { return Object.assign({}, a, b); };
// Deep equality
function deepEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return a == b;
if (typeof a !== typeof b) return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((v, i) => deepEqual(v, b[i]));
}
if (typeof a === "object") {
const ka = Object.keys(a).filter(k => k !== "_nil");
const kb = Object.keys(b).filter(k => k !== "_nil");
if (ka.length !== kb.length) return false;
return ka.every(k => deepEqual(a[k], b[k]));
}
return false;
}
env["equal?"] = deepEqual;
env["identical?"] = function(a, b) { return a === b; };
// Continuation support
env["make-continuation"] = function(fn) {
const c = { fn: fn, _continuation: true, call: function(v) { return fn(v); } };
return c;
};
env["continuation?"] = function(x) { return x != null && x._continuation === true; };
env["continuation-fn"] = function(c) { return c.fn; };
// Render helpers
if (Sx.renderToHtml) {
env["render-html"] = function(expr, e) { return Sx.renderToHtml(expr, e || env); };
}
// Type system helpers — available when types module is included
env["test-prim-param-types"] = function(name, types) { return null; };
env["component-param-types"] = function(c) { return c && c._paramTypes ? c._paramTypes : null; };
env["component-set-param-types!"] = function(c, t) { if (c) c._paramTypes = t; return null; };
// Platform test functions
env["try-call"] = function(thunk) {
try {
Sx.eval([thunk], env);
return { ok: true };
} catch (e) {
return { ok: false, error: e.message || String(e) };
}
};
env["report-pass"] = function(name) {
passCount++;
const ctx = suiteStack.join(" > ");
console.log(` PASS: ${ctx} > ${name}`);
return null;
};
env["report-fail"] = function(name, error) {
failCount++;
const ctx = suiteStack.join(" > ");
console.log(` FAIL: ${ctx} > ${name}: ${error}`);
return null;
};
env["push-suite"] = function(name) {
suiteStack.push(name);
console.log(`${" ".repeat(suiteStack.length - 1)}Suite: ${name}`);
return null;
};
env["pop-suite"] = function() {
suiteStack.pop();
return null;
};
// Load test framework
const projectDir = path.join(__dirname, "..", "..");
const specTests = path.join(projectDir, "spec", "tests");
const webTests = path.join(projectDir, "web", "tests");
const frameworkSrc = fs.readFileSync(path.join(specTests, "test-framework.sx"), "utf8");
const frameworkExprs = Sx.parse(frameworkSrc);
for (const expr of frameworkExprs) {
Sx.eval(expr, env);
}
// Determine which tests to run
const args = process.argv.slice(2).filter(a => !a.startsWith("--"));
let testFiles = [];
if (args.length > 0) {
// Specific test files
for (const arg of args) {
const name = arg.endsWith(".sx") ? arg : `${arg}.sx`;
const specPath = path.join(specTests, name);
const webPath = path.join(webTests, name);
if (fs.existsSync(specPath)) testFiles.push(specPath);
else if (fs.existsSync(webPath)) testFiles.push(webPath);
else console.error(`Test file not found: ${name}`);
}
} else {
// All spec tests
for (const f of fs.readdirSync(specTests).sort()) {
if (f.startsWith("test-") && f.endsWith(".sx") && f !== "test-framework.sx") {
testFiles.push(path.join(specTests, f));
}
}
}
// Run tests
for (const testFile of testFiles) {
const name = path.basename(testFile);
console.log("=" .repeat(60));
console.log(`Running ${name}`);
console.log("=" .repeat(60));
try {
const src = fs.readFileSync(testFile, "utf8");
const exprs = Sx.parse(src);
for (const expr of exprs) {
Sx.eval(expr, env);
}
} catch (e) {
console.error(`ERROR in ${name}: ${e.message}`);
failCount++;
}
}
// Summary
console.log("=" .repeat(60));
console.log(`Results: ${passCount} passed, ${failCount} failed`);
console.log("=" .repeat(60));
process.exit(failCount > 0 ? 1 : 0);