Headless execution of the Rust engine in browsers, Node and WASI runtimes for differential tests
Document status:
reviewed. Canonical source.
Summary
Three headless execution paths exist for a Rust engine compiled to WebAssembly. wasm-bindgen-test compiles #[wasm_bindgen_test] functions for wasm32-unknown-unknown and runs them under Node by default or, with wasm_bindgen_test_configure!(run_in_browser) or WASM_BINDGEN_USE_BROWSER=1, in headless Chrome, Firefox or Safari through WebDriver; wasm-pack test --headless --chrome --firefox wraps the runner and driver management. wasmtime executes wasm32-wasip1 modules and wasm32-wasip2 components (both Tier 2 targets shipped by rustup) with capability-scoped filesystem access (--dir) and direct function invocation (--invoke), which suits CLI-equivalent conformance runs without a browser. Playwright launches Chromium, Firefox and WebKit headlessly and evaluates JavaScript in the page (page.evaluate), which is the mechanism for browser differential layout tests: a page renders the CSS-equivalent of a NUIF fixture, the test reads getBoundingClientRect results, and compares them against the WASM engine’s LayoutSnapshot loaded in the same page or in Node.
NUIF interpretation: the engine’s differential layout suite should run in three layers: wasmtime for fast, GPU-free CLI parity; wasm-bindgen-test under Node for binding-level round trips; Playwright for browser-oracle comparisons where the oracle is the browser’s own layout, not WebGPU rendering.
Evidence
- wasm-bindgen-test: “an experimental test harness for Rust programs compiled to Wasm using
wasm-bindgenand thewasm32-unknown-unknowntarget”; tests are written with#[wasm_bindgen_test]and run withcargo test --target wasm32-unknown-unknown;#[wasm_bindgen_test(unsupported = test)]falls back to#[test]on native targets; tests “must be in the root of the crate, or within apub mod”. Locator: wasm-bindgenguide/src/wasm-bindgen-test/{index.md,usage.md}, main (2026-08). - Versions: wasm-bindgen 0.2.127 and wasm-bindgen-test 0.3.77 published 2026-08-08; wasm-pack 0.15.0 published 2026-05-15. Locator: crates.io API; wasm-bindgen
CHANGELOG.md“[0.2.127]”. - Browser configuration: default is Node;
WASM_BINDGEN_USE_BROWSER=1,WASM_BINDGEN_USE_DEDICATED_WORKER,..._SHARED_WORKER,..._SERVICE_WORKER,WASM_BINDGEN_USE_DENO,WASM_BINDGEN_USE_NODE_EXPERIMENTAL; forced per crate viawasm_bindgen_test_configure!(run_in_browser | run_in_dedicated_worker | run_in_shared_worker | run_in_service_worker | run_in_node_experimental). Locator:guide/src/wasm-bindgen-test/browsers.mdlines 1-35. - Headless drivers:
wasm-pack test --headless --chrome --firefox --safari; without wasm-pack setCHROMEDRIVER,GECKODRIVERorSAFARIDRIVER(orCHROMEDRIVER_REMOTE) and runcargo test --target wasm32-unknown-unknown;webdriver.jsonorWASM_BINDGEN_TEST_WEBDRIVER_JSONsupplies capabilities;NO_HEADLESS=1serves the tests for a visible browser. Locator:browsers.mdlines 58-165. - CI examples run
cargo testnatively, thenwasm-pack test --headless --chromeand--firefox(GitHub Actions). Locator:guide/src/wasm-bindgen-test/continuous-integration.md. - wasm-pack test: wraps
wasm-bindgen-test-runner; accepts a crate path,--release, environment flags--node --firefox --chrome --safari --headless,--panic-unwind(nightly,-Z build-std), and passes extra arguments tocargo test. Locator: wasm-packdocs/src/commands/test.md. - Coverage on wasm requires nightly
-Cinstrument-coverage -Zno-profiler-runtimeand--cfg=wasm_bindgen_unstable_test_coverage, thencargo +nightly llvm-cov test --target wasm32-unknown-unknown. Locator:guide/src/wasm-bindgen-test/coverage.mdlines 12-46. - wasmtime 48.0.1 (2026-08-24): README shows
rustup target add wasm32-wasip2,rustc hello.rs --target wasm32-wasip2and running the resulting component; built on Cranelift; supports WASI. Locator: wasmtimeREADME.mdlines 63-131. - wasmtime CLI:
wasmtime --dir=. --dir=/tmp demo.wasm args...grants directory capabilities (--dir=host::guestmapping);wasmtime run --invoke 'add(1, 2)' add.wasmcalls exported functions of modules or components and skipswasi:cli/run;-W/--wasmconfigures proposals. Locator:docs/WASI-tutorial.mdlines 157-243;docs/cli-options.mdlines 22-142, 336. - rustc targets:
wasm32-wasip1is Tier 2, cross-compiled, shipsstdwith a self-contained sysroot, installed byrustup target add wasm32-wasip1, and “will generate core WebAssembly modules”;wasm32-wasip2is Tier 2 and “outputs a component” built on the component model. Locator: rustc bookplatform-support/wasm32-wasip1.md,wasm32-wasip2.md. - Playwright: latest release v1.62.1 (2026-07-30);
npx playwright install chromium|firefox|webkit,install --with-deps; projects run the same test underchromium,firefox,webkit;page.evaluate(() => document.location.href)returns serialisable values from page scripts, includingasyncfunctions. Locator:docs/src/browsers.md;docs/src/evaluating.mdlines 15-44;gh api repos/microsoft/playwright/releases/latest. - Taffy generates layout fixtures by driving Chrome for Testing through ChromeDriver with
fantoccini, downloading a matching Chrome/driver pair once per version. Locator: Taffyscripts/gentest/src/main.rslines 11-67;CONTRIBUTING.mdlines 26-35. - egui_kittest removes
Backends::BROWSER_WEBGPUfrom its test setup because it relies on blocking screenshots. Locator: eguicrates/egui_kittest/src/wgpu.rslines 20-27. - The NUIF whitepaper and ADR 0001 assign the WASM boundary to Rust and the editor shell to a web stack; ARCHITECTURE.md draws the Rust core behind a WASM boundary. Locator:
docs/whitepaper/06-language-and-runtime-choice.md;adrs/0001-rust-reference-core.md;apps/editor/ARCHITECTURE.md. - Figma documents that a plug-in UI iframe can use browser APIs including WebAssembly, while host-document access remains in the plug-in API. This supports a capability-free WASM core in the iframe and a thin, separately tested host adapter. Locator: https://developers.figma.com/docs/plugins/, retrieved 2026-08-30.
Mechanism
Three execution layers and their contracts:
1. wasm32-wasip1 / wasip2 ──▶ wasmtime run --dir=fixtures::/fixtures nuif-cli.wasm layout /fixtures/x.nuif --context ...
oracle: identical stdout/JSON to the native CLI (parity test); no browser, no GPU; capability-scoped FS.
2. wasm32-unknown-unknown ──▶ cargo test --target wasm32-unknown-unknown (Node by default)
#[wasm_bindgen_test] fn layout_roundtrip() { let snap = engine.layout(doc, ctx); assert_eq!(canonical(snap), expected) }
oracle: expectations embedded or fetched; exercises wasm-bindgen glue and JS-facing API.
3. Playwright ──▶ chromium/firefox/webkit headless
page.setContent(html_from_fixture); const boxes = await page.evaluate(() => [...document.querySelectorAll('[data-nuif-id]')]
.map(e => { const r = e.getBoundingClientRect(); return [e.dataset.nuifId, r.x, r.y, r.width, r.height]; }));
const ours = await page.evaluate(() => nuif.layout(doc, ctx)); // WASM engine loaded in the same page, or run in Node
compare(boxes, ours, tolerance_from_context);
oracle: browser layout; only for semantics where NUIF declares CSS equivalence (conformance/PLAN.md).
Invariants and constraints from the sources: wasm-bindgen tests must be in the crate root or a pub mod; browser runs need a driver binary and a browser on the host (wasm-pack manages drivers); Safari runs only on macOS; wasmtime exposes files only under --dir mappings; wasm32-wasip2 produces components, so a CLI targeting wasip2 must use wasi:cli/run or --invoke; blocking GPU readback is unavailable in browsers, so in-browser tests should assert on scene data or layout boxes, not on rendered pixels.
Reproducibility: pin browser versions (Playwright pins browser builds per release; Taffy pins Chrome for Testing), pin wasmtime (cargo install --locked wasmtime-cli at a fixed version), and record the browser name and version in the differential report as part of the evaluation context.
The implemented first layer is nuif-wasm-api-0: a
wasm32-unknown-unknown module generated with wasm-bindgen 0.2.127. It accepts
explicit canonical-text/CBOR, deterministic packages, capability-set and patch
byte arrays; exposes validation, hashing, encoding, bounded atomic application
and exact undo/redo; and declares no host authority. Structural package load
retains inert verified resources. Evaluation requires a separate exact
manifest-capability check. cargo xtask gate-wasm generates Node and
direct-browser packages, initializes the web target in pinned headless Chrome,
drives the Node package, and requires edited bare and package bytes to equal the
native CLI. A behavior-bearing package proves exact resource preservation plus
typed missing/exact capability outcomes without executing behavior. This closes
binding and browser-package initialization only; it does not close
browser-layout, WASI CLI or host-adapter behavior described above.
NUIF relevance
Borrow
- wasmtime with
--diras the sandboxed executor for awasm32-wasip1build ofnuif-cli, because it yields CLI parity tests without a browser and demonstrates the capability boundary that the security suite needs. - The wasm-bindgen-test/wasm-pack headless flow for binding-level tests of the engine’s JS API, because ARCHITECTURE.md requires the editor’s in-process API to mirror CLI semantics and this tests the boundary itself.
- Taffy’s pinned Chrome for Testing approach for browser oracles, because differential layout results are only reproducible with a pinned browser build.
Adapt
- Playwright rather than raw WebDriver for the browser differential suite, because one runner covers Chromium, Firefox and WebKit and
page.evaluatereturns structured results; the runner should live underxtaskor atests/browserpackage and emit the NUIF report format. - Browser-differential fixtures should be generated from NUIF fixtures into HTML with
data-nuif-idattributes so that boxes are keyed byEntityId, mirroring Taffy’s generated fixtures.
Reject
- In-browser WebGPU rendering as a conformance oracle, because blocking readback is unavailable (egui_kittest removes
BROWSER_WEBGPU) and GPU results vary by implementation; render conformance stays on the CPU reference path. - Node-only testing as the sole WASM layer, because browser layout is the oracle for CSS-equivalent semantics and only a browser provides it.
Open questions
- Whether
wasm32-wasip2component output should be the CLI’s WASM form now, or whetherwasip1modules are preferable until the component-model boundary (nuif:research:wasm-component-model) is adopted. - Whether wasm-bindgen’s browser runner or Playwright should own the browser differential tests; running both duplicates driver management.
- Whether font availability in headless browsers can be pinned tightly enough for text-dependent layout fixtures, or whether differential tests must exclude text metrics.
- Whether
wasm32-unknown-unknownbuilds ofnuif-renderwithwgpuare needed at all for tests, given that in-browser pixel assertions are rejected above. - Whether to acquire wasm-bindgen’s immutable prebuilt CLI archives with per-platform checksums instead of compiling its full optional test-runner tool graph. The latter currently warns about future-incompatible HTTP-server dependencies, but those dependencies are absent from the NUIF module and workspace runtime graph.