/** * ICD Test Runner — UI * Render functions, suite dispatcher, provider setup panel, fixtures panel, export. */ (function () { 'use strict'; var T = window.ICD; if (!T) return; var $ = T.$; var esc = T.esc; // ─── Provider Setup UI Panel ────────────────────────────── T.renderProviderSetup = function () { if (!T.el.providerSetup) return; T.el.providerSetup.innerHTML = ''; var cardStyle = { background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', padding: '16px', marginBottom: '0' }; var card = $('div', { style: cardStyle }); // Header row var hdr = $('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' } }, [ $('div', { style: { fontSize: '13px', fontWeight: '600', color: 'var(--text)' } }, 'Provider Test Setup'), T.providerSetup.configured ? $('span', { className: 'badge badge-success', style: { fontSize: '11px' } }, esc(T.providerSetup.provider) + ' configured') : $('span', { style: { fontSize: '11px', color: 'var(--text-3)' } }, 'Not configured — optional') ]); card.appendChild(hdr); // Input row var inputRow = $('div', { style: { display: 'flex', gap: '10px', alignItems: 'flex-end', flexWrap: 'wrap' } }); // Provider select var selWrap = $('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px' } }, [ $('label', { style: { fontSize: '11px', color: 'var(--text-3)', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.5px' } }, 'Provider') ]); var sel = $('select', { style: { background: 'var(--bg-raised)', color: 'var(--text)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', padding: '7px 10px', fontSize: '13px', fontFamily: 'var(--font)', minWidth: '140px' } }); ['openai', 'anthropic', 'venice', 'openrouter'].forEach(function (p) { var opt = $('option', { value: p }, p); if (p === T.providerSetup.provider) opt.selected = true; sel.appendChild(opt); }); sel.addEventListener('change', function () { T.providerSetup.provider = sel.value; }); selWrap.appendChild(sel); inputRow.appendChild(selWrap); // API Key input var keyWrap = $('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px', flex: '1', minWidth: '240px' } }, [ $('label', { style: { fontSize: '11px', color: 'var(--text-3)', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.5px' } }, 'API Key') ]); var keyInput = $('input', { type: 'password', placeholder: 'sk-... / anthropic-... / paste key', style: { background: 'var(--bg-raised)', color: 'var(--text)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', padding: '7px 10px', fontSize: '13px', fontFamily: 'var(--mono)', width: '100%' } }); keyInput.value = T.providerSetup.apiKey; keyInput.addEventListener('input', function () { T.providerSetup.apiKey = keyInput.value; }); keyWrap.appendChild(keyInput); inputRow.appendChild(keyWrap); // Endpoint override (collapsed by default) var epWrap = $('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px', minWidth: '200px' } }, [ $('label', { style: { fontSize: '11px', color: 'var(--text-3)', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.5px' } }, 'Endpoint (optional)') ]); var epInput = $('input', { type: 'text', placeholder: T.PROVIDER_DEFAULTS[T.providerSetup.provider] || 'default', style: { background: 'var(--bg-raised)', color: 'var(--text)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', padding: '7px 10px', fontSize: '12px', fontFamily: 'var(--mono)', width: '100%' } }); epInput.value = T.providerSetup.endpoint; epInput.addEventListener('input', function () { T.providerSetup.endpoint = epInput.value; }); epWrap.appendChild(epInput); inputRow.appendChild(epWrap); // Configure button var cfgBtn = $('button', { className: T.providerSetup.configured ? 'btn-ghost' : 'btn-primary', style: { alignSelf: 'flex-end', whiteSpace: 'nowrap' }, onClick: function () { if (!T.providerSetup.apiKey) { if (window.sw && sw.toast) sw.toast('Paste an API key first', 'warning'); return; } T.providerSetup.configured = true; T.renderProviderSetup(); T.renderControls(); if (window.sw && sw.toast) sw.toast(T.providerSetup.provider + ' configured — run Provider tier', 'success'); } }, T.providerSetup.configured ? 'Reconfigure' : 'Configure'); inputRow.appendChild(cfgBtn); if (T.providerSetup.configured) { var clearBtn = $('button', { className: 'btn-ghost', style: { alignSelf: 'flex-end', color: 'var(--danger)', whiteSpace: 'nowrap' }, onClick: function () { T.providerSetup.apiKey = ''; T.providerSetup.configured = false; T.renderProviderSetup(); T.renderControls(); } }, 'Clear Key'); inputRow.appendChild(clearBtn); } card.appendChild(inputRow); // Security note card.appendChild($('div', { style: { fontSize: '11px', color: 'var(--text-3)', marginTop: '8px', fontStyle: 'italic' } }, 'Key stays in browser memory only — never persisted, cleaned up on page close. Creates temporary configs and deletes on teardown.')); T.el.providerSetup.appendChild(card); } T.renderShell = function () { T.mount.innerHTML = ''; T.mount.style.padding = '24px 32px'; T.mount.style.maxWidth = '1000px'; T.mount.style.margin = '0 auto'; // Top bar with user menu var topbar = $('div', { style: { display: 'flex', justifyContent: 'flex-end', marginBottom: '8px' } }); T.mount.appendChild(topbar); if (window.sw?.userMenu) window.sw.userMenu(topbar, { flyout: 'down' }); // Header var user = window.sw?.auth?.user || T.user || {}; var hdr = $('div', { style: { marginBottom: '24px' } }, [ $('h1', { style: { fontSize: '22px', fontWeight: '700', color: 'var(--text)', margin: '0 0 4px 0' } }, 'ICD Test Runner'), $('div', { style: { fontSize: '13px', color: 'var(--text-3)' } }, 'v' + (T.manifest.version || '0.28.0') + ' · ' + esc(user.username || 'unknown') + (user.role === 'admin' ? ' (admin)' : ' (user)')) ]); T.mount.appendChild(hdr); // Controls T.el.controls = $('div', { style: { display: 'flex', gap: '10px', marginBottom: '16px', flexWrap: 'wrap', alignItems: 'center' } }); T.mount.appendChild(T.el.controls); // Fixtures panel (admin only) if (T.user.role === 'admin') { T.el.fixtures = $('div', { style: { marginBottom: '16px' } }); T.mount.appendChild(T.el.fixtures); } // Provider setup panel T.el.providerSetup = $('div', { style: { marginBottom: '16px' } }); T.mount.appendChild(T.el.providerSetup); // Progress T.el.progress = $('div', { style: { marginBottom: '16px' } }); T.mount.appendChild(T.el.progress); // Summary T.el.summary = $('div', { style: { marginBottom: '20px' } }); T.mount.appendChild(T.el.summary); // Detail table T.el.detail = $('div', {}); T.mount.appendChild(T.el.detail); T.renderControls(); T.renderFixtures(); T.renderProviderSetup(); } T.renderControls = function () { T.el.controls.innerHTML = ''; var btnSmoke = $('button', { className: 'btn-primary', onClick: function () { T.runSuite('smoke'); } }, 'Smoke'); var btnCrud = $('button', { className: 'btn-secondary', onClick: function () { T.runSuite('crud'); } }, 'CRUD'); var btnAll = $('button', { className: 'btn-primary', onClick: function () { T.runSuite('all'); } }, 'Run All'); var btnClear = $('button', { className: 'btn-ghost', onClick: function () { T.results = []; T.renderProgress(); T.renderSummary(); T.renderDetail(); } }, 'Clear'); T.el.controls.appendChild(btnSmoke); T.el.controls.appendChild(btnCrud); // AuthZ button — only useful if admin (needs fixtures) if (T.user.role === 'admin') { var btnAuthz = $('button', { className: T.fixtures.ready ? 'btn-secondary' : 'btn-ghost', style: { opacity: T.fixtures.ready ? '1' : '0.5' }, onClick: function () { T.runSuite('authz'); } }, 'AuthZ'); T.el.controls.appendChild(btnAuthz); var btnSecurity = $('button', { className: T.fixtures.ready ? 'btn-secondary' : 'btn-ghost', style: { opacity: T.fixtures.ready ? '1' : '0.5', borderColor: T.fixtures.ready ? 'var(--danger)' : undefined, color: T.fixtures.ready ? 'var(--danger)' : undefined }, onClick: function () { T.runSuite('security'); } }, 'Security'); T.el.controls.appendChild(btnSecurity); } // Provider tier button — needs provider setup var btnProv = $('button', { className: T.providerSetup.configured ? 'btn-secondary' : 'btn-ghost', style: { opacity: T.providerSetup.configured ? '1' : '0.5' }, onClick: function () { T.runSuite('provider'); } }, 'Providers'); T.el.controls.appendChild(btnProv); // SDK tier button — tests switchboard-sdk.js contract var btnSdk = $('button', { className: (typeof window.sw !== 'undefined') ? 'btn-secondary' : 'btn-ghost', style: { opacity: (typeof window.sw !== 'undefined') ? '1' : '0.5' }, onClick: function () { T.runSuite('sdk'); } }, 'SDK'); T.el.controls.appendChild(btnSdk); // Packaging tier button — extension permission lifecycle (v0.29.0) var btnPkg = $('button', { className: 'btn-secondary', onClick: function () { T.runSuite('packaging'); } }, 'Packaging'); T.el.controls.appendChild(btnPkg); T.el.controls.appendChild(btnAll); T.el.controls.appendChild(btnClear); // Separator T.el.controls.appendChild($('div', { style: { width: '1px', height: '28px', background: 'var(--border)', flexShrink: '0' } })); // Export buttons var btnCopy = $('button', { className: 'btn-ghost', onClick: function () { T.exportReport('clipboard'); } }, 'Copy Report'); var btnDownload = $('button', { className: 'btn-ghost', onClick: function () { T.exportReport('download'); } }, 'Download'); T.el.controls.appendChild(btnCopy); T.el.controls.appendChild(btnDownload); // Fixture controls (admin) if (T.user.role === 'admin') { T.el.controls.appendChild($('div', { style: { width: '1px', height: '28px', background: 'var(--border)', flexShrink: '0' } })); if (!T.fixtures.ready) { T.el.controls.appendChild($('button', { className: 'btn-secondary', onClick: async function () { await T.provisionFixtures(); T.renderControls(); } }, 'Provision Test Users')); } else { T.el.controls.appendChild($('button', { className: 'btn-ghost', style: { color: 'var(--danger)' }, onClick: async function () { await T.teardownFixtures(); T.renderControls(); } }, 'Tear Down Fixtures')); } } } // ─── Fixtures Panel ───────────────────────────────────────── T.renderFixtures = function () { if (!T.el.fixtures) return; T.el.fixtures.innerHTML = ''; if (!T.fixtures.ready) return; var panel = $('div', { style: { background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', padding: '14px 18px', borderLeft: '3px solid var(--accent)' } }); panel.appendChild($('div', { style: { fontSize: '13px', fontWeight: '600', color: 'var(--text)', marginBottom: '10px' } }, 'Test Fixtures')); // Users table var tbl = $('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: '12px', fontFamily: 'var(--mono)' } }); var hdr = $('tr', { style: { borderBottom: '1px solid var(--border)' } }); ['Role', 'Username', 'Password', 'Token', 'ID'].forEach(function (h) { hdr.appendChild($('th', { style: { textAlign: 'left', padding: '4px 8px', color: 'var(--text-3)', fontSize: '10px', textTransform: 'uppercase', letterSpacing: '0.5px' } }, h)); }); tbl.appendChild(hdr); T.fixtures.users.forEach(function (u) { var tr = $('tr', { style: { borderBottom: '1px solid var(--border)' } }); tr.appendChild($('td', { style: { padding: '4px 8px', color: 'var(--accent)' } }, u.role)); tr.appendChild($('td', { style: { padding: '4px 8px', color: 'var(--text)' } }, u.username)); // Password with copy button var pwTd = $('td', { style: { padding: '4px 8px' } }); var pwSpan = $('span', { style: { color: 'var(--warning)', cursor: 'pointer' }, title: 'Click to copy' }, u.password); pwSpan.addEventListener('click', function () { navigator.clipboard.writeText(u.password); if (window.sw && sw.toast) sw.toast('Password copied', 'info'); }); pwTd.appendChild(pwSpan); tr.appendChild(pwTd); // Token status var tokenStatus = u.token ? 'yes' : 'FAILED'; var tokenColor = u.token ? 'var(--success)' : 'var(--danger)'; tr.appendChild($('td', { style: { padding: '4px 8px', color: tokenColor } }, tokenStatus)); // ID (truncated) tr.appendChild($('td', { style: { padding: '4px 8px', color: 'var(--text-3)' } }, (u.id || '').substring(0, 8) + '…')); tbl.appendChild(tr); }); panel.appendChild(tbl); // Team/group info var meta = []; if (T.fixtures.team) meta.push('Team: ' + T.fixtures.team.name + ' (' + T.fixtures.team.id.substring(0, 8) + '…)'); if (T.fixtures.group) meta.push('Group: ' + T.fixtures.group.name + ' (' + T.fixtures.group.id.substring(0, 8) + '…)'); if (meta.length > 0) { panel.appendChild($('div', { style: { marginTop: '8px', fontSize: '11px', color: 'var(--text-3)' } }, meta.join(' · '))); } T.el.fixtures.appendChild(panel); } // ─── Export ───────────────────────────────────────────────── function buildReport() { var now = new Date().toISOString(); var pass = T.results.filter(function (r) { return r.status === 'pass'; }).length; var fail = T.results.filter(function (r) { return r.status === 'fail'; }).length; var skip = T.results.filter(function (r) { return r.status === 'skip'; }).length; var total = T.results.length; var totalMs = T.results.reduce(function (s, r) { return s + r.duration; }, 0); var lines = []; lines.push('=== Switchboard Core ICD Test Report ==='); lines.push('Generated: ' + now); lines.push('Platform: v' + (T.manifest.version || '0.28.0')); lines.push('User: ' + T.user.username + ' (' + T.user.role + ')'); lines.push('URL: ' + location.href); lines.push(''); lines.push('--- Summary ---'); lines.push('Total: ' + total + ' Pass: ' + pass + ' Fail: ' + fail + (skip > 0 ? ' Skip: ' + skip : '') + ' Rate: ' + (total > 0 ? Math.round((pass / (total - skip)) * 100) : 0) + '% Duration: ' + totalMs + 'ms'); // Critical failures callout var criticals = T.results.filter(function (r) { return r.status === 'fail' && r.detail && r.detail.indexOf('CRITICAL') !== -1; }); if (criticals.length > 0) { lines.push(''); lines.push('!!! ' + criticals.length + ' CRITICAL SECURITY FAILURE(S) !!!'); criticals.forEach(function (r) { lines.push(' → [' + r.tier + '/' + r.domain + '] ' + r.name); lines.push(' ' + r.detail); }); } // Fixture info if (T.fixtures.ready) { lines.push(''); lines.push('--- Test Fixtures ---'); T.fixtures.users.forEach(function (u) { lines.push(' ' + pad(u.role, 8) + pad(u.username, 40) + (u.token ? 'token:OK' : 'token:FAIL')); }); if (T.fixtures.team) lines.push(' Team: ' + T.fixtures.team.name + ' (' + T.fixtures.team.id + ')'); if (T.fixtures.group) lines.push(' Group: ' + T.fixtures.group.name + ' (' + T.fixtures.group.id + ')'); } // Provider config info if (T.providerSetup.configured) { lines.push(''); lines.push('--- Provider Setup ---'); lines.push(' Type: ' + T.providerSetup.provider); lines.push(' Endpoint: ' + (T.providerSetup.endpoint || T.PROVIDER_DEFAULTS[T.providerSetup.provider] || 'default')); lines.push(' Key: ' + (T.providerSetup.apiKey ? T.providerSetup.apiKey.slice(0, 8) + '...' : 'none')); } lines.push(''); // Domain breakdown var domains = {}; T.results.forEach(function (r) { var k = r.domain; if (!domains[k]) domains[k] = { pass: 0, fail: 0, skip: 0, total: 0, ms: 0 }; domains[k].total++; domains[k].ms += r.duration; if (r.status === 'pass') domains[k].pass++; else if (r.status === 'skip') domains[k].skip++; else domains[k].fail++; }); lines.push('--- By Domain ---'); Object.keys(domains).forEach(function (d) { var s = domains[d]; var flag = s.fail > 0 ? 'FAIL' : (s.skip > 0 && s.pass === 0 ? 'SKIP' : 'OK'); lines.push(' ' + pad(d, 16) + pad(flag, 6) + pad(s.pass + '/' + s.total, 8) + s.ms + 'ms'); }); lines.push(''); // Failures only (if any) var failures = T.results.filter(function (r) { return r.status === 'fail'; }); if (failures.length > 0) { lines.push('--- Failures (' + failures.length + ') ---'); failures.forEach(function (r, i) { lines.push(' ' + (i + 1) + '. [' + r.tier + '/' + r.domain + '] ' + r.name); lines.push(' ' + r.detail); }); lines.push(''); } // Skipped (if any) var skipped = T.results.filter(function (r) { return r.status === 'skip'; }); if (skipped.length > 0) { lines.push('--- Skipped (' + skipped.length + ') ---'); skipped.forEach(function (r, i) { lines.push(' ' + (i + 1) + '. [' + r.tier + '/' + r.domain + '] ' + r.name); lines.push(' ' + r.detail); }); lines.push(''); } // Full detail table lines.push('--- Full Results ---'); lines.push(pad('STATUS', 8) + pad('TIER', 8) + pad('DOMAIN', 16) + pad('MS', 6) + 'TEST'); lines.push(repeat('-', 90)); T.results.forEach(function (r) { var statusStr = r.status === 'pass' ? 'PASS' : (r.status === 'skip' ? 'SKIP' : 'FAIL'); var line = pad(statusStr, 8) + pad(r.tier, 8) + pad(r.domain, 16) + pad(String(r.duration), 6) + r.name; if (r.status === 'fail' && r.detail) { line += '\n' + repeat(' ', 38) + '↳ ' + r.detail; } if (r.status === 'skip' && r.detail) { line += '\n' + repeat(' ', 38) + '↳ SKIP: ' + r.detail; } lines.push(line); }); lines.push(''); lines.push('=== END ==='); return lines.join('\n'); } function pad(s, w) { s = String(s); while (s.length < w) s += ' '; return s; } function repeat(ch, n) { var s = ''; for (var i = 0; i < n; i++) s += ch; return s; } T.exportReport = function (mode) { if (T.results.length === 0) { if (window.sw && sw.toast) sw.toast('No T.results to export — run a suite first', 'warning'); return; } var text = buildReport(); if (mode === 'clipboard') { navigator.clipboard.writeText(text).then(function () { if (window.sw && sw.toast) sw.toast('Report copied to clipboard', 'success'); }).catch(function () { // Fallback: select-all textarea clipboardFallback(text); }); } else if (mode === 'download') { var ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); var filename = 'icd-report-' + ts + '.txt'; var blob = new Blob([text], { type: 'text/plain' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); if (window.sw && sw.toast) sw.toast('Downloaded ' + filename, 'success'); } } function clipboardFallback(text) { var ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;left:-9999px;top:0;'; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); } catch (e) { /* give up */ } document.body.removeChild(ta); if (window.sw && sw.toast) sw.toast('Report copied (fallback)', 'info'); } T.runSuite = async function (which) { if (T.running) return; if (which === 'authz' && !T.fixtures.ready) { if (window.sw && sw.toast) sw.toast('Provision test fixtures first (button on the right)', 'warning'); return; } if (which === 'security' && !T.fixtures.ready) { if (window.sw && sw.toast) sw.toast('Provision test fixtures first (button on the right)', 'warning'); return; } if (which === 'provider' && !T.providerSetup.configured) { if (window.sw && sw.toast) sw.toast('Configure a provider (type + API key) in the panel above', 'warning'); return; } T.running = true; T.results = []; // Capture auth token before any test (needed for DELETE fallback) await T.captureAuthToken(); T.renderProgress(); T.renderSummary(); T.renderDetail(); try { if (which === 'smoke' || which === 'all') await T.runSmoke(); if (which === 'crud' || which === 'all') await T.runCrud(); if (which === 'authz' || (which === 'all' && T.fixtures.ready)) await T.runAuthz(); if (which === 'security' || (which === 'all' && T.fixtures.ready)) await T.runSecurity(); if (which === 'provider' || (which === 'all' && T.providerSetup.configured)) await T.runProviders(); if (which === 'sdk' || which === 'all') { if (typeof T.runSdk === 'function') await T.runSdk(); } if (which === 'packaging' || which === 'all') { if (typeof T.runPackaging === 'function') await T.runPackaging(); } } catch (e) { T.results.push({ tier: '?', domain: 'runner', name: 'FATAL', status: 'fail', duration: 0, detail: String(e) }); } // Always T.cleanup (CRUD-created resources, not fixtures) await T.runCleanup(); T.running = false; T.renderSummary(); T.renderDetail(); T.renderProgress(); if (typeof UI !== 'undefined') { var pass = T.results.filter(function (r) { return r.status === 'pass'; }).length; var fail = T.results.filter(function (r) { return r.status === 'fail'; }).length; var critical = T.results.filter(function (r) { return r.status === 'fail' && r.detail && r.detail.indexOf('CRITICAL') !== -1; }).length; var msg = pass + ' passed, ' + fail + ' failed'; if (critical > 0) msg += ' (' + critical + ' CRITICAL)'; if (window.sw && sw.toast) sw.toast(msg, critical > 0 ? 'error' : fail > 0 ? 'warning' : 'success'); } } T.renderProgress = function () { if (!T.el.progress) return; var pass = T.results.filter(function (r) { return r.status === 'pass'; }).length; var fail = T.results.filter(function (r) { return r.status === 'fail'; }).length; var skip = T.results.filter(function (r) { return r.status === 'skip'; }).length; var total = T.results.length; var countable = total - skip; var pct = countable > 0 ? Math.round((pass / countable) * 100) : 0; T.el.progress.innerHTML = ''; if (total === 0) return; // Progress bar var barOuter = $('div', { style: { height: '8px', borderRadius: '4px', background: 'var(--bg-raised)', overflow: 'hidden', marginBottom: '8px' } }); var barInner = $('div', { style: { height: '100%', borderRadius: '4px', width: pct + '%', background: fail > 0 ? 'var(--warning)' : 'var(--success)', transition: 'width 200ms ease' } }); barOuter.appendChild(barInner); T.el.progress.appendChild(barOuter); var label = $('div', { style: { fontSize: '12px', color: 'var(--text-2)' } }, (T.running ? 'Running... ' : '') + total + ' tests · ' + pass + ' pass · ' + fail + ' fail' + (skip > 0 ? ' · ' + skip + ' skip' : '') + ' · ' + pct + '%'); T.el.progress.appendChild(label); } T.renderSummary = function () { if (!T.el.summary) return; T.el.summary.innerHTML = ''; if (T.results.length === 0) return; // Group by domain var domains = {}; T.results.forEach(function (r) { var k = r.domain; if (!domains[k]) domains[k] = { pass: 0, fail: 0, skip: 0, total: 0, ms: 0 }; domains[k].total++; domains[k].ms += r.duration; if (r.status === 'pass') domains[k].pass++; else if (r.status === 'skip') domains[k].skip++; else domains[k].fail++; }); var grid = $('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))', gap: '8px' } }); Object.keys(domains).forEach(function (d) { var s = domains[d]; var borderColor = s.fail > 0 ? 'var(--danger)' : (s.skip > 0 ? 'var(--warning)' : 'var(--success)'); var label = s.pass + '/' + s.total + ' pass'; if (s.skip > 0) label += ' · ' + s.skip + ' skip'; label += ' · ' + s.ms + 'ms'; var card = $('div', { style: { background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', padding: '10px 14px', borderLeft: '3px solid ' + borderColor } }, [ $('div', { style: { fontSize: '13px', fontWeight: '600', color: 'var(--text)', marginBottom: '4px' } }, d), $('div', { style: { fontSize: '12px', color: 'var(--text-2)' } }, label) ]); grid.appendChild(card); }); T.el.summary.appendChild(grid); } T.renderDetail = function () { if (!T.el.detail) return; T.el.detail.innerHTML = ''; if (T.results.length === 0) return; var table = $('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: '13px', fontFamily: 'var(--mono)', background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius)' } }); // Header var thead = $('thead'); var trh = $('tr', { style: { borderBottom: '1px solid var(--border)' } }); ['', 'Tier', 'Domain', 'Test', 'ms', 'Detail'].forEach(function (h) { trh.appendChild($('th', { style: { textAlign: 'left', padding: '8px 10px', fontSize: '11px', fontWeight: '600', color: 'var(--text-3)', textTransform: 'uppercase', letterSpacing: '0.5px' } }, h)); }); thead.appendChild(trh); table.appendChild(thead); var tbody = $('tbody'); T.results.forEach(function (r) { var isPass = r.status === 'pass'; var isSkip = r.status === 'skip'; var rowBg = isPass ? 'transparent' : (isSkip ? 'rgba(234,179,8,0.04)' : 'rgba(239,68,68,0.04)'); var dotColor = isPass ? 'var(--success)' : (isSkip ? 'var(--warning)' : 'var(--danger)'); var detailColor = isPass ? 'var(--text-3)' : (isSkip ? 'var(--warning)' : 'var(--danger)'); var tr = $('tr', { style: { borderBottom: '1px solid var(--border)', background: rowBg } }); // Status dot tr.appendChild($('td', { style: { padding: '6px 10px', width: '20px' } }, [ $('span', { style: { display: 'inline-block', width: '8px', height: '8px', borderRadius: '50%', background: dotColor } }) ])); tr.appendChild($('td', { style: { padding: '6px 10px', color: 'var(--text-3)' } }, r.tier)); tr.appendChild($('td', { style: { padding: '6px 10px', color: 'var(--accent)' } }, r.domain)); tr.appendChild($('td', { style: { padding: '6px 10px', color: 'var(--text)' } }, r.name)); tr.appendChild($('td', { style: { padding: '6px 10px', color: 'var(--text-3)', textAlign: 'right' } }, String(r.duration))); var detailText = isSkip ? ('SKIP: ' + (r.detail || '')) : (r.detail || ''); var detailTd = $('td', { style: { padding: '6px 10px', color: detailColor, maxWidth: '320px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: detailText ? 'pointer' : 'default' }, title: detailText || '' }, detailText ? detailText.substring(0, 120) : '✓'); if (detailText && detailText.length > 120) { detailTd.addEventListener('click', function () { if (detailTd.style.whiteSpace === 'nowrap') { detailTd.style.whiteSpace = 'pre-wrap'; detailTd.style.wordBreak = 'break-all'; detailTd.textContent = detailText; } else { detailTd.style.whiteSpace = 'nowrap'; detailTd.textContent = r.detail.substring(0, 120); } }); } tr.appendChild(detailTd); tbody.appendChild(tr); }); table.appendChild(tbody); T.el.detail.appendChild(table); } })();