748 lines
29 KiB
JavaScript
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);
|
|
}
|
|
|
|
})();
|