Files
rose-ash/shared/static/scripts/sx-test-runner.js
giles 7eb158c79f Add live browser test runner to /specs/testing page
sx-browser.js evaluates test.sx directly in the browser — click
"Run 81 tests" to see SX test itself. Uses the same Sx global that
rendered the page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 11:00:37 +00:00

97 lines
3.8 KiB
JavaScript

// sx-test-runner.js — Run test.sx in the browser using sx-browser.js.
// Loaded on the /specs/testing page. Uses the Sx global.
(function() {
var NIL = Sx.NIL;
function isNil(x) { return x === NIL || x === null || x === undefined; }
function deepEqual(a, b) {
if (a === b) return true;
if (isNil(a) && isNil(b)) return true;
if (typeof a !== typeof b) return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
for (var i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
return true;
}
if (a && typeof a === "object" && b && typeof b === "object") {
var ka = Object.keys(a), kb = Object.keys(b);
if (ka.length !== kb.length) return false;
for (var j = 0; j < ka.length; j++) if (!deepEqual(a[ka[j]], b[ka[j]])) return false;
return true;
}
return false;
}
window.sxRunTests = function(srcId, outId, btnId) {
var src = document.getElementById(srcId).textContent;
var out = document.getElementById(outId);
var btn = document.getElementById(btnId);
var stack = [], passed = 0, failed = 0, num = 0, lines = [];
var env = {
"try-call": function(thunk) {
try {
Sx.eval([thunk], env);
return { ok: true };
} catch(e) {
return { ok: false, error: e.message || String(e) };
}
},
"report-pass": function(name) {
num++; passed++;
lines.push("ok " + num + " - " + stack.concat([name]).join(" > "));
},
"report-fail": function(name, error) {
num++; failed++;
lines.push("not ok " + num + " - " + stack.concat([name]).join(" > "));
lines.push(" # " + error);
},
"push-suite": function(name) { stack.push(name); },
"pop-suite": function() { stack.pop(); },
"equal?": function(a, b) { return deepEqual(a, b); },
"eq?": function(a, b) { return a === b; },
"boolean?": function(x) { return typeof x === "boolean"; },
"string-length": function(s) { return String(s).length; },
"substring": function(s, start, end) { return String(s).slice(start, end); },
"string-contains?": function(s, n) { return String(s).indexOf(n) !== -1; },
"upcase": function(s) { return String(s).toUpperCase(); },
"downcase": function(s) { return String(s).toLowerCase(); },
"reverse": function(c) { return c ? c.slice().reverse() : []; },
"flatten": function(c) {
var r = [];
for (var i = 0; i < (c||[]).length; i++) {
if (Array.isArray(c[i])) for (var j = 0; j < c[i].length; j++) r.push(c[i][j]);
else r.push(c[i]);
}
return r;
},
"has-key?": function(d, k) { return d && typeof d === "object" && k in d; },
"append": function(c, x) { return Array.isArray(x) ? (c||[]).concat(x) : (c||[]).concat([x]); },
};
try {
var t0 = performance.now();
var exprs = Sx.parseAll(src);
for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);
var elapsed = Math.round(performance.now() - t0);
lines.push("");
lines.push("1.." + num);
lines.push("# tests " + (passed + failed));
lines.push("# pass " + passed);
if (failed > 0) lines.push("# fail " + failed);
lines.push("# time " + elapsed + "ms");
} catch(e) {
lines.push("");
lines.push("FATAL: " + (e.message || String(e)));
}
out.textContent = lines.join("\n");
out.style.display = "block";
btn.textContent = passed + "/" + (passed + failed) + " passed" + (failed === 0 ? "" : " (" + failed + " failed)");
btn.className = failed > 0
? "px-4 py-2 rounded-md bg-red-600 text-white font-medium text-sm cursor-default"
: "px-4 py-2 rounded-md bg-green-600 text-white font-medium text-sm cursor-default";
};
})();