Files
kjol/go/wasmruntime/testdata/domexec.js

110 lines
3.6 KiB
JavaScript

// 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 DNode {
constructor(tag) {
this.tag = tag;
this.childNodes = [];
this.attrs = {};
this.parentNode = null;
this.listeners = {};
this.nodeValue = null;
this.rawHTML = null;
}
get firstChild() { return this.childNodes[0] ?? 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("");
return `<${n.tag}${attrs}>${n.childNodes.map(serialize).join("")}</${n.tag}>`;
}
globalThis.document = {
createElement: (tag) => new DNode(tag),
createTextNode: (text) => { const n = new DNode("#text"); n.nodeValue = text; return n; },
getElementById: () => null,
};
// ---- 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);
});