mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 16:45:22 +00:00
131 lines
4.4 KiB
JavaScript
131 lines
4.4 KiB
JavaScript
// Tiny shared harness for challenge pages.
|
|
// Each page calls Challenge.init({id, instructions}) then Challenge.pass()/fail()
|
|
// based on its own listeners.
|
|
(function () {
|
|
const events = [];
|
|
const state = {
|
|
id: null,
|
|
verdict: "PENDING",
|
|
reason: [],
|
|
events,
|
|
details: [],
|
|
thresholds: {},
|
|
};
|
|
|
|
function render() {
|
|
const el = document.getElementById("__verdict");
|
|
if (!el) return;
|
|
el.textContent = state.verdict;
|
|
el.dataset.verdict = state.verdict;
|
|
el.style.background =
|
|
state.verdict === "PASS" ? "#1f7a1f" :
|
|
state.verdict === "FAIL" ? "#a11" :
|
|
state.verdict === "SKIP" ? "#76520b" :
|
|
state.verdict === "WARN" ? "#6b5d00" : "#444";
|
|
el.style.color = "#fff";
|
|
const r = document.getElementById("__reason");
|
|
if (r) r.textContent = state.reason.join("\n");
|
|
}
|
|
|
|
function log(name, detail) {
|
|
events.push({ t: performance.now(), name, ...detail });
|
|
if (events.length > 500) events.shift();
|
|
}
|
|
|
|
const Challenge = {
|
|
init({ id, instructions, thresholds = {} }) {
|
|
state.id = id;
|
|
state.thresholds = parseThresholds(thresholds);
|
|
document.title = `[${state.verdict}] ${id}`;
|
|
const root = document.body;
|
|
const bar = document.createElement("div");
|
|
bar.style.cssText =
|
|
"position:sticky;top:0;background:#111;color:#eee;padding:8px 12px;font:13px monospace;border-bottom:1px solid #333;z-index:9999";
|
|
bar.innerHTML = `
|
|
<b>${id}</b>
|
|
<span id="__verdict" style="margin-left:8px;padding:2px 8px;border-radius:4px;background:#444">PENDING</span>
|
|
<span style="margin-left:12px;opacity:.7">${instructions}</span>
|
|
<pre id="__reason" style="white-space:pre-wrap;margin:6px 0 0;color:#bbb;font:12px monospace"></pre>
|
|
`;
|
|
root.insertBefore(bar, root.firstChild);
|
|
window.__challenge = id;
|
|
window.__verdict = state.verdict;
|
|
window.__reason = state.reason;
|
|
window.__events = state.events;
|
|
render();
|
|
},
|
|
pass(...reasons) {
|
|
if (state.verdict === "FAIL") return; // sticky
|
|
state.verdict = "PASS";
|
|
state.reason.push(...reasons.map((r) => "✓ " + r));
|
|
window.__verdict = state.verdict;
|
|
document.title = `[PASS] ${state.id}`;
|
|
persist(); render();
|
|
},
|
|
fail(...reasons) {
|
|
state.verdict = "FAIL";
|
|
state.reason.push(...reasons.map((r) => "✗ " + r));
|
|
window.__verdict = state.verdict;
|
|
document.title = `[FAIL] ${state.id}`;
|
|
persist(); render();
|
|
},
|
|
skip(...reasons) {
|
|
if (state.verdict === "FAIL" || state.verdict === "PASS") return;
|
|
state.verdict = "SKIP";
|
|
state.reason.push(...reasons.map((r) => "↷ " + r));
|
|
window.__verdict = state.verdict;
|
|
document.title = `[SKIP] ${state.id}`;
|
|
persist(); render();
|
|
},
|
|
warn(...reasons) {
|
|
if (state.verdict === "FAIL" || state.verdict === "PASS") return;
|
|
state.verdict = "WARN";
|
|
state.reason.push(...reasons.map((r) => "! " + r));
|
|
window.__verdict = state.verdict;
|
|
document.title = `[WARN] ${state.id}`;
|
|
persist(); render();
|
|
},
|
|
partial({ name, pass, reason, data }) {
|
|
state.details.push({ name, pass: !!pass, reason: reason || "", data });
|
|
log("partial", { name, pass: !!pass, reason, data });
|
|
persist(); render();
|
|
return !!pass;
|
|
},
|
|
finishPartials() {
|
|
const failed = state.details.filter(d => !d.pass);
|
|
if (failed.length) Challenge.fail(...failed.map(d => `${d.name}: ${d.reason || "failed"}`));
|
|
else Challenge.pass(...state.details.map(d => `${d.name}: ok`));
|
|
},
|
|
getThreshold(name, fallback) {
|
|
return Object.prototype.hasOwnProperty.call(state.thresholds, name) ? state.thresholds[name] : fallback;
|
|
},
|
|
log,
|
|
state,
|
|
};
|
|
|
|
function persist() {
|
|
try {
|
|
localStorage.setItem(
|
|
"pi-chrome-suite:" + state.id,
|
|
JSON.stringify({ id: state.id, verdict: state.verdict, reason: state.reason, details: state.details, thresholds: state.thresholds, events: state.events.slice(-50), ts: Date.now() })
|
|
);
|
|
} catch {}
|
|
}
|
|
|
|
function parseThresholds(defaults) {
|
|
const out = { ...defaults };
|
|
try {
|
|
const qs = new URLSearchParams(location.search);
|
|
for (const [k, v] of qs) {
|
|
if (!k.startsWith("threshold.")) continue;
|
|
const key = k.slice("threshold.".length);
|
|
const num = Number(v);
|
|
out[key] = Number.isFinite(num) ? num : v;
|
|
}
|
|
} catch {}
|
|
return out;
|
|
}
|
|
|
|
window.Challenge = Challenge;
|
|
})();
|