Files

200 lines
6.8 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenClaw E2E Harness</title>
<style>
body { background:#111; color:#eee; font-family:system-ui, sans-serif; padding:16px; }
pre { background:#0b0b0b; padding:12px; border:1px solid #333; border-radius:8px; }
</style>
</head>
<body>
<h1>OpenClaw E2E Harness</h1>
<div id="mount" style="height: 600px; border: 1px solid #333; border-radius: 8px; overflow: hidden;"></div>
<pre id="log">Running...</pre>
<script type="module">
import { TabManager } from '../openclaw_tabs.js';
import {
isAbortError,
linkAbortSignal,
parseJsonOrThrow,
parseJsonSafe,
} from '../openclaw_utils.js';
import {
composeFetchWrappersOnce,
getFetchWrapperMeta,
withAbortPassthrough,
withGetRetry,
withPreconnectHint,
} from '../openclaw_fetch_wrappers.js';
const logEl = document.getElementById('log');
const mount = document.getElementById('mount');
const results = { passed: 0, failed: 0, failures: [] };
function assert(condition, message) {
if (!condition) throw new Error(message || 'assertion_failed');
}
async function run(name, fn) {
try {
await fn();
results.passed++;
} catch (e) {
results.failed++;
results.failures.push({ name, message: e?.message || String(e) });
}
}
function setDone() {
window.__OPENCLAW_E2E_DONE__ = true;
window.__OPENCLAW_E2E_RESULTS__ = results;
logEl.textContent = JSON.stringify(results, null, 2);
}
// Minimal DOM surface for TabManager tests
function createHost() {
mount.innerHTML = '';
const container = document.createElement('div');
container.style.height = '100%';
container.style.display = 'flex';
container.style.flexDirection = 'column';
const tabBar = document.createElement('div');
tabBar.style.display = 'flex';
tabBar.style.gap = '6px';
tabBar.style.padding = '6px';
tabBar.style.borderBottom = '1px solid #333';
const content = document.createElement('div');
content.style.flex = '1';
content.style.padding = '8px';
container.appendChild(tabBar);
container.appendChild(content);
mount.appendChild(container);
return { tabBar, content };
}
function makeTab(id, title) {
return {
id,
title,
render: (pane) => {
const marker = document.createElement('div');
marker.dataset.testid = `tab-${id}`;
marker.textContent = `Rendered: ${id}`;
pane.appendChild(marker);
},
};
}
(async () => {
await run('Tab switching keeps content', async () => {
const { tabBar, content } = createHost();
const tm = new TabManager();
tm.registerTab(makeTab('a', 'A'));
tm.registerTab(makeTab('b', 'B'));
tm.init(tabBar, content);
tm.activateTab('a');
assert(document.querySelector('[data-testid="tab-a"]'), 'tab-a not rendered');
tm.activateTab('b');
assert(document.querySelector('[data-testid="tab-b"]'), 'tab-b not rendered');
tm.activateTab('a');
// Should still be there after switching back
assert(document.querySelector('[data-testid="tab-a"]'), 'tab-a disappeared after switching back');
});
await run('Remount re-renders loaded tabs', async () => {
// First mount
let host = createHost();
const tm = new TabManager();
tm.registerTab(makeTab('a', 'A'));
tm.registerTab(makeTab('b', 'B'));
tm.init(host.tabBar, host.content);
tm.activateTab('a');
assert(document.querySelector('[data-testid="tab-a"]'), 'tab-a not rendered on first mount');
// Simulate ComfyUI remount (new tabBar/content nodes)
host = createHost();
tm.init(host.tabBar, host.content);
tm.activateTab('a');
assert(document.querySelector('[data-testid="tab-a"]'), 'tab-a not rendered after remount');
});
await run('R55 safe JSON parse helpers are deterministic', async () => {
const ok = parseJsonSafe('{"a":1}', null);
assert(ok.ok === true, 'parseJsonSafe should succeed on valid JSON');
assert(ok.value.a === 1, 'parsed JSON payload mismatch');
const bad = parseJsonSafe('{oops}', []);
assert(bad.ok === false, 'parseJsonSafe should fail on invalid JSON');
assert(Array.isArray(bad.value), 'fallback value should be returned on parse failure');
let threw = false;
try {
parseJsonOrThrow('{oops}', 'Invalid JSON');
} catch (e) {
threw = true;
assert(String(e.message || '').includes('Invalid JSON'), 'parseJsonOrThrow message mismatch');
}
assert(threw, 'parseJsonOrThrow should throw on invalid JSON');
});
await run('R55 abort linkage propagates cancel once', async () => {
const upstream = new AbortController();
const local = new AbortController();
let callbackCount = 0;
const detach = linkAbortSignal(upstream.signal, local, () => {
callbackCount += 1;
});
upstream.abort();
assert(local.signal.aborted === true, 'linked controller was not aborted');
assert(callbackCount === 1, 'abort callback should fire once');
const abortErr = new DOMException('Aborted', 'AbortError');
assert(isAbortError(abortErr) === true, 'isAbortError should detect AbortError');
assert(isAbortError(new Error('x')) === false, 'isAbortError false-positive');
detach();
});
await run('R96 fetch wrapper composition is idempotent', async () => {
let calls = 0;
const baseFetch = async () => {
calls += 1;
if (calls === 1) throw new Error('transient');
return new Response('{}', { status: 200, headers: { 'Content-Type': 'application/json' } });
};
const decorators = [withAbortPassthrough(), withPreconnectHint(), withGetRetry({ retries: 1 })];
const wrapped1 = composeFetchWrappersOnce(baseFetch, decorators);
const wrapped2 = composeFetchWrappersOnce(wrapped1, decorators);
assert(wrapped1 === wrapped2, 'expected idempotent wrapper reuse on repeated composition');
const res = await wrapped2('https://example.com/health', { method: 'GET' });
assert(res.status === 200, 'wrapped fetch did not return response');
assert(calls === 2, `expected single retry path (2 calls), got ${calls}`);
const meta = getFetchWrapperMeta(wrapped2);
assert(meta && Array.isArray(meta.chainIds), 'missing wrapper metadata');
assert(meta.chainIds.length === 3, 'unexpected decorator chain metadata');
});
setDone();
})();
</script>
</body>
</html>