#!/usr/bin/env node
import { readFileSync } from "node:fs";
import {
connectWindowWeb,
createWindowWebImports,
normalizeCanvasWheelDelta,
} from "../moui/backend/web/browser_runtime.js";
const assert = (condition, message) => {
if (!condition) throw new Error(message);
};
const elementsById = new Map();
class FakeElement {
constructor(tagName = "div") {
this.tagName = tagName.toUpperCase();
this.children = [];
this.parentElement = null;
this.style = {};
this.listeners = new Map();
this.attributes = new Map();
this.dataset = {};
this.id = "";
this.width = 400;
this.height = 300;
this.clientWidth = 400;
this.clientHeight = 300;
this.value = "";
this.textContent = "";
this.tabIndex = -1;
}
appendChild(child) {
child.parentElement = this;
this.children.push(child);
if (child.id) elementsById.set(child.id, child);
return child;
}
remove() {
if (!this.parentElement) return;
this.parentElement.children = this.parentElement.children.filter(child => child !== this);
if (this.id) elementsById.delete(this.id);
this.parentElement = null;
}
contains(target) {
if (target === this) return true;
return this.children.some(child => child.contains(target));
}
setAttribute(name, value) { this.attributes.set(name, `${value}`); }
removeAttribute(name) { this.attributes.delete(name); }
getAttribute(name) { return this.attributes.get(name) ?? null; }
addEventListener(type, handler, options) {
const handlers = this.listeners.get(type) ?? [];
handlers.push({ handler, options });
this.listeners.set(type, handlers);
}
removeEventListener(type, handler) {
const handlers = this.listeners.get(type) ?? [];
this.listeners.set(type, handlers.filter(entry => entry.handler !== handler));
}
dispatch(type, init = {}) {
const event = {
type,
button: 0,
buttons: 0,
pointerId: 1,
pointerType: "mouse",
clientX: 0,
clientY: 0,
cancelable: true,
defaultPrevented: false,
preventDefault() { this.defaultPrevented = true; },
...init,
};
if (!event.target) event.target = this;
const path = [];
for (let current = this; current; current = current.parentElement) path.push(current);
for (const current of [...path].reverse()) {
event.currentTarget = current;
for (const { handler, options } of current.listeners.get(type) ?? []) {
if (options?.capture) handler(event);
}
}
for (const current of path) {
event.currentTarget = current;
for (const { handler, options } of current.listeners.get(type) ?? []) {
if (!options?.capture) handler(event);
}
}
return event;
}
focus() { fakeDocument.activeElement = this; }
select() {}
setPointerCapture(pointerId) { this.capturedPointerId = pointerId; }
releasePointerCapture(pointerId) { this.releasedPointerId = pointerId; }
getBoundingClientRect() {
return {
left: 0,
top: 0,
width: this.clientWidth || this.width || 1,
height: this.clientHeight || this.height || 1,
};
}
}
class FakeCanvasElement extends FakeElement {
constructor() { super("canvas"); }
}
const fakeDocument = {
body: new FakeElement("body"),
activeElement: null,
title: "",
createElement(tagName) {
return tagName === "canvas" ? new FakeCanvasElement() : new FakeElement(tagName);
},
getElementById(id) { return elementsById.get(id) ?? null; },
querySelector() { return null; },
execCommand() { return true; },
};
const windowListeners = new Map();
const fakeWindow = {
devicePixelRatio: 1,
innerWidth: 400,
innerHeight: 300,
location: new URL("http://example.test/?section=overview"),
history: { pushState() {}, replaceState() {}, back() {}, forward() {} },
addEventListener(type, handler) {
const handlers = windowListeners.get(type) ?? [];
handlers.push(handler);
windowListeners.set(type, handlers);
},
removeEventListener(type, handler) {
const handlers = windowListeners.get(type) ?? [];
windowListeners.set(type, handlers.filter(current => current !== handler));
},
matchMedia() { return { matches: false, addEventListener() {}, removeEventListener() {} }; },
};
globalThis.HTMLElement = FakeElement;
globalThis.HTMLCanvasElement = FakeCanvasElement;
globalThis.Node = FakeElement;
globalThis.document = fakeDocument;
globalThis.window = fakeWindow;
const localSettings = new Map();
globalThis.localStorage = {
getItem(key) { return localSettings.has(key) ? localSettings.get(key) : null; },
setItem(key, value) { localSettings.set(key, `${value}`); },
removeItem(key) { localSettings.delete(key); },
};
const webString = (imports, value) => {
const handle = imports.begin_create_string();
for (const char of value) {
imports.string_append_char(handle, char.codePointAt(0));
}
return imports.finish_create_string(handle);
};
const readWebString = (imports, handle) => {
let value = "";
for (;;) {
const codePoint = imports.string_read_char(handle);
if (codePoint < 0) break;
value += String.fromCodePoint(codePoint);
}
imports.finish_read_string(handle);
return value;
};
const createRuntime = ({ pointerFlags = 1 } = {}) => {
const imports = createWindowWebImports();
const events = [];
imports.set_dispatch_event(() => {});
imports.set_dispatch_pointer_input((rawId, kind, x, y, deltaX, deltaY, button, modifiers) => {
const event = { rawId, kind, x, y, deltaX, deltaY, button, modifiers };
events.push(event);
return typeof pointerFlags === "function" ? pointerFlags(event) : pointerFlags;
});
const canvas = imports.create_canvas("", 400, 300);
imports.install_canvas_events(7, canvas);
return { imports, canvas, events };
};
{
const imports = createWindowWebImports();
assert(!imports.settings_has_value(webString(imports, "editor/theme")));
assert(
imports.settings_write(
webString(imports, "editor/theme"),
webString(imports, "dark"),
),
);
assert(imports.settings_has_value(webString(imports, "editor/theme")));
assert(
readWebString(
imports,
imports.settings_read(webString(imports, "editor/theme")),
) === "dark",
);
assert(
imports.settings_write(
webString(imports, "editor/empty"),
webString(imports, ""),
),
);
assert(imports.settings_has_value(webString(imports, "editor/empty")));
assert(
readWebString(
imports,
imports.settings_read(webString(imports, "editor/empty")),
) === "",
);
assert(imports.settings_remove(webString(imports, "editor/theme")));
assert(!imports.settings_has_value(webString(imports, "editor/theme")));
}
{
const imports = createWindowWebImports();
let legacyCalls = 0;
let pointerCalls = 0;
connectWindowWeb({
exports: {
web_dispatch_event() { legacyCalls += 1; },
web_dispatch_pointer_input() { pointerCalls += 1; return 1; },
},
}, imports);
const canvas = imports.create_canvas("", 400, 300);
imports.install_canvas_events(9, canvas);
canvas.dispatch("pointerdown", { clientX: 6, clientY: 7 });
assert(pointerCalls === 1 && legacyCalls === 0, "connectWindowWeb must install the direct pointer ABI");
}
{
const { canvas, events } = createRuntime({ pointerFlags: event => event.kind === 24 ? 5 : 3 });
const down = canvas.dispatch("pointerdown", {
clientX: 12.5,
clientY: 18.25,
button: 2,
pointerId: 42,
ctrlKey: true,
altKey: true,
});
const up = canvas.dispatch("pointerup", { clientX: 12.5, clientY: 18.25, button: 2, pointerId: 42 });
assert(down.defaultPrevented && up.defaultPrevented, "handled pointer input must prevent browser defaults");
assert(canvas.capturedPointerId === 42 && canvas.releasedPointerId === 42, "wasm flags must drive real pointer capture");
assert(
JSON.stringify(events[0]) === JSON.stringify({ rawId: 7, kind: 23, x: 12.5, y: 18.25, deltaX: 0, deltaY: 0, button: 2, modifiers: 6 }),
"pointer ABI must contain only runtime-consumed fields",
);
}
{
const imports = createWindowWebImports();
let canvas;
const pointerFocus = [];
imports.set_dispatch_event(() => {});
imports.set_dispatch_pointer_input((_rawId, kind) => {
pointerFocus.push({ kind, focused: fakeDocument.activeElement === canvas });
return 3;
});
canvas = imports.create_canvas("", 400, 300);
imports.install_canvas_events(8, canvas);
canvas.dispatch("pointerdown", { clientX: 10, clientY: 12 });
assert(
JSON.stringify(pointerFocus) === JSON.stringify([{ kind: 23, focused: true }]),
"pointer down must establish canvas-host focus before calling wasm",
);
}
{
const imports = createWindowWebImports();
const legacyEvents = [];
imports.set_dispatch_event(kind => legacyEvents.push(kind));
imports.set_dispatch_pointer_input(() => 3);
const canvas = imports.create_canvas("", 400, 300);
imports.install_canvas_events(10, canvas);
const semanticsTarget = new FakeElement("a");
canvas.parentElement.appendChild(semanticsTarget);
canvas.dispatch("pointerdown", { clientX: 10, clientY: 12 });
canvas.dispatch("blur", { relatedTarget: semanticsTarget });
await Promise.resolve();
assert(
!legacyEvents.includes(12),
"focus moving into the semantics layer must not cancel a canvas pointer",
);
}
{
const previousDpr = fakeWindow.devicePixelRatio;
fakeWindow.devicePixelRatio = 2;
try {
const { canvas, events } = createRuntime();
canvas.dispatch("pointerdown", { clientX: 120, clientY: 220 });
canvas.dispatch("wheel", { clientX: 120, clientY: 220, deltaX: 2, deltaY: -3, deltaMode: 1 });
assert(events[0].x === 240 && events[0].y === 440, "coordinates must use physical canvas pixels");
assert(events[1].deltaX === 64 && events[1].deltaY === 96, "wheel deltas must normalize and scale once");
} finally {
fakeWindow.devicePixelRatio = previousDpr;
}
}
{
const previousDpr = fakeWindow.devicePixelRatio;
fakeWindow.devicePixelRatio = 2;
try {
const { imports, canvas } = createRuntime();
const resizeEvents = [];
imports.set_dispatch_event((kind, rawId, width, height, scaleFactor) => {
resizeEvents.push({ kind, rawId, width, height, scaleFactor });
});
const resize = windowListeners.get("resize").at(-1);
resize?.();
assert(
JSON.stringify(resizeEvents) === JSON.stringify([
{ kind: 10, rawId: 7, width: 800, height: 600, scaleFactor: 2 },
]),
"resize events must provide the DPR used to map physical pointer coordinates",
);
assert(canvas.width === 800 && canvas.height === 600, "resize must preserve physical canvas dimensions");
} finally {
fakeWindow.devicePixelRatio = previousDpr;
}
}
{
const { canvas, events } = createRuntime();
canvas.dispatch("pointerdown", { pointerType: "touch", pointerId: 3, clientX: 120, clientY: 220 });
const move = canvas.dispatch("pointermove", { pointerType: "touch", pointerId: 3, clientX: 112, clientY: 160 });
canvas.dispatch("pointerup", { pointerType: "touch", pointerId: 3, clientX: 112, clientY: 160 });
assert(events.map(event => event.kind).join(",") === "23,30,24", "touch drag must enter MoUI once per native pointer event");
assert(events[1].deltaX === 8 && events[1].deltaY === 60 && move.defaultPrevented, "handled touch scroll must use direct physical deltas");
}
{
const { imports, canvas, events } = createRuntime();
const semanticsNode = {
node_id: "12",
role: "link",
label: "Docs",
value: "",
description: "",
url: "#docs",
level: 0,
checked: false,
selected: false,
expanded: false,
invalid: false,
required: false,
disabled: false,
focused: false,
actions: ["focus", "activate"],
frame: { origin: { x: 10, y: 10 }, size: { width: 100, height: 24 } },
children: [],
};
const semantics = {
kind: "full",
generation: "1",
root: "12",
focused: null,
nodes: [semanticsNode],
removed: [],
};
imports.sync_semantics(7, imports.register_host_string(canvas.id), imports.register_host_string(JSON.stringify(semantics)));
assert(
globalThis.__mouiAccessibilitySemanticsEvidence.at(-1)?.generation === "1",
"Web semantics commits must be observable by the browser L2 recorder",
);
const layer = canvas.parentElement.children.find(child => child.className === "moui-semantics-layer");
const link = layer.children[0];
link.dispatch("pointerdown", { clientX: 20, clientY: 20 });
link.dispatch("pointerup", { clientX: 20, clientY: 20 });
link.dispatch("click", { detail: 1, clientX: 20, clientY: 20 });
const wheel = link.dispatch("wheel", { clientX: 20, clientY: 20, deltaY: 10 });
assert(events.map(event => event.kind).join(",") === "23,24,30", "semantic overlay input must use the host router exactly once");
assert(wheel.defaultPrevented, "handled semantic wheel must use the runtime default-action policy");
assert(!layer.listeners.has("pointermove") && !layer.listeners.has("wheel"), "semantic layers must not forward synthetic input");
}
{
const observed = [];
const imports = createWindowWebImports({ onEvent: event => observed.push(event) });
imports.record_semantics_action(7, imports.register_host_string(JSON.stringify({
node_id: "12",
request_generation: "41",
action: "activate",
status: "passed",
before: "41",
after: "42",
pending_work: true,
error: "",
})));
assert(
observed.length === 1 &&
observed[0].name === "accessibility_action" &&
observed[0].source === "runtime-receipt" &&
observed[0].rawId === 7 &&
observed[0].before === "41" &&
observed[0].after === "42",
"Web semantics action evidence must preserve the exact runtime receipt",
);
assert(
globalThis.__mouiAccessibilityActionEvidence.at(-1)?.node_id === "12",
"Web semantics action receipts must be available to the browser L2 recorder",
);
}
{
const { canvas, events } = createRuntime();
const nativeInput = new FakeElement("textarea");
nativeInput.setAttribute("data-moui-native-input", "true");
canvas.parentElement.appendChild(nativeInput);
nativeInput.dispatch("pointerdown", { clientX: 8, clientY: 9 });
nativeInput.dispatch("wheel", { deltaX: 1, deltaY: 1 });
assert(events.length === 0, "marked native controls must bypass canvas routing");
}
{
const { canvas, events } = createRuntime({ pointerFlags: 0 });
const down = canvas.dispatch("pointerdown", { clientX: 20, clientY: 30 });
const wheel = canvas.dispatch("wheel", { deltaX: 3, deltaY: -4 });
const zoom = canvas.dispatch("wheel", { deltaX: 3, deltaY: -4, ctrlKey: true });
canvas.dispatch("mousedown", { clientX: 20, clientY: 30 });
assert(
!down.defaultPrevented && !wheel.defaultPrevented && !zoom.defaultPrevented,
`unhandled input and browser zoom must keep defaults: ${JSON.stringify({ down: down.defaultPrevented, wheel: wheel.defaultPrevented, zoom: zoom.defaultPrevented })}`,
);
assert(events.length === 2, "mouse fallback events must not form a second input path");
}
assert(
JSON.stringify(normalizeCanvasWheelDelta({ deltaX: 1, deltaY: 2, deltaMode: 2 }, { pageHeight: 300 })) === JSON.stringify({ x: 300, y: -600 }),
"page wheel normalization is incorrect",
);
const browserSource = readFileSync(new URL("../moui/backend/web/browser_runtime.js", import.meta.url), "utf8");
const rendererSource = readFileSync(new URL("../moui_web_renderer/runtime.js", import.meta.url), "utf8");
assert(!/dispatchEvent\(new (?:PointerEvent|MouseEvent|WheelEvent)/.test(browserSource), "browser input router must not synthesize DOM input");
assert(!/dispatchEvent\(new (?:PointerEvent|MouseEvent|WheelEvent)/.test(rendererSource), "renderer overlays must not synthesize DOM input");
console.log("browser runtime input router tests: ok");