React itself as the test suite
An engine that renders a page is not an engine that runs an application. Getting chatgpt's sign-in page to draw turned out to be the easier half; what was left was a pile of missing JavaScript the page only needed because it is a React application. Given how much of the web that describes, the sensible move was to stop writing test pages and load the real thing.
Eleven scenarios now load the genuine React 18 and 19 development builds and React Router 7 into a document and ask what a page asks: render an element, update on a click, run an effect, hydrate server markup without a mismatch, hear a keystroke in a controlled input, resolve Suspense through a promise, submit a form and prevent it, read an external store and move keyed nodes, run a React 19 form action and use(), and render a route and navigate on a link.
The development builds are the right choice rather than the production ones, because they say what went wrong. Every console.error they print is a finding under the test that hit it, which turns React's own internal assertions into a test suite for somebody else's engine. All eleven pass now.
What they found, in the JavaScript
In the order they found it, because each one blocked the next.
Math.log did not exist, so neither React would load at all. var log = Math.log is the first line of both, for a clz32 fallback. Math now has the transcendental functions through libm, plus clz32, imul, fround and a random that is not 0.5 for ever.
Two symbols were never equal to each other, because strict equality had no arm for them. Every switch on a $$typeof fell through and React refused its own elements as children, which is a confusing thing to be told by a library about itself.
Object.defineProperties was missing. A var declared without an initialiser inside a for-loop's head made a fresh binding in the loop's scope and shadowed the hoisted one, so React 19's host-parent walk lost what it had found the moment the loop ended. An explicit class constructor had no home object, so super() inside one threw "super outside a method" - which is every class component with a constructor, React Router's error boundary among them.
The lexer counted a template substitution's closing brace off without counting its opening brace on, walked its depth below zero on a file full of templates, and panicked on a shift by a negative number. Array.prototype.reduceRight and the URI functions were absent, and a router's first act on a path is to decode it.
What they found, in the DOM
'oninput' in document was false. That is React's feature test for the input event, so a controlled input fell back to a polyfill written for IE8 and never heard a keystroke. Every node, the document and the window now carry the on-event properties, assignable and run on dispatch.
An input with no type attribute answered that it had no type, rather than text. The value, checked and type reflections, the boolean and string ones, the attribute list that hydration walks, and form.reset and requestSubmit are all added. document.title is writable and document.defaultView is the window, which is where a router goes looking for history.
A constructed MouseEvent has a button of 0 and the rest of the mouse, keyboard and pointer fields, because a router treats a click as navigation only when the button is 0 and no modifier is held - so without that, every link was a click that did nothing.
And the console prints an Error as its name and message and fills %s from the arguments, and a call on an undefined name now says which name. Those two are not features, they are the difference between a finding and a blank line.
Bytes, a clock and a random source
A hydrating application also asks for a web platform underneath it.
ArrayBuffer, the nine typed array kinds and DataView exist; a view's bytes sit in a cell every view of the buffer shares. TextEncoder and TextDecoder work, and btoa and atob are the crate's own base64. The integer conversions wrap through a 128-bit type beyond 2^63, because doing the modulo in a double rounded 2^64 - 2 up to 2^64 and turned an Int16 of -2 into -1.
crypto.subtle.digest is SHA-256 and SHA-512 from the same crate the kernel's TLS uses, which is the half of SubtleCrypto a login flow's PKCE challenge needs; the rest of subtle rejects with an error a page can catch rather than pretending. getRandomValues and randomUUID draw from a hash DRBG seeded from the machine's entropy pool, which refuses - as the TLS handshake does - until the pool is credited. Drawn before the seed arrives, the numbers come from a weak seed and the log says so, once.
The browser also turns the page's clock now. Its timers were due against a time nobody had asked the machine for, so a page's setTimeout never fired on the X1 at all. React's scheduler is nothing but timers and a message channel, so a hydrating application was a page that never hydrated - and that is a single missing wire rather than anything to do with React.
What is not built
These are host tests. All eleven pass on the Mac; the X1 is a separate question with a separate set of answers, and the parsing cost of a real React bundle on that machine is its own problem.
Eleven scenarios is eleven scenarios. React's test suite is not eleven scenarios, and passing the ones chosen is weaker evidence than it looks - they were chosen by me, from what an application does, which is not the same as from what React does.
What it did buy is a sign-in that completes: the machine can log in to chatgpt now, which is the first thing on that list that is a result rather than a test passing.