diff options
Diffstat (limited to 'tests/runner.js')
| -rw-r--r-- | tests/runner.js | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/tests/runner.js b/tests/runner.js new file mode 100644 index 0000000..bfafa10 --- /dev/null +++ b/tests/runner.js @@ -0,0 +1,128 @@ +/** + * 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<void>} 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 }; +})();
\ No newline at end of file |
