Real apps
Generated from docs/compat/apps.md when the site was built at 7aa23e2.
niso’s compatibility target is real apps that run and look right on every host (DECISIONS.md, Priorities). WPT stays the regression net. The corpus:
| App | Build | Driver’s flow |
|---|---|---|
TodoMVC, Svelte, React and Vue (todomvc-svelte, todomvc-react, todomvc-vue) |
tastejs/todomvc at ff43b02, each example built as upstream builds it, from its lockfile: Vite for Svelte and Vue, webpack for React | add three todos, complete one, filter Active, Completed and All through the filter links, edit one by double-click, delete one, reload |
Excalidraw (excalidraw) |
@excalidraw/excalidraw 0.18.1 in a minimal Vite page (tests/apps/excalidraw/host, every version pinned by its package-lock.json), with its fonts served beside it |
pick tools on the toolbar, draw a rectangle and an arrow by dragging with pointer events, add text in the text editor, select the rectangle and drag it, undo with Ctrl+Z, redo with the toolbar’s button, export to a canvas and to PNG |
VS Code for the web (vscode-web) |
the vscode-web 1.145.2 standalone build (commit c4453f8) | open an untitled JavaScript file, type a function, check its tokens are highlighted (vscode-web.md) |
| Bluesky’s social-app (web build) | not in the harness yet (bluesky.md) | |
niso’s docs site (Astro Starlight, docs/site/ on docs-study) |
not in the harness yet |
Running
Section titled “Running”tests/apps/run.sh NAME --fetchtests/apps/run.sh NAME [--engine quickjs|node|bun] [--screenshot]--fetch fetches and builds the app with tests/apps/<app>/fetch.sh into
$NISO_APPS/NAME (target/apps by default), whose root/ is the static
serve root. It is the only step that uses the network, so runs work offline.
It stamps the build with a hash of what the app pins (fetch.sh, host/
with its lockfile, make-index.mjs) and the variant; a run whose build is
missing or stamped differently, as after a pin is bumped, stops and asks for
--fetch. The vscode-web tarball is checked against its sha256. Each run
serves it at http://localhost/ and loads its
index.html with the app’s driver.js added as the last module script. The
driver acts the way a user’s input arrives (DOM events: clicks, pointer
drags, typing, keys), checks the app’s state after each step, and prints
NISO-APP ok STEP, NISO-APP FAIL STEP: why and NISO-APP note ... lines,
then NISO-APP done. A driver that prints NISO-APP reload runs again as a
reload: a second process with the same storage directory (--storage, in
niso-run and niso-node). run.sh prints the driver’s lines and exits 1 on a
FAIL or when the driver never finishes. --screenshot renders the page with
niso-iced into NAME/ENGINE.png: on QuickJS niso-iced runs the page and its
driver itself; on Node and Bun it renders the DOM the run ended with, with
scripts removed, so canvases are blank there, with a storage directory of
its own (NISO_STORAGE_DIR). Each engine’s runs write pages of their own
(root/niso-app-ENGINE-N.html), so runs on different engines can share a
build at the same time. NISO_PROFILE=debug uses debug
builds, and NISO_BUILD=0 uses what is already built (for machines where
builds take a shared lock).
An app is a directory with fetch.sh WORKDIR [VARIANT], driver.js and an
optional app.conf (max_ms, the page time each run gets, and size, the
screenshot’s). todomvc-svelte is tests/apps/todomvc with the variant
svelte.
Status
Section titled “Status”Checked on 2026-11-09 on Linux (yachiyo) at 5589c475, now on main, with debug builds for Node, Bun and niso-iced. TodoMVC Svelte on QuickJS was run again on 2026-11-10 on macOS with main at 621af1a6: edit and delete still do nothing.
| App | QuickJS (niso-run) | Node 26.10 (V8) | Bun 1.4.4 (JSC) | niso-iced (QuickJS) | iOS, Android |
|---|---|---|---|---|---|
| TodoMVC Svelte | edit and delete do nothing (TodoMVC 1) | passes | passes | runs; looks wrong (TodoMVC 2-6) | not run |
| TodoMVC React | passes | passes | passes | runs; looks wrong (TodoMVC 2-6) | not run |
| TodoMVC Vue | passes | passes | passes | runs; looks wrong (TodoMVC 2-6) | not run |
| Excalidraw | nothing can be drawn; the whole flow passes behind three probes | the whole flow passes behind two probes | as Node | draws; its UI looks wrong (Excalidraw 4-5) | not run |
| VS Code | highlights | highlights | highlights | see vscode-web.md | not run |
“Passes” means every driver step says ok. TodoMVC’s reload step checks that localStorage survives a reload, with a key of the driver’s own: tastejs’s current examples keep their todos in memory, so each shows an empty list after the reload, as it does in a browser.
Probes
Section titled “Probes”When a step fails on a missing API, Excalidraw’s driver logs the failure,
then installs probes and retries the step, to see what breaks next. Probes
are logged (NISO-APP note probe: ...) and stand in for what niso lacks.
They are not fixes, and a run that needs one fails. Today’s:
document.getSelection returning null, FontFace and document.fonts
stand-ins that load nothing and, on QuickJS, RegExp.$1-$9 set by RegExp.prototype.exec.
TodoMVC
Section titled “TodoMVC”| QuickJS in niso-iced (Vue) | Chrome, the same DOM |
|---|---|
![]() |
![]() |
The other screenshots: Svelte (its edit and delete failed, so it still has three todos), React, and Vue’s DOM after a Node run, rendered by niso-iced: Vue on Node.
What breaks, most harmful first:
- Svelte 5 loses its event handlers on QuickJS, so edit and delete do
nothing. Svelte keeps delegated handlers (click, dblclick, keydown,
input, …) as an expando on each element, set while the element is still
in a cloned template, and looks them up on the element when an event
comes. niso’s wrapper of a node that isn’t connected is weak
(
bindings/js/glue/wrappers.js; the known limitation in docs/engines/napi.md), so a label or button that no script refers to gets a new wrapper without the handlers once the old one is collected, which QuickJS does at once. V8 and JSC collect later, so Node and Bun pass by luck of timing. Every Svelte 5 app is affected. Medium: a wrapper with expandos has to live while its node can still be reached, which the core can tell the binding (DECISIONS, Core: the core notifies bindings when a node becomes unreachable). - No text decorations in the render stream (fixed on main since this check by 725ab097, which sends decoration lines with the text and has every host draw them; TodoMVC hasn’t been run again with it): the completed todo wasn’t struck through.
- No CSS background images or SVG images in the render stream: the
todos’ check circles (an SVG data URL in
background-image) are missing, so nothing shows which todos are done. render.rs sends only<img>rasters and canvases (IMAGE);ImageData::Svgand backgrounds are dropped. Large: background layers (position, size, repeat) and SVG rasterizing, for every host. - Inline boxes lose their padding, margins and borders: the filter links run together (“AllActiveCompleted” in Vue, whose compiled template has no spaces between them) and the selected filter has no border. Either the inline layout (blitz and Parley) or the render stream; not narrowed down. Medium.
- niso-iced draws no rotations: the toggle-all chevron stays unrotated
over the input (“iced draws only translations and uniform scales of a
transform” in its log).
hosts/iced. Small to medium. - Box shadows (the card’s shadow and the stacked-paper footer) and the placeholder’s italic are missing in niso-iced. Small each; not narrowed down.
Fixed before this check: a click on a link to a fragment did nothing, so Svelte’s and React’s filters never changed, and fragment navigations fired no popstate, which React Router’s hash history listens for.
Excalidraw
Section titled “Excalidraw”| QuickJS in niso-iced, after the flow | Chrome, the same DOM (canvases blank) | Node’s DOM in niso-iced |
|---|---|---|
![]() |
![]() |
![]() |
With the probes, the flow passes on all three engines: the rectangle and the arrow are drawn on the static canvas, text goes in through the text editor, the rectangle moves, Ctrl+Z and the redo button work, and export gives a 420x255 canvas with ink and a valid PNG of it. What breaks, most harmful first:
- No Selection API: every pointerdown on the canvas throws at once
(
document.getSelectionis not a function: right after capturing the pointer, Excalidraw clears the document’s selection), so nothing can be drawn, with real input too, on every engine and host. niso has noSelection, nowindow.getSelectionordocument.getSelection, and noRangeorStaticRange. Large: the Selection API and Range on the core’s DOM (DECISIONS, Editing), in a crate of their own; WPT’sselection/anddom/ranges/are the checklist. - No CSS Font Loading API:
FontFaceanddocument.fontsdon’t exist. Excalidraw’s font loading rejects at startup, and the text tool and export throw (“FontFace is not defined”), so there is no text and no export. Medium to large: FontFace and FontFaceSet over the fonts@font-facealready loads, in their own crate; WPT’scss/css-font-loading/is the checklist. - QuickJS-ng has no RegExp legacy statics (
RegExp.$1): Rough.js parses every shape’s SVG path with them, so on QuickJS the first shape throws inside React’s render and the whole editor unmounts. An engine gap (third_party/quickjs); Node and Bun have them. Small to medium in QuickJS-ng’s regexp exec (the last match kept on the realm), best done upstream. - Inline SVG isn’t drawn: every toolbar and panel icon is an empty
button. The render stream has no op for
<svg>content (crates/niso-core/src/render.rs). Large: SVG rendering for every host (rasterized with usvg as a first step). - Visually hidden headings show (“Shapes”, “Actions”, “Canvas
actions”): Excalidraw’s
.visually-hidden(1x1 px,overflow: hidden,clip: rect(1px, 1px, 1px, 1px)) doesn’t hide them in niso-iced. Eitherclipor the overflow clip of a 1 px box; not narrowed down. Small to medium. - Missing UI Events members that React and Excalidraw look up
(
niso-run --missing):MouseEvent.relatedTarget,movementX,movementYandgetModifierState, andFocusEvent,CompositionEvent,ClipboardEvent,DragEvent,HTMLElement.contentEditableandHTMLLinkElement.relList. React synthesizes mouseenter and mouseleave from relatedTarget, so hover states can go wrong. Small to medium each.
Fixed before this check: under Node and Bun, a module script’s own module
wasn’t in the module map, so Vite’s chunks, which import back from the
entry, ran it a second time; Excalidraw mounted twice into one container,
React’s second mount failed in removeChild, and the editor never came up.
setPointerCapture threw NotFoundError for the mouse until a host reported
it; the mouse is now pointer 1 and always active, as in Chrome (DECISIONS,
Editing). Native radio groups replaced Excalidraw’s own radio buttons, which
it hides (opacity: 0) to draw its icons; such a group now stays plain
(DECISIONS, Elements). The screenshots above predate both.
Harness gaps
Section titled “Harness gaps”- Drivers dispatch DOM events from page script, so their events are
untrusted and skip the host’s input path (hit testing, pointer capture,
focus, the compatibility mouse events). niso-wpt’s testdriver drives WPT’s
tests through niso-actions (
niso_dom_pointerand the other input calls); a page-facing bridge to it in niso-run and niso-node would let drivers give real input. - niso-iced runs only QuickJS, so Node’s and Bun’s screenshots show their final DOM without scripts or canvas content.




