v0.7.1 Surface Runner Framework
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 23s
CI/CD / test-frontend (pull_request) Successful in 26s
CI/CD / test-go-pg (pull_request) Successful in 2m35s
CI/CD / test-sqlite (pull_request) Successful in 3m19s
CI/CD / build-and-deploy (pull_request) Successful in 1m46s
All checks were successful
CI/CD / detect-changes (pull_request) Successful in 23s
CI/CD / test-frontend (pull_request) Successful in 26s
CI/CD / test-go-pg (pull_request) Successful in 2m35s
CI/CD / test-sqlite (pull_request) Successful in 3m19s
CI/CD / build-and-deploy (pull_request) Successful in 1m46s
sw.testing SDK module — structured test framework for surface runners: - suite/test registration, lifecycle hooks (beforeAll/afterAll/beforeEach/afterEach) - Assertion library (ok/eq/neq/gt/match/throws/status/shape/arrayOf) - Auto-cleanup via track(type, id) with LIFO deletion - Three result statuses: passed/failed/warned - Structured JSON results with timing, warnings, cleanup stats test-runner manifest type: - ValidateManifest accepts "test-runner" packages - Excluded from sidebar nav (extensionNavItems filters surface/full only) - DB migrations: SQLite 013, Postgres 014 (CHECK constraint) ICD runner migration (kernel-only): - Migrated from T.test()/T.assert() to sw.testing.suite() - Stripped extension-dependent tests (channels, notes, personas, etc.) - Kernel suites: smoke, crud, authz, security, providers, packaging, sdk - Deleted ui.js + css (rendering delegated to registry surface) SDK runner migration (kernel-only): - Migrated from T.dualTest()/T.domains to sw.testing.suite() - Stripped extension domains (belong in v0.7.2 package runners) - Kernel suites: misc, workflows, admin, packages, connections, deps, composition Runner registry surface (/s/test-runners): - Admin-only dashboard discovering test-runner packages - Run All / per-runner Run buttons, real-time results - Export Failures / Export Full Results (JSON download) - Dark mode styling using kernel CSS variables - Suite count polling for async runner script loading 141 passed, 0 failed on fresh minimal install. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
203
packages/test-runners/css/main.css
Normal file
203
packages/test-runners/css/main.css
Normal file
@@ -0,0 +1,203 @@
|
||||
/* Test Runners — Registry Surface
|
||||
Uses kernel CSS variables from variables.css (--bg-*, --border, --text-*, etc.) */
|
||||
|
||||
.ext-test-runners-root {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: var(--sp-6, 24px) var(--sp-4, 16px);
|
||||
font-family: var(--font, system-ui, sans-serif);
|
||||
color: var(--text, #e8e8ed);
|
||||
}
|
||||
|
||||
/* ── Run All Bar ─────────────────────────────── */
|
||||
|
||||
.ext-test-runners-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-4, 16px);
|
||||
margin-bottom: var(--sp-6, 24px);
|
||||
padding: var(--sp-4, 16px);
|
||||
background: var(--bg-raised, #222227);
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
border: 1px solid var(--border, #2e2e35);
|
||||
}
|
||||
|
||||
.ext-test-runners-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
color: var(--text, #e8e8ed);
|
||||
}
|
||||
|
||||
.ext-test-runners-summary {
|
||||
display: flex;
|
||||
gap: var(--sp-3, 12px);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.ext-test-runners-stat {
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ext-test-runners-stat--pass { background: var(--success-dim, rgba(34,197,94,0.2)); color: var(--success-light, #4ade80); }
|
||||
.ext-test-runners-stat--fail { background: var(--danger-dim, rgba(239,68,68,0.2)); color: var(--danger-light, #f87171); }
|
||||
.ext-test-runners-stat--warn { background: var(--warning-dim, rgba(234,179,8,0.2)); color: var(--warning-light, #fbbf24); }
|
||||
.ext-test-runners-stat--skip { background: var(--bg-hover, #2a2a30); color: var(--text-3, #6b6b7b); }
|
||||
|
||||
/* ── Runner Cards ────────────────────────────── */
|
||||
|
||||
.ext-test-runners-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-4, 16px);
|
||||
}
|
||||
|
||||
.ext-test-runners-card {
|
||||
border: 1px solid var(--border, #2e2e35);
|
||||
border-radius: var(--radius-lg, 12px);
|
||||
padding: var(--sp-4, 16px);
|
||||
background: var(--bg-surface, #18181b);
|
||||
}
|
||||
|
||||
.ext-test-runners-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-3, 12px);
|
||||
margin-bottom: var(--sp-3, 12px);
|
||||
}
|
||||
|
||||
.ext-test-runners-card__icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.ext-test-runners-card__title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
color: var(--text, #e8e8ed);
|
||||
}
|
||||
|
||||
.ext-test-runners-card__desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-2, #9898a8);
|
||||
margin: 0 0 var(--sp-3, 12px) 0;
|
||||
}
|
||||
|
||||
.ext-test-runners-card__requires {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-3, #6b6b7b);
|
||||
margin-bottom: var(--sp-2, 8px);
|
||||
}
|
||||
|
||||
.ext-test-runners-card__requires--missing {
|
||||
color: var(--warning, #eab308);
|
||||
}
|
||||
|
||||
/* ── Results Panel ───────────────────────────── */
|
||||
|
||||
.ext-test-runners-results {
|
||||
margin-top: var(--sp-3, 12px);
|
||||
border-top: 1px solid var(--border, #2e2e35);
|
||||
padding-top: var(--sp-3, 12px);
|
||||
}
|
||||
|
||||
.ext-test-runners-suite {
|
||||
margin-bottom: var(--sp-3, 12px);
|
||||
}
|
||||
|
||||
.ext-test-runners-suite__name {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: var(--sp-1, 4px);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--text, #e8e8ed);
|
||||
}
|
||||
|
||||
.ext-test-runners-suite__name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.ext-test-runners-test {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--sp-2, 8px);
|
||||
padding: 2px 0 2px var(--sp-4, 16px);
|
||||
font-size: 0.85rem;
|
||||
font-family: var(--mono, monospace);
|
||||
}
|
||||
|
||||
.ext-test-runners-test__status {
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ext-test-runners-test__name {
|
||||
flex: 1;
|
||||
color: var(--text-2, #9898a8);
|
||||
}
|
||||
|
||||
.ext-test-runners-test__time {
|
||||
color: var(--text-3, #6b6b7b);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.ext-test-runners-test__detail {
|
||||
color: var(--danger-light, #f87171);
|
||||
font-size: 0.8rem;
|
||||
padding-left: calc(var(--sp-4, 16px) + 24px);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Status colors */
|
||||
.ext-test-runners-status--passed { color: var(--success, #1dab51); }
|
||||
.ext-test-runners-status--failed { color: var(--danger, #ef4444); }
|
||||
.ext-test-runners-status--warned { color: var(--warning, #eab308); }
|
||||
.ext-test-runners-status--skipped { color: var(--text-3, #6b6b7b); }
|
||||
|
||||
/* ── Loading / Empty ─────────────────────────── */
|
||||
|
||||
.ext-test-runners-loading,
|
||||
.ext-test-runners-empty {
|
||||
text-align: center;
|
||||
padding: var(--sp-8, 32px);
|
||||
color: var(--text-2, #9898a8);
|
||||
}
|
||||
|
||||
/* ── Running indicator ───────────────────────── */
|
||||
|
||||
.ext-test-runners-running {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2, 8px);
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-2, #9898a8);
|
||||
}
|
||||
|
||||
@keyframes ext-test-runners-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.ext-test-runners-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid var(--border, #2e2e35);
|
||||
border-top-color: var(--accent, #6493ed);
|
||||
border-radius: 50%;
|
||||
animation: ext-test-runners-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
/* ── Export Bar ───────────────────────────────── */
|
||||
|
||||
.ext-test-runners-export {
|
||||
display: flex;
|
||||
gap: var(--sp-3, 12px);
|
||||
margin-top: var(--sp-4, 16px);
|
||||
padding-top: var(--sp-4, 16px);
|
||||
border-top: 1px solid var(--border, #2e2e35);
|
||||
}
|
||||
428
packages/test-runners/js/main.js
Normal file
428
packages/test-runners/js/main.js
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* Test Runners — Registry Surface
|
||||
*
|
||||
* Discovers installed test-runner packages, loads their suites into
|
||||
* sw.testing, and provides a dashboard to run and view results.
|
||||
*/
|
||||
(async function () {
|
||||
'use strict';
|
||||
|
||||
var mount = document.getElementById('extension-mount');
|
||||
if (!mount) return;
|
||||
mount.classList.add('ext-test-runners-root');
|
||||
|
||||
// ─── Boot SDK ─────────────────────────────────────────────
|
||||
try {
|
||||
var base = window.__BASE__ || '';
|
||||
var ver = window.__VERSION__ || '0';
|
||||
|
||||
if (!window.preact) {
|
||||
var { h, render } = await import(base + '/js/sw/vendor/preact.module.js');
|
||||
var hooksModule = await import(base + '/js/sw/vendor/hooks.module.js');
|
||||
var htmModule = await import(base + '/js/sw/vendor/htm.module.js');
|
||||
window.preact = { h, render };
|
||||
window.hooks = hooksModule;
|
||||
window.html = htmModule.default.bind(h);
|
||||
}
|
||||
|
||||
var sdk = await import(base + '/js/sw/sdk/index.js?v=' + ver);
|
||||
await sdk.boot();
|
||||
} catch (e) {
|
||||
mount.innerHTML = '<p style="color:var(--danger);padding:24px;">SDK boot failed: ' + e.message + '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
var { h, render } = preact;
|
||||
var { useState, useEffect, useCallback } = hooks;
|
||||
|
||||
// ─── Shell Topbar ─────────────────────────────────────────
|
||||
if (sw.shell && sw.shell.topbar) {
|
||||
sw.shell.topbar.setTitle('Test Runners');
|
||||
}
|
||||
|
||||
// ─── Status Icons ─────────────────────────────────────────
|
||||
var STATUS_ICON = {
|
||||
passed: '\u2713', failed: '\u2717', warned: '\u26A0', skipped: '\u2014'
|
||||
};
|
||||
|
||||
// ─── App Component ────────────────────────────────────────
|
||||
|
||||
function App() {
|
||||
var _s = useState(null); var runners = _s[0], setRunners = _s[1];
|
||||
var _s2 = useState(null); var allPkgs = _s2[0], setAllPkgs = _s2[1];
|
||||
var _s3 = useState(null); var results = _s3[0], setResults = _s3[1];
|
||||
var _s4 = useState(false); var running = _s4[0], setRunning = _s4[1];
|
||||
var _s5 = useState(null); var runningSuite = _s5[0], setRunningSuite = _s5[1];
|
||||
var _s6 = useState(''); var error = _s6[0], setError = _s6[1];
|
||||
var _s7 = useState(false); var loaded = _s7[0], setLoaded = _s7[1];
|
||||
|
||||
// Discover runners and load their scripts
|
||||
useEffect(function () {
|
||||
discoverAndLoad().catch(function (e) {
|
||||
setError('Failed to discover runners: ' + e.message);
|
||||
});
|
||||
}, []);
|
||||
|
||||
async function discoverAndLoad() {
|
||||
// Fetch all packages (admin endpoint)
|
||||
var pkgsResp = await sw.api.get('/api/v1/admin/packages');
|
||||
var pkgs = pkgsResp.data || pkgsResp;
|
||||
setAllPkgs(pkgs);
|
||||
|
||||
// Filter test-runner type
|
||||
var testRunners = pkgs.filter(function (p) {
|
||||
return p.type === 'test-runner' && p.enabled;
|
||||
});
|
||||
setRunners(testRunners);
|
||||
|
||||
// Clear any previously registered suites
|
||||
sw.testing.clear();
|
||||
|
||||
// Load each runner's main.js to register suites.
|
||||
// Runner main.js scripts are async IIFEs that load sub-modules sequentially,
|
||||
// so the script onload fires before suites are registered. We load all runner
|
||||
// scripts, then poll until suite count stabilizes.
|
||||
var surfaceBase = (window.__BASE__ || '') + '/surfaces/';
|
||||
for (var i = 0; i < testRunners.length; i++) {
|
||||
var r = testRunners[i];
|
||||
await loadRunnerScript(surfaceBase + r.id + '/js/main.js?v=' + (r.version || '0') + '.' + Date.now());
|
||||
}
|
||||
|
||||
// Wait for suites to stabilize (runners load sub-modules async)
|
||||
await waitForSuites();
|
||||
|
||||
setLoaded(true);
|
||||
console.log('[TestRunners] Loaded ' + testRunners.length + ' runners, ' + sw.testing.suites().length + ' suites');
|
||||
}
|
||||
|
||||
function loadRunnerScript(src) {
|
||||
return new Promise(function (resolve) {
|
||||
var s = document.createElement('script');
|
||||
s.src = src;
|
||||
s.onload = resolve;
|
||||
s.onerror = function () {
|
||||
console.warn('[TestRunners] Failed to load: ' + src);
|
||||
resolve();
|
||||
};
|
||||
document.body.appendChild(s);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForSuites() {
|
||||
return new Promise(function (resolve) {
|
||||
var lastCount = 0;
|
||||
var stableCount = 0;
|
||||
var interval = setInterval(function () {
|
||||
var count = sw.testing.suites().length;
|
||||
if (count === lastCount && count > 0) {
|
||||
stableCount++;
|
||||
if (stableCount >= 3) { clearInterval(interval); resolve(); }
|
||||
} else {
|
||||
stableCount = 0;
|
||||
lastCount = count;
|
||||
}
|
||||
}, 500);
|
||||
// Timeout after 30s regardless
|
||||
setTimeout(function () { clearInterval(interval); resolve(); }, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
// Check requires for a runner
|
||||
function checkRequires(runner) {
|
||||
var requires = runner.manifest?.requires || runner.requires || [];
|
||||
if (!requires.length || !allPkgs) return { met: requires, missing: [] };
|
||||
var installedIds = allPkgs.map(function (p) { return p.id; });
|
||||
var met = [], missing = [];
|
||||
for (var i = 0; i < requires.length; i++) {
|
||||
if (installedIds.indexOf(requires[i]) !== -1) met.push(requires[i]);
|
||||
else missing.push(requires[i]);
|
||||
}
|
||||
return { met: met, missing: missing };
|
||||
}
|
||||
|
||||
// Run all suites
|
||||
var runAll = useCallback(async function () {
|
||||
if (running) return;
|
||||
setRunning(true);
|
||||
setResults(null);
|
||||
setRunningSuite('all');
|
||||
try {
|
||||
var r = await sw.testing.run();
|
||||
setResults(r);
|
||||
} catch (e) {
|
||||
setError('Run failed: ' + e.message);
|
||||
}
|
||||
setRunning(false);
|
||||
setRunningSuite(null);
|
||||
}, [running]);
|
||||
|
||||
// Run suites for a single runner
|
||||
var runRunner = useCallback(async function (runner) {
|
||||
if (running) return;
|
||||
setRunning(true);
|
||||
setRunningSuite(runner.id);
|
||||
|
||||
var prefix = runner.id === 'icd-test-runner' ? 'icd/' :
|
||||
runner.id === 'sdk-test-runner' ? 'sdk/' : runner.id + '/';
|
||||
var suiteNames = sw.testing.suites().filter(function (n) { return n.indexOf(prefix) === 0; });
|
||||
|
||||
var allSuiteResults = [];
|
||||
var summary = { total: 0, passed: 0, failed: 0, warned: 0, skipped: 0 };
|
||||
var start = performance.now();
|
||||
|
||||
for (var i = 0; i < suiteNames.length; i++) {
|
||||
var r = await sw.testing.run(suiteNames[i]);
|
||||
for (var j = 0; j < r.suites.length; j++) {
|
||||
allSuiteResults.push(r.suites[j]);
|
||||
for (var k = 0; k < r.suites[j].tests.length; k++) {
|
||||
var t = r.suites[j].tests[k];
|
||||
summary.total++;
|
||||
if (t.status === 'passed') summary.passed++;
|
||||
else if (t.status === 'failed') summary.failed++;
|
||||
else if (t.status === 'warned') summary.warned++;
|
||||
else if (t.status === 'skipped') summary.skipped++;
|
||||
}
|
||||
}
|
||||
if (r.suites[0] && r.suites[0].status === 'skipped') summary.skipped++;
|
||||
}
|
||||
|
||||
setResults({
|
||||
timestamp: new Date().toISOString(),
|
||||
duration_ms: Math.round(performance.now() - start),
|
||||
summary: summary,
|
||||
suites: allSuiteResults
|
||||
});
|
||||
setRunning(false);
|
||||
setRunningSuite(null);
|
||||
}, [running]);
|
||||
|
||||
if (error) {
|
||||
return html`<div class="sw-inline-error"><p>${error}</p></div>`;
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
return html`<div class="ext-test-runners-loading">Loading test runners...</div>`;
|
||||
}
|
||||
|
||||
if (!runners || runners.length === 0) {
|
||||
return html`<div class="ext-test-runners-empty">
|
||||
<p>No test-runner packages installed.</p>
|
||||
<p style="font-size:0.85rem;color:var(--text-muted)">Install packages with <code>"type": "test-runner"</code> to see them here.</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
var suiteCount = sw.testing.suites().length;
|
||||
|
||||
return html`
|
||||
<div>
|
||||
<${RunAllBar}
|
||||
suiteCount=${suiteCount}
|
||||
results=${results}
|
||||
running=${running}
|
||||
runningSuite=${runningSuite}
|
||||
onRunAll=${runAll}
|
||||
/>
|
||||
<div class="ext-test-runners-grid">
|
||||
${runners.map(function (r) {
|
||||
return html`<${RunnerCard}
|
||||
key=${r.id}
|
||||
runner=${r}
|
||||
requires=${checkRequires(r)}
|
||||
results=${results}
|
||||
running=${running}
|
||||
runningSuite=${runningSuite}
|
||||
onRun=${function () { runRunner(r); }}
|
||||
/>`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── Export Helpers ─────────────────────────────────────────
|
||||
|
||||
function exportFailures(results) {
|
||||
if (!results) return;
|
||||
var failures = [];
|
||||
for (var i = 0; i < results.suites.length; i++) {
|
||||
var suite = results.suites[i];
|
||||
for (var j = 0; j < suite.tests.length; j++) {
|
||||
var t = suite.tests[j];
|
||||
if (t.status === 'failed' || t.status === 'warned') {
|
||||
failures.push({
|
||||
suite: suite.name,
|
||||
test: t.name,
|
||||
status: t.status,
|
||||
detail: t.detail || null,
|
||||
warnings: t.warnings && t.warnings.length > 0 ? t.warnings : undefined,
|
||||
duration_ms: t.duration_ms,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
var report = {
|
||||
exported_at: new Date().toISOString(),
|
||||
run_at: results.timestamp,
|
||||
duration_ms: results.duration_ms,
|
||||
summary: results.summary,
|
||||
failures: failures,
|
||||
};
|
||||
var json = JSON.stringify(report, null, 2);
|
||||
var blob = new Blob([json], { type: 'application/json' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'test-failures-' + new Date().toISOString().slice(0, 19).replace(/:/g, '-') + '.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function exportFull(results) {
|
||||
if (!results) return;
|
||||
var json = JSON.stringify(results, null, 2);
|
||||
var blob = new Blob([json], { type: 'application/json' });
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'test-results-' + new Date().toISOString().slice(0, 19).replace(/:/g, '-') + '.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// ─── RunAllBar ────────────────────────────────────────────
|
||||
|
||||
function RunAllBar(props) {
|
||||
var s = props.results?.summary;
|
||||
var hasFailures = s && (s.failed > 0 || s.warned > 0);
|
||||
return html`
|
||||
<div class="ext-test-runners-bar">
|
||||
<h2>Test Runners</h2>
|
||||
${props.running
|
||||
? html`<span class="ext-test-runners-running"><span class="ext-test-runners-spinner"></span> Running${props.runningSuite === 'all' ? ' all' : ''}...</span>`
|
||||
: html`<button class="sw-btn sw-btn--primary" onClick=${props.onRunAll}>Run All (${props.suiteCount} suites)</button>`
|
||||
}
|
||||
${s ? html`
|
||||
<div class="ext-test-runners-summary">
|
||||
<span class="ext-test-runners-stat ext-test-runners-stat--pass">${s.passed} passed</span>
|
||||
${s.failed > 0 ? html`<span class="ext-test-runners-stat ext-test-runners-stat--fail">${s.failed} failed</span>` : null}
|
||||
${s.warned > 0 ? html`<span class="ext-test-runners-stat ext-test-runners-stat--warn">${s.warned} warned</span>` : null}
|
||||
${s.skipped > 0 ? html`<span class="ext-test-runners-stat ext-test-runners-stat--skip">${s.skipped} skipped</span>` : null}
|
||||
<span style="color:var(--text-3);font-size:0.8rem">${props.results.duration_ms}ms</span>
|
||||
</div>
|
||||
` : null}
|
||||
</div>
|
||||
${s && !props.running ? html`
|
||||
<div class="ext-test-runners-export">
|
||||
${hasFailures ? html`<button class="sw-btn sw-btn--sm" onClick=${function () { exportFailures(props.results); }}>Export Failures</button>` : null}
|
||||
<button class="sw-btn sw-btn--sm" onClick=${function () { exportFull(props.results); }}>Export Full Results</button>
|
||||
</div>
|
||||
` : null}
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── RunnerCard ───────────────────────────────────────────
|
||||
|
||||
function RunnerCard(props) {
|
||||
var r = props.runner;
|
||||
var manifest = r.manifest || {};
|
||||
var icon = manifest.icon || r.icon || '📦';
|
||||
var title = r.title || manifest.title || r.id;
|
||||
var desc = manifest.description || r.description || '';
|
||||
var req = props.requires;
|
||||
|
||||
// Filter results for this runner's suites
|
||||
var prefix = r.id === 'icd-test-runner' ? 'icd/' :
|
||||
r.id === 'sdk-test-runner' ? 'sdk/' : r.id + '/';
|
||||
var runnerSuites = props.results?.suites?.filter(function (s) {
|
||||
return s.name.indexOf(prefix) === 0;
|
||||
}) || [];
|
||||
|
||||
return html`
|
||||
<div class="ext-test-runners-card">
|
||||
<div class="ext-test-runners-card__header">
|
||||
<span class="ext-test-runners-card__icon">${icon}</span>
|
||||
<h3 class="ext-test-runners-card__title">${title}</h3>
|
||||
${req.missing.length > 0
|
||||
? null
|
||||
: props.running
|
||||
? (props.runningSuite === r.id
|
||||
? html`<span class="ext-test-runners-running"><span class="ext-test-runners-spinner"></span></span>`
|
||||
: null)
|
||||
: html`<button class="sw-btn sw-btn--sm" onClick=${props.onRun}>Run</button>`
|
||||
}
|
||||
</div>
|
||||
${desc ? html`<p class="ext-test-runners-card__desc">${desc}</p>` : null}
|
||||
${req.missing.length > 0
|
||||
? html`<p class="ext-test-runners-card__requires ext-test-runners-card__requires--missing">
|
||||
Missing required packages: ${req.missing.join(', ')}
|
||||
</p>`
|
||||
: req.met.length > 0
|
||||
? html`<p class="ext-test-runners-card__requires">Requires: ${req.met.join(', ')}</p>`
|
||||
: null
|
||||
}
|
||||
${runnerSuites.length > 0 ? html`<${ResultsPanel} suites=${runnerSuites} />` : null}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── ResultsPanel ─────────────────────────────────────────
|
||||
|
||||
function ResultsPanel(props) {
|
||||
return html`
|
||||
<div class="ext-test-runners-results">
|
||||
${props.suites.map(function (suite) {
|
||||
return html`<${SuiteResult} key=${suite.name} suite=${suite} />`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function SuiteResult(props) {
|
||||
var _s = useState(true); var expanded = _s[0], setExpanded = _s[1];
|
||||
var suite = props.suite;
|
||||
var statusClass = 'ext-test-runners-status--' + suite.status;
|
||||
|
||||
var counts = { passed: 0, failed: 0, warned: 0, skipped: 0 };
|
||||
for (var i = 0; i < suite.tests.length; i++) {
|
||||
var s = suite.tests[i].status;
|
||||
if (counts[s] !== undefined) counts[s]++;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="ext-test-runners-suite">
|
||||
<div class="ext-test-runners-suite__name ${statusClass}"
|
||||
onClick=${function () { setExpanded(!expanded); }}>
|
||||
${expanded ? '▼' : '▶'} ${suite.name}
|
||||
${' '}(${counts.passed}/${suite.tests.length} passed${counts.failed ? ', ' + counts.failed + ' failed' : ''}${suite.status === 'skipped' ? ' — skipped' + (suite.reason ? ': ' + suite.reason : '') : ''})
|
||||
${' '}${suite.duration_ms}ms
|
||||
</div>
|
||||
${expanded && suite.tests.length > 0 ? suite.tests.map(function (t) {
|
||||
return html`<${TestResult} key=${t.name} test=${t} />`;
|
||||
}) : null}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function TestResult(props) {
|
||||
var t = props.test;
|
||||
var statusClass = 'ext-test-runners-status--' + t.status;
|
||||
var icon = STATUS_ICON[t.status] || '?';
|
||||
|
||||
return html`
|
||||
<div>
|
||||
<div class="ext-test-runners-test">
|
||||
<span class="ext-test-runners-test__status ${statusClass}">${icon}</span>
|
||||
<span class="ext-test-runners-test__name">${t.name}</span>
|
||||
<span class="ext-test-runners-test__time">${t.duration_ms}ms</span>
|
||||
</div>
|
||||
${t.detail ? html`<div class="ext-test-runners-test__detail">${t.detail}</div>` : null}
|
||||
${t.warnings && t.warnings.length > 0 ? t.warnings.map(function (w) {
|
||||
return html`<div class="ext-test-runners-test__detail" style="color:var(--warning,#856404)">${w}</div>`;
|
||||
}) : null}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ─── Render ───────────────────────────────────────────────
|
||||
render(html`<${App} />`, mount);
|
||||
})();
|
||||
11
packages/test-runners/manifest.json
Normal file
11
packages/test-runners/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"id": "test-runners",
|
||||
"icon": "🧪",
|
||||
"type": "surface",
|
||||
"title": "Test Runners",
|
||||
"route": "/s/test-runners",
|
||||
"auth": "admin",
|
||||
"layout": "single",
|
||||
"version": "0.1.0",
|
||||
"description": "Run and view results from all installed test-runner packages."
|
||||
}
|
||||
Reference in New Issue
Block a user