Add Testing as top-level docs section with per-module specs
New /testing/ section with 6 pages: overview (all specs), evaluator, parser, router, renderer, and runners. Each page runs tests server-side (Python) and offers a browser "Run tests" button (JS). Modular browser runner (sxRunModularTests) loads framework + per-spec sources from DOM. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// sx-test-runner.js — Run test.sx in the browser using sx-browser.js.
|
||||
// Loaded on the /specs/testing page. Uses the Sx global.
|
||||
// sx-test-runner.js — Run SX test specs in the browser using sx-browser.js.
|
||||
// Supports both legacy (monolithic test.sx) and modular (per-spec) modes.
|
||||
(function() {
|
||||
var NIL = Sx.NIL;
|
||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||
@@ -21,13 +21,9 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
window.sxRunTests = function(srcId, outId, btnId) {
|
||||
var src = document.getElementById(srcId).textContent;
|
||||
var out = document.getElementById(outId);
|
||||
var btn = document.getElementById(btnId);
|
||||
|
||||
// --- Platform functions shared across all specs ---
|
||||
function makeEnv() {
|
||||
var stack = [], passed = 0, failed = 0, num = 0, lines = [];
|
||||
|
||||
var env = {
|
||||
"try-call": function(thunk) {
|
||||
try {
|
||||
@@ -49,6 +45,7 @@
|
||||
"push-suite": function(name) { stack.push(name); },
|
||||
"pop-suite": function() { stack.pop(); },
|
||||
|
||||
// Primitives that sx-browser.js may not expose in env
|
||||
"equal?": function(a, b) { return deepEqual(a, b); },
|
||||
"eq?": function(a, b) { return a === b; },
|
||||
"boolean?": function(x) { return typeof x === "boolean"; },
|
||||
@@ -68,28 +65,159 @@
|
||||
},
|
||||
"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]); },
|
||||
"for-each-indexed": function(f, coll) {
|
||||
for (var i = 0; i < (coll||[]).length; i++) Sx.eval([f, i, coll[i]], env);
|
||||
},
|
||||
"for-each": function(f, coll) {
|
||||
for (var i = 0; i < (coll||[]).length; i++) Sx.eval([f, coll[i]], env);
|
||||
},
|
||||
"dict-set!": function(d, k, v) { if (d) d[k] = v; },
|
||||
"dict-has?": function(d, k) { return d && typeof d === "object" && k in d; },
|
||||
"dict-get": function(d, k) { return d ? d[k] : undefined; },
|
||||
"starts-with?": function(s, prefix) { return String(s).indexOf(prefix) === 0; },
|
||||
"ends-with?": function(s, suffix) { var str = String(s); return str.indexOf(suffix) === str.length - suffix.length; },
|
||||
"slice": function(s, start, end) { return end !== undefined ? s.slice(start, end) : s.slice(start); },
|
||||
"inc": function(n) { return n + 1; },
|
||||
"append!": function(arr, item) { if (Array.isArray(arr)) arr.push(item); },
|
||||
"dict": function() { return {}; },
|
||||
|
||||
// --- Parser platform functions ---
|
||||
"sx-parse": function(source) { return Sx.parseAll(source); },
|
||||
"sx-serialize": function(val) {
|
||||
if (val === NIL || val === null || val === undefined) return "nil";
|
||||
if (typeof val === "boolean") return val ? "true" : "false";
|
||||
if (typeof val === "number") return String(val);
|
||||
if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
||||
if (val && (val._sym || val._sx_symbol)) return val.name;
|
||||
if (val && (val._kw || val._sx_keyword)) return ":" + val.name;
|
||||
if (Array.isArray(val)) return "(" + val.map(function(x) { return env["sx-serialize"](x); }).join(" ") + ")";
|
||||
if (val && typeof val === "object") {
|
||||
var parts = [];
|
||||
Object.keys(val).forEach(function(k) { parts.push(":" + k); parts.push(env["sx-serialize"](val[k])); });
|
||||
return "{" + parts.join(" ") + "}";
|
||||
}
|
||||
return String(val);
|
||||
},
|
||||
"make-symbol": function(name) { return Sx.sym ? Sx.sym(name) : { _sx_symbol: true, name: name, toString: function() { return name; } }; },
|
||||
"make-keyword": function(name) { return Sx.kw ? Sx.kw(name) : { _sx_keyword: true, name: name, toString: function() { return name; } }; },
|
||||
"symbol-name": function(s) { return s && s.name ? s.name : String(s); },
|
||||
"keyword-name": function(k) { return k && k.name ? k.name : String(k); },
|
||||
|
||||
// --- Render platform function ---
|
||||
"render-html": function(sxSource) {
|
||||
if (!Sx.renderToHtml) throw new Error("render-to-html not available");
|
||||
var exprs = Sx.parseAll(sxSource);
|
||||
var result = "";
|
||||
for (var i = 0; i < exprs.length; i++) result += Sx.renderToHtml(exprs[i], env);
|
||||
return result;
|
||||
},
|
||||
};
|
||||
return { env: env, getResults: function() { return { passed: passed, failed: failed, num: num, lines: lines }; } };
|
||||
}
|
||||
|
||||
function evalSource(src, env) {
|
||||
var exprs = Sx.parseAll(src);
|
||||
for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);
|
||||
}
|
||||
|
||||
function loadRouterFromBootstrap(env) {
|
||||
if (Sx.splitPathSegments) {
|
||||
env["split-path-segments"] = Sx.splitPathSegments;
|
||||
env["parse-route-pattern"] = Sx.parseRoutePattern;
|
||||
env["match-route-segments"] = Sx.matchRouteSegments;
|
||||
env["match-route"] = Sx.matchRoute;
|
||||
env["find-matching-route"] = Sx.findMatchingRoute;
|
||||
env["make-route-segment"] = Sx.makeRouteSegment;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Legacy runner (monolithic test.sx) ---
|
||||
window.sxRunTests = function(srcId, outId, btnId) {
|
||||
var src = document.getElementById(srcId).textContent;
|
||||
var out = document.getElementById(outId);
|
||||
var btn = document.getElementById(btnId);
|
||||
var ctx = makeEnv();
|
||||
|
||||
try {
|
||||
var t0 = performance.now();
|
||||
var exprs = Sx.parseAll(src);
|
||||
for (var i = 0; i < exprs.length; i++) Sx.eval(exprs[i], env);
|
||||
evalSource(src, ctx.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");
|
||||
var r = ctx.getResults();
|
||||
r.lines.push("");
|
||||
r.lines.push("1.." + r.num);
|
||||
r.lines.push("# tests " + (r.passed + r.failed));
|
||||
r.lines.push("# pass " + r.passed);
|
||||
if (r.failed > 0) r.lines.push("# fail " + r.failed);
|
||||
r.lines.push("# time " + elapsed + "ms");
|
||||
} catch(e) {
|
||||
lines.push("");
|
||||
lines.push("FATAL: " + (e.message || String(e)));
|
||||
var r = ctx.getResults();
|
||||
r.lines.push("");
|
||||
r.lines.push("FATAL: " + (e.message || String(e)));
|
||||
}
|
||||
|
||||
out.textContent = lines.join("\n");
|
||||
out.textContent = r.lines.join("\n");
|
||||
out.style.display = "block";
|
||||
btn.textContent = passed + "/" + (passed + failed) + " passed" + (failed === 0 ? "" : " (" + failed + " failed)");
|
||||
btn.className = failed > 0
|
||||
btn.textContent = r.passed + "/" + (r.passed + r.failed) + " passed" + (r.failed === 0 ? "" : " (" + r.failed + " failed)");
|
||||
btn.className = r.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";
|
||||
};
|
||||
|
||||
// --- Modular runner (per-spec or all) ---
|
||||
var SPECS = {
|
||||
"eval": { needs: [] },
|
||||
"parser": { needs: ["sx-parse"] },
|
||||
"router": { needs: [] },
|
||||
"render": { needs: ["render-html"] },
|
||||
};
|
||||
|
||||
window.sxRunModularTests = function(specName, outId, btnId) {
|
||||
var out = document.getElementById(outId);
|
||||
var btn = document.getElementById(btnId);
|
||||
var ctx = makeEnv();
|
||||
var specs = specName === "all" ? Object.keys(SPECS) : [specName];
|
||||
|
||||
try {
|
||||
var t0 = performance.now();
|
||||
|
||||
// Load framework
|
||||
var fwEl = document.getElementById("test-framework-source");
|
||||
if (fwEl) {
|
||||
evalSource(fwEl.textContent, ctx.env);
|
||||
}
|
||||
|
||||
for (var si = 0; si < specs.length; si++) {
|
||||
var sn = specs[si];
|
||||
if (!SPECS[sn]) continue;
|
||||
|
||||
// Load router from bootstrap if needed
|
||||
if (sn === "router") loadRouterFromBootstrap(ctx.env);
|
||||
|
||||
// Find spec source — either per-spec textarea or embedded in overview
|
||||
var specEl = document.getElementById("test-spec-" + sn);
|
||||
if (specEl) {
|
||||
evalSource(specEl.textContent, ctx.env);
|
||||
}
|
||||
}
|
||||
|
||||
var elapsed = Math.round(performance.now() - t0);
|
||||
var r = ctx.getResults();
|
||||
r.lines.push("");
|
||||
r.lines.push("1.." + r.num);
|
||||
r.lines.push("# tests " + (r.passed + r.failed));
|
||||
r.lines.push("# pass " + r.passed);
|
||||
if (r.failed > 0) r.lines.push("# fail " + r.failed);
|
||||
r.lines.push("# time " + elapsed + "ms");
|
||||
} catch(e) {
|
||||
var r = ctx.getResults();
|
||||
r.lines.push("");
|
||||
r.lines.push("FATAL: " + (e.message || String(e)));
|
||||
}
|
||||
|
||||
out.textContent = r.lines.join("\n");
|
||||
out.style.display = "block";
|
||||
btn.textContent = r.passed + "/" + (r.passed + r.failed) + " passed" + (r.failed === 0 ? "" : " (" + r.failed + " failed)");
|
||||
btn.className = r.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";
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user