// 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; } 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; } // --- Platform functions shared across all specs --- function makeEnv() { 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(); }, // 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"; }, "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]); }, "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; } } function loadDepsFromBootstrap(env) { if (Sx.scanRefs) { env["scan-refs"] = Sx.scanRefs; env["scan-components-from-source"] = Sx.scanComponentsFromSource; env["transitive-deps"] = Sx.transitiveDeps; env["compute-all-deps"] = Sx.computeAllDeps; env["components-needed"] = Sx.componentsNeeded; env["page-component-bundle"] = Sx.pageComponentBundle; env["page-css-classes"] = Sx.pageCssClasses; env["scan-io-refs"] = Sx.scanIoRefs; env["transitive-io-refs"] = Sx.transitiveIoRefs; env["compute-all-io-refs"] = Sx.computeAllIoRefs; env["component-pure?"] = Sx.componentPure_p; env["test-env"] = function() { return env; }; } } function loadEngineFromBootstrap(env) { if (Sx.parseTime) { env["parse-time"] = Sx.parseTime; env["parse-trigger-spec"] = Sx.parseTriggerSpec; env["default-trigger"] = Sx.defaultTrigger; env["parse-swap-spec"] = Sx.parseSwapSpec; env["parse-retry-spec"] = Sx.parseRetrySpec; env["next-retry-ms"] = function(cur, cap) { return Math.min(cur * 2, cap); }; env["filter-params"] = Sx.filterParams; } } // --- 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(); evalSource(src, 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"; }; // --- Modular runner (per-spec or all) --- var SPECS = { "eval": { needs: [] }, "parser": { needs: ["sx-parse"] }, "router": { needs: [] }, "render": { needs: ["render-html"] }, "deps": { needs: [] }, "engine": { needs: [] }, }; 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 module functions from bootstrap if (sn === "router") loadRouterFromBootstrap(ctx.env); if (sn === "deps") loadDepsFromBootstrap(ctx.env); if (sn === "engine") loadEngineFromBootstrap(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"; }; })();