/** * Minimal Test Runner for Browser * Run tests by opening tests/index.html in browser */ const results = { passed: 0, failed: 0, tests: [] } /** * Define a test * @param {string} name - Test name * @param {Function} fn - Test function */ export function test(name, fn) { try { fn() results.passed++ results.tests.push({ name, status: 'passed' }) console.log(`✓ ${name}`) } catch (error) { results.failed++ results.tests.push({ name, status: 'failed', error: error.message }) console.error(`✗ ${name}`) console.error(` ${error.message}`) } } /** * Group tests * @param {string} name - Group name * @param {Function} fn - Function containing tests */ export function describe(name, fn) { console.group(name) fn() console.groupEnd() } /** * Assert equality * @param {*} actual * @param {*} expected * @param {string} [message] */ export function assertEquals(actual, expected, message = '') { if (actual !== expected) { throw new Error( message || `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}` ) } } /** * Assert truthy * @param {*} value * @param {string} [message] */ export function assertTrue(value, message = '') { if (!value) { throw new Error(message || `Expected truthy value, got ${value}`) } } /** * Assert falsy * @param {*} value * @param {string} [message] */ export function assertFalse(value, message = '') { if (value) { throw new Error(message || `Expected falsy value, got ${value}`) } } /** * Assert throws * @param {Function} fn * @param {string} [message] */ export function assertThrows(fn, message = '') { try { fn() throw new Error(message || 'Expected function to throw') } catch (e) { if (e.message === (message || 'Expected function to throw')) { throw e } } } /** * Assert deep equality for objects/arrays * @param {*} actual * @param {*} expected * @param {string} [message] */ export function assertDeepEquals(actual, expected, message = '') { const actualStr = JSON.stringify(actual) const expectedStr = JSON.stringify(expected) if (actualStr !== expectedStr) { throw new Error( message || `Expected ${expectedStr}, got ${actualStr}` ) } } /** * Assert inequality * @param {*} actual * @param {*} expected * @param {string} [message] */ export function assertNotEquals(actual, expected, message = '') { if (actual === expected) { throw new Error( message || `Expected values to differ, but both are ${JSON.stringify(actual)}` ) } } const asyncQueue = [] /** * Define an async test * @param {string} name - Test name * @param {Function} fn - Async test function */ export function asyncTest(name, fn) { asyncQueue.push({ name, fn }) } /** * Run all queued async tests sequentially * @returns {Promise} */ export async function runAsyncTests() { for (const { name, fn } of asyncQueue) { try { await fn() results.passed++ results.tests.push({ name, status: 'passed' }) console.log(`✓ ${name}`) } catch (error) { results.failed++ results.tests.push({ name, status: 'failed', error: error.message }) console.error(`✗ ${name}`) console.error(` ${error.message}`) } } asyncQueue.length = 0 } /** * Get test results * @returns {Object} */ export function getResults() { return { ...results } } /** * Render results to DOM * @param {HTMLElement} container */ export function renderResults(container) { const summary = document.createElement('div') summary.className = 'test-summary' summary.innerHTML = `

Test Results

Passed: ${results.passed}

Failed: ${results.failed}

` const list = document.createElement('ul') list.className = 'test-list' results.tests.forEach(t => { const li = document.createElement('li') li.className = t.status li.innerHTML = t.status === 'passed' ? `✓ ${t.name}` : `✗ ${t.name}
${t.error}` list.appendChild(li) }) container.appendChild(summary) container.appendChild(list) }