// A `go test -exec` wrapper for GOOS=js GOARCH=wasm that installs a minimal DOM // before starting the Go runtime, so the reconciler can be driven headlessly // under node instead of only in a browser. From kjol/go: // // GOOS=js GOARCH=wasm go test -exec="node testdata/domexec.js" ./wasmruntime // // The subtlety that matters: in a real browser, setting .innerHTML *parses* the // markup into real child nodes, so a later appendChild lands after it. The shim // models that (one opaque "#raw" child) rather than keeping innerHTML as a // detached string — treating it as a string would hide the very class of bug // these tests exist to catch (stale raw markup surviving a diff). "use strict"; const { execSync } = require("child_process"); class CSSStyle { constructor() { this.props = {}; } setProperty(k, v) { this.props[k] = String(v); } removeProperty(k) { delete this.props[k]; } getPropertyValue(k) { return this.props[k] ?? ""; } get cssText() { return Object.keys(this.props).sort().map((k) => `${k}: ${this.props[k]}`).join("; "); } } const XHTML_NS = "http://www.w3.org/1999/xhtml"; const SVG_NS = "http://www.w3.org/2000/svg"; class DNode { constructor(tag, ns = XHTML_NS) { this.tag = tag; // An element's namespace is fixed at creation. createElement() always yields // HTML — which is why an built that way is inert; see the reconciler. this.namespaceURI = ns; this.childNodes = []; this.attrs = {}; this.parentNode = null; this.listeners = {}; this.nodeValue = null; this.rawHTML = null; this.style = new CSSStyle(); // Tests set .rect to control what getBoundingClientRect reports; there is no // layout engine here, so geometry is whatever the test declares. this.rect = { left: 0, top: 0, width: 0, height: 0 }; } get nodeType() { return this.tag === "#text" ? 3 : 1; } get firstChild() { return this.childNodes[0] ?? null; } get parentElement() { return this.parentNode; } getAttribute(k) { return k in this.attrs ? this.attrs[k] : null; } hasAttribute(k) { return k in this.attrs; } getBoundingClientRect() { const r = this.rect; return { left: r.left, top: r.top, width: r.width, height: r.height, right: r.left + r.width, bottom: r.top + r.height }; } contains(other) { for (let n = other; n; n = n.parentNode) if (n === this) return true; return false; } // Only the attribute-presence selectors the runtime actually uses, e.g. // "[data-floating-id]". closest(selector) { const m = /^\[([a-zA-Z0-9-]+)\]$/.exec(selector); if (!m) throw new Error(`domexec shim: unsupported selector ${selector}`); for (let n = this; n; n = n.parentNode) if (n.nodeType === 1 && n.hasAttribute(m[1])) return n; return null; } appendChild(c) { c.parentNode = this; this.childNodes.push(c); return c; } removeChild(c) { const i = this.childNodes.indexOf(c); if (i < 0) throw new Error("removeChild: node is not a child"); this.childNodes.splice(i, 1); c.parentNode = null; return c; } replaceChild(next, old) { const i = this.childNodes.indexOf(old); if (i < 0) throw new Error("replaceChild: node is not a child"); this.childNodes[i] = next; next.parentNode = this; old.parentNode = null; return old; } setAttribute(k, v) { this.attrs[k] = String(v); } removeAttribute(k) { delete this.attrs[k]; } addEventListener(name, fn) { (this.listeners[name] ??= []).push(fn); } removeEventListener(name, fn) { const l = this.listeners[name] ?? []; const i = l.indexOf(fn); if (i >= 0) l.splice(i, 1); } set innerHTML(html) { for (const c of this.childNodes) c.parentNode = null; this.childNodes = []; if (html !== "") { const raw = new DNode("#raw"); raw.rawHTML = html; raw.parentNode = this; this.childNodes.push(raw); } } get innerHTML() { return this.childNodes.map(serialize).join(""); } get outerHTML() { return serialize(this); } } function serialize(n) { if (n.tag === "#text") return n.nodeValue ?? ""; if (n.tag === "#raw") return n.rawHTML ?? ""; const attrs = Object.keys(n.attrs).sort().map((k) => ` ${k}="${n.attrs[k]}"`).join(""); const style = n.style.cssText ? ` style="${n.style.cssText}"` : ""; return `<${n.tag}${attrs}${style}>${n.childNodes.map(serialize).join("")}`; } const body = new DNode("body"); const documentElement = new DNode("html"); globalThis.document = { body, documentElement, createElement: (tag) => new DNode(tag, XHTML_NS), createElementNS: (ns, tag) => new DNode(tag, ns), createTextNode: (text) => { const n = new DNode("#text"); n.nodeValue = text; return n; }, getElementById: () => null, querySelector: () => null, addEventListener: () => {}, removeEventListener: () => {}, }; // Viewport size the floating engine collides against. Tests override these. globalThis.innerWidth = 1024; globalThis.innerHeight = 768; globalThis.addEventListener ??= () => {}; globalThis.removeEventListener ??= () => {}; globalThis.requestAnimationFrame = (fn) => setTimeout(() => fn(0), 0); globalThis.cancelAnimationFrame = (id) => clearTimeout(id); globalThis.getComputedStyle = (el) => ({ getPropertyValue: (k) => el.style.getPropertyValue(k), fontSize: "16px", }); // ---- go_js_wasm_exec boilerplate ---- globalThis.require = require; globalThis.fs = require("fs"); globalThis.TextEncoder = require("util").TextEncoder; globalThis.TextDecoder = require("util").TextDecoder; globalThis.performance ??= require("performance"); globalThis.crypto ??= require("crypto"); // wasm_exec.js moved from misc/wasm to lib/wasm in Go 1.24. const goroot = execSync("go env GOROOT").toString().trim(); try { require(goroot + "/lib/wasm/wasm_exec.js"); } catch { require(goroot + "/misc/wasm/wasm_exec.js"); } const go = new Go(); go.argv = process.argv.slice(2); go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); go.exit = process.exit; WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { process.on("exit", (code) => { if (code === 0 && !go.exited) { go._pendingEvent = { id: 0 }; go._resume(); } }); return go.run(result.instance); }).catch((err) => { console.error(err); process.exit(1); });