Browser support¶
Measured, on the same day, against the same build: the
simple-app demo, forced to an 80×24 grid, diffed cell by cell against the
same application running in a real terminal.
Engine |
Version |
Renders identically to a terminal |
JSPI |
Notes |
|---|---|---|---|---|
Chromium |
153 |
yes |
yes |
Chrome, Edge, Brave, Opera, Arc — one engine |
Firefox |
155 |
yes |
yes |
|
WebKit |
26.6 |
yes |
yes |
Playwright’s build; see the Safari caveat below |
Safari |
— |
not yet reported |
— |
Checked weekly on macOS; see below |
No page errors, no failed requests, and no divergent rows in any of them. SharedArrayBuffer
is unavailable in all three, which is expected: the default build is main-thread only so that
no COOP/COEP headers are needed.
What is checked, and how¶
Every push runs textual-wasm check --strict --browser <engine> for all three engines. Each
one builds the app, serves it, boots Pyodide in that browser, sends a keystroke, reads the
grid back, and diffs it against a capture from a real pty running the same app through
Textual’s own driver.
$ textual-wasm check --browser firefox
│ browser │ ran │ firefox 155.0, 19 rows rendered │
│ terminal │ ran │ tmux 3.5a │
identical: terminal and browser render the same
You can point it at the browser you ship to:
|
Drives |
|---|---|
|
Playwright’s own builds. Reproducible; what CI runs. |
|
The browser installed on your machine, through Playwright’s channels. |
|
Real Safari through |
Important
What this does not cover
The comparison reads xterm.js’s buffer, not pixels — and xterm.js’s character-width logic is the same JavaScript in every browser. So cell assignment is engine-independent by construction, and a green row here does not mean the glyphs were drawn identically.
What multi-engine testing catches is everything around that: whether Pyodide boots, whether the page’s JavaScript works, which capabilities each engine reports, and how fast each one gets there. What it cannot catch is a font rendering wider than the cell it was assigned. That is a property of the font and the platform’s text stack, not of the engine, and covering it would need pixel comparison — which is flaky enough across engines to be worth less than it costs.
Why Edge is not in the matrix¶
It is Chromium, running the same xterm.js. Testing it would measure the same renderer twice
and report the result as though it were independent evidence. The same goes for Brave, Opera,
Arc and Vivaldi.
--browser msedge exists if you want to run it against your installed copy — for a policy or
enterprise-configuration question, where the browser really is different — but it is not part
of the matrix.
Safari¶
Playwright’s WebKit is not Safari. It is a different port of the same engine, built against GTK on Linux, with a different font stack and none of Apple’s platform limits. It answers “does this engine run the page”, which is most of what matters. It cannot answer two questions that are specific to Safari:
The font fallback, which decides how a glyph is drawn when the pinned stack is absent.
The WebAssembly memory ceiling, which on iOS in particular is real and which a ~10 MB interpreter has to fit inside.
So real Safari runs on a macOS runner once a week, outside the per-push gates.
safaridriver is the flakiest link in this whole matrix and macOS runners are slow; a gate
people learn to re-run is worse than no gate. A failure there is a signal to investigate, not
a blocked merge.
That leg is new and has not yet reported. This page will carry its result when it does. If you rely on Safari today, run it yourself:
sudo safaridriver --enable
textual-wasm check --browser safari --app myapp.main:App
Mobile¶
Untested. A Textual app in a mobile browser has two problems that are not about this project: there is no physical keyboard, and iOS applies a tighter WebAssembly memory ceiling than desktop Safari. The terminal renders; whether an app is usable that way is a question about the app.
Requirements¶
Whatever the engine, a host has to serve .wasm as application/wasm and support
WebAssembly with BigInt integration — which every browser in the table above has done for
years. No cross-origin isolation, no special headers, no service worker.