This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/packages/icd-test-runner/js/ui.js
2026-03-17 16:28:47 +00:00

748 lines
29 KiB
JavaScript

/**
* 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 (typeof UI !== 'undefined') UI.toast('Paste an API key first', 'warning');
return;
}
T.providerSetup.configured = true;
T.renderProviderSetup();
T.renderControls();
if (typeof UI !== 'undefined') UI.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';
// Header
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(T.user.username) + (T.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 Switchboard !== 'undefined') ? 'btn-secondary' : 'btn-ghost',
style: { opacity: (typeof Switchboard !== '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 (typeof UI !== 'undefined') UI.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('=== Chat Switchboard 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 (typeof UI !== 'undefined') UI.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 (typeof UI !== 'undefined') UI.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 (typeof UI !== 'undefined') UI.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 (typeof UI !== 'undefined') UI.toast('Report copied (fallback)', 'info');
}
T.runSuite = async function (which) {
if (T.running) return;
if (which === 'authz' && !T.fixtures.ready) {
if (typeof UI !== 'undefined') UI.toast('Provision test fixtures first (button on the right)', 'warning');
return;
}
if (which === 'security' && !T.fixtures.ready) {
if (typeof UI !== 'undefined') UI.toast('Provision test fixtures first (button on the right)', 'warning');
return;
}
if (which === 'provider' && !T.providerSetup.configured) {
if (typeof UI !== 'undefined') UI.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)';
UI.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);
}
})();