Changeset 0.28.7 (#193)
This commit is contained in:
739
packages/icd-test-runner/js/ui.js
Normal file
739
packages/icd-test-runner/js/ui.js
Normal file
@@ -0,0 +1,739 @@
|
||||
/**
|
||||
* 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);
|
||||
|
||||
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(); }
|
||||
} 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);
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user