/** * Minimal in-browser test runner (no npm / no Node). * Provides describe/it/assert and renders results into the document. */ (() => { 'use strict'; const suites = []; let currentSuite = null; const results = []; /** * @param {string} name * @param {() => void} fn */ function describe(name, fn) { currentSuite = { name, tests: [] }; suites.push(currentSuite); fn(); currentSuite = null; } /** * @param {string} name * @param {() => void|Promise} fn */ function it(name, fn) { if (!currentSuite) throw new Error('it() outside describe()'); currentSuite.tests.push({ name, fn }); } class AssertionError extends Error {} /** * @param {*} actual * @param {*} expected * @param {string} [message] */ function assertEqual(actual, expected, message) { const a = JSON.stringify(actual); const b = JSON.stringify(expected); if (a !== b) { throw new AssertionError(message || `expected ${b}, got ${a}`); } } /** * @param {*} actual * @param {string} [message] */ function assertTrue(actual, message) { if (actual !== true) { throw new AssertionError(message || `expected true, got ${JSON.stringify(actual)}`); } } /** * @param {*} actual * @param {string} [message] */ function assertFalse(actual, message) { if (actual !== false) { throw new AssertionError(message || `expected false, got ${JSON.stringify(actual)}`); } } /** * @param {number} actual * @param {number} expected * @param {number} epsilon * @param {string} [message] */ function assertCloseTo(actual, expected, epsilon, message) { if (Math.abs(actual - expected) > epsilon) { throw new AssertionError(message || `expected ~${expected}, got ${actual}`); } } /** * @param {Function} fn * @param {string} [message] */ async function assertThrowsAsync(fn, message) { try { await fn(); } catch { return; } throw new AssertionError(message || 'expected function to throw'); } async function run() { const root = document.getElementById('results'); const summary = document.getElementById('summary'); let passed = 0; let failed = 0; for (const suite of suites) { const suiteEl = document.createElement('section'); const heading = document.createElement('h2'); heading.textContent = suite.name; suiteEl.append(heading); for (const test of suite.tests) { const row = document.createElement('div'); row.className = 'test'; try { await test.fn(); passed++; row.classList.add('pass'); row.textContent = `✔ ${test.name}`; } catch (error) { failed++; row.classList.add('fail'); row.textContent = `✘ ${test.name} — ${error && error.message ? error.message : String(error)}`; } suiteEl.append(row); } root.append(suiteEl); } summary.textContent = `${passed} passed, ${failed} failed`; summary.className = failed === 0 ? 'all-pass' : 'has-failures'; document.title = failed === 0 ? 'PASS - Tests' : 'FAIL - Tests'; } window.TestRunner = { describe, it, assertEqual, assertTrue, assertFalse, assertCloseTo, assertThrowsAsync, run }; })();