forked from Zakaria/hermes-agent
100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
|
|
def test_dashboard_overlay_cleans_timers_and_ignores_late_fetches():
|
|
plugin_root = Path(__file__).resolve().parents[1]
|
|
script = textwrap.dedent(
|
|
f"""
|
|
(async function () {{
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const vm = require('vm');
|
|
|
|
const timers = new Map();
|
|
let nextTimer = 1;
|
|
const events = [];
|
|
const cleanups = [];
|
|
let registeredComponent = null;
|
|
let stateValue = {{ enabled: true, mode: 'idle', mood: 'calm', name: 'Signal', revision: 1 }};
|
|
let errorWrites = 0;
|
|
let stateWrites = 0;
|
|
|
|
function makeElement(type, props, ...children) {{
|
|
return {{ type, props: props || {{}}, children }};
|
|
}}
|
|
const React = {{
|
|
createElement: makeElement,
|
|
useState(initial) {{
|
|
if (initial === null) {{
|
|
const setter = function (value) {{ stateWrites += 1; stateValue = value; }};
|
|
return [stateValue, setter];
|
|
}}
|
|
const setter = function () {{ errorWrites += 1; }};
|
|
return [initial, setter];
|
|
}},
|
|
useRef(initial) {{ return {{ current: initial }}; }},
|
|
useEffect(fn) {{
|
|
const cleanup = fn();
|
|
if (cleanup) cleanups.push(cleanup);
|
|
}},
|
|
}};
|
|
const windowObj = {{
|
|
__HERMES_PLUGIN_SDK__: {{
|
|
React,
|
|
hooks: React,
|
|
authedFetch(url, options) {{
|
|
if (options && options.method === 'POST') {{
|
|
events.push(JSON.parse(options.body).type);
|
|
}}
|
|
return Promise.resolve({{
|
|
ok: true,
|
|
json: () => Promise.resolve({{ ok: true, state: stateValue }}),
|
|
}});
|
|
}},
|
|
}},
|
|
__HERMES_PLUGINS__: {{
|
|
registerSlot(plugin, slot, component) {{ registeredComponent = component; }}
|
|
}},
|
|
setTimeout(fn, delay) {{
|
|
const id = nextTimer++;
|
|
timers.set(id, {{ fn, delay }});
|
|
return id;
|
|
}},
|
|
clearTimeout(id) {{ timers.delete(id); }},
|
|
setInterval(fn, delay) {{
|
|
const id = nextTimer++;
|
|
timers.set(id, {{ fn, delay, interval: true }});
|
|
return id;
|
|
}},
|
|
clearInterval(id) {{ timers.delete(id); }},
|
|
}};
|
|
const context = {{ window: windowObj, fetch: windowObj.__HERMES_PLUGIN_SDK__.authedFetch, console }};
|
|
vm.createContext(context);
|
|
vm.runInContext(fs.readFileSync({str(plugin_root / 'dashboard/dist/index.js')!r}, 'utf8'), context);
|
|
assert.equal(typeof registeredComponent, 'function');
|
|
|
|
const tree = registeredComponent();
|
|
const avatar = tree.children[1];
|
|
avatar.props.onMouseEnter();
|
|
avatar.props.onMouseEnter();
|
|
const hoverTimers = Array.from(timers.values()).filter(t => t.delay === 1000);
|
|
assert.equal(hoverTimers.length, 1, 'duplicate hover enters should leave one hover timer');
|
|
|
|
for (const cleanup of cleanups) cleanup();
|
|
assert.equal(timers.size, 0, 'unmount should clear interval, hover, and transition timers');
|
|
|
|
avatar.props.onClick();
|
|
await Promise.resolve();
|
|
assert.deepEqual(events, ['click']);
|
|
assert.equal(stateWrites, 0, 'late fetch resolution after unmount must not write state');
|
|
assert.equal(errorWrites, 0, 'late fetch resolution after unmount must not write errors');
|
|
}})();
|
|
"""
|
|
)
|
|
result = subprocess.run(["node", "-e", script], text=True, capture_output=True, check=False)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|