/** * 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 = '

SDK boot failed: ' + e.message + '

'; 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); }); } // Runner dependency declarations. // Stored here (not in manifests) to avoid the package system marking // runners as dormant due to install-order timing during bundled install. var RUNNER_REQUIRES = { 'notes-runner': ['notes'], 'chat-runner': ['chat', 'chat-core'], 'schedules-runner': ['schedules'], 'workflow-runner': ['content-approval'], 'renderer-runner': ['mermaid-renderer'] }; // Check requires for a runner function checkRequires(runner) { var requires = RUNNER_REQUIRES[runner.id] || runner.manifest?.requires || runner.requires || []; if (!requires.length || !allPkgs) return { met: requires, missing: [] }; var installedIds = allPkgs.filter(function (p) { return p.enabled; }).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 // Post results to kernel API for CI consumption (fire-and-forget) function postResults(runnerId, data) { sw.api.post('/api/v1/admin/test-runners/results', { runner_id: runnerId || 'all', results: data }).catch(function (e) { console.warn('[TestRunners] Failed to store results:', e.message); }); } var runAll = useCallback(async function () { if (running) return; setRunning(true); setResults(null); setRunningSuite('all'); try { var r = await sw.testing.run(); setResults(r); postResults('all', 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); // Map runner package ID to suite name prefix. // ICD/SDK runners use 'icd/'/'sdk/' prefixes. // Package runners use the package name they test: 'notes/', 'chat/', etc. var prefix; if (runner.id === 'icd-test-runner') prefix = 'icd/'; else if (runner.id === 'sdk-test-runner') prefix = 'sdk/'; else prefix = runner.id.replace(/-runner$/, '') + '/'; 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++; } var runnerResults = { timestamp: new Date().toISOString(), duration_ms: Math.round(performance.now() - start), summary: summary, suites: allSuiteResults }; setResults(runnerResults); postResults(runner.id, runnerResults); setRunning(false); setRunningSuite(null); }, [running]); if (error) { return html`

${error}

`; } if (!loaded) { return html`
Loading test runners...
`; } if (!runners || runners.length === 0) { return html`

No test-runner packages installed.

Install packages with "type": "test-runner" to see them here.

`; } var suiteCount = sw.testing.suites().length; return html`
<${RunAllBar} suiteCount=${suiteCount} results=${results} running=${running} runningSuite=${runningSuite} onRunAll=${runAll} />
${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); }} />`; })}
`; } // ─── 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`

Test Runners

${props.running ? html` Running${props.runningSuite === 'all' ? ' all' : ''}...` : html`` } ${s ? html`
${s.passed} passed ${s.failed > 0 ? html`${s.failed} failed` : null} ${s.warned > 0 ? html`${s.warned} warned` : null} ${s.skipped > 0 ? html`${s.skipped} skipped` : null} ${props.results.duration_ms}ms
` : null}
${s && !props.running ? html`
${hasFailures ? html`` : null}
` : 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.replace(/-runner$/, '') + '/'; var runnerSuites = props.results?.suites?.filter(function (s) { return s.name.indexOf(prefix) === 0; }) || []; return html`
${icon}

${title}

${req.missing.length > 0 ? null : props.running ? (props.runningSuite === r.id ? html`` : null) : html`` }
${desc ? html`

${desc}

` : null} ${req.missing.length > 0 ? html`

Missing required packages: ${req.missing.join(', ')}

` : req.met.length > 0 ? html`

Requires: ${req.met.join(', ')}

` : null } ${runnerSuites.length > 0 ? html`<${ResultsPanel} suites=${runnerSuites} />` : null}
`; } // ─── ResultsPanel ───────────────────────────────────────── function ResultsPanel(props) { return html`
${props.suites.map(function (suite) { return html`<${SuiteResult} key=${suite.name} suite=${suite} />`; })}
`; } 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`
${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
${expanded && suite.tests.length > 0 ? suite.tests.map(function (t) { return html`<${TestResult} key=${t.name} test=${t} />`; }) : null}
`; } function TestResult(props) { var t = props.test; var statusClass = 'ext-test-runners-status--' + t.status; var icon = STATUS_ICON[t.status] || '?'; return html`
${icon} ${t.name} ${t.duration_ms}ms
${t.detail ? html`
${t.detail}
` : null} ${t.warnings && t.warnings.length > 0 ? t.warnings.map(function (w) { return html`
${w}
`; }) : null}
`; } // ─── Render ─────────────────────────────────────────────── render(html`<${App} />`, mount); })();