mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
200 lines
6.8 KiB
HTML
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>
|