How this is builtGroundworkHow this is built
13 / 26 written chapters

I7 · Part 2 · How it runs

How an answer gets checked

Tests written once in JavaScript, replayed in six other languages, and a React sandbox in an iframe.

9 min read1 diagram8 sections
08On this page
  1. What a test is
  2. Grading JavaScript and TypeScript
  3. Other languages: record, then replay
  4. Recording
  5. Starters
  6. The marked-line harness
  7. Solved
  8. React component exercises

What a test is

Every exercise lives in content/practice/ as a plain object: an id, a chapter, a level, a title, a brief in HTML, the starter code, hints, a reference solution and a list of tests. A test is only a name and a body, and the body is JavaScript that calls the reader's function and asserts on the answer:

{ name: 'null reports "null"', body: 'assert.equal(kindOf(null), "null");' }

There are 538 exercises with 4,362 tests between them, and 99 of the exercises are React components rather than functions. The assert the tests call comes from lib/assertKit.ts, 75 lines with six methods: ok, equal, notEqual, deepEqual, type and throws. Its deepEqual treats two NaNs as equal, compares Maps and Sets by their printed form, and walks arrays and plain objects key by key. Its fmt prints values the way the console does, so a failure reads "expected [1, 2] but got [2, 1]" rather than "[object Object]". The same file is used by the JavaScript worker, the React sandbox and the build-time recorder, so a test means the same thing in all three.

Grading JavaScript and TypeScript

Submit calls the runner with the exercise's tests. buildTestSource in lib/runner.ts turns each one into an async function with its own try/catch, which pushes { index, name, ok, message } onto a results array. The tests are placed after the reader's code inside the same function, so they can call anything the reader declared. One failing test cannot stop the others, and a test that throws reports the error's message as the reason.

TypeScript is compiled first and then graded exactly like JavaScript. If the reader's code crashes before the tests, or runs past the five-second limit, the result list comes back empty, and the Test Result tab says so instead of claiming a score.

Other languages: record, then replay

The tests are JavaScript, but a reader can solve a problem in Python, Ruby, PHP, Lua, C or C++. Nobody wrote those tests by hand. They are recorded from the reference solution at build time, and the recorded answers are replayed against the reader's code.

AT BUILD TIME IN THE BROWSER An exercise solution + JS tests recordPolyglot() runs the solution in node:vm and records every call /problems/[slug]/cases force-static JSON signature and cases fetched once withHarness() reader's code, then a call for every recorded case The runtime prints one marked line per case gradeResults() matches rows to cases numbers within 1e-6 then the Test Result tab
The reference solution is the oracle. A test written once in JavaScript becomes a list of inputs and expected outputs that any language can be asked to reproduce.

Recording

lib/polyglot/record.ts finds the solution's first top-level function and reads its parameter names. It then keeps only the tests it can replay: tests whose every statement is either assert.equal or assert.deepEqual (or their strict forms) called directly on that function, or a plain const. Anything that chains off the result, such as fn(x).length, is skipped.

For each kept test it runs the solution with vm.runInNewContext and a two-second timeout, with the function wrapped so every call records its arguments and its return value. Arguments are copied through JSON before the call, so a solution that mutates its input does not change what was recorded. A value only counts if it is plain data: finite numbers, booleans, strings and nested arrays. A function that returns null or an object gives up. The recorder then infers a signature from the values, with unify widening int and float to float and refusing anything that changes kind between calls.

app/problems/[slug]/cases/route.ts runs this for every exercise. It is force-static with generateStaticParams, so every file is written once during the build and served as static JSON. The workspace fetches it only when the reader switches a problem to any language except JavaScript and SQL, or presses Debug, and keeps it in a Map for the rest of the visit.

Of the 538 exercises, 222 record cleanly. They produce 2,048 cases across 1,439 tests, and 395 of their tests are skipped because they check a property of the answer rather than the answer itself. The Test Result tab says how many were skipped and suggests switching to JavaScript to run them. The other 316 stay JavaScript-only, each with a reason the editor shows in place of a starter:

Why it stays in JavaScriptExercises
A React component99
Its tests check properties only JavaScript can replay84
It works on JavaScript objects61
It is about JavaScript itself (no top-level function)27
Its function takes arguments only JavaScript can express17
It is about JavaScript's async model11
Its arguments change type between calls11
Its tests check something besides the return value6

Starters

lib/polyglot/starters.ts turns the signature into starter code for 13 languages: the six graded ones plus TypeScript, Java, Go, Rust, Kotlin, Swift and C#. Each starter declares the function with the right types, returns a zero value of the return type, and names the call the tests will make. Ruby and Rust get snake_case names. Lua's starter warns that its tables start at 1. The C starter adds a length after each array argument and a returnSize pointer when it returns an array. A list of integers comes out like this:

LanguageA list of integers
TypeScriptnumber[]
Pythonlist[int]
C++vector<int>
Cint* plus a length
Java, C#int[]
Go[]int
RustVec<i32>
KotlinIntArray
Swift[Int]

Java, Go, Rust, Kotlin, Swift and C# get a starter but no grading, because nothing runs them in the browser.

The marked-line harness

withHarness in lib/polyglot/grade.ts appends a driver to the reader's code. For Python, Ruby, PHP and Lua it embeds the cases as data and loops over them. For C and C++ it writes one block per case, declaring each argument as a literal of the right type, because those languages have no JSON parser to lean on. Every case prints one line: the marker @@groundwork-case@@, then a JSON array of test index, case index, the value returned, and the error message if the call threw.

While the run streams, the workspace takes any console line that starts with the marker out of the output and keeps it as a row. The reader's own prints still appear. When the run ends, gradeResults checks every case of every test. Numbers match within a relative tolerance of one in a million, so 0.1 + 0.2 passes. A missing row means the call crashed or ran out of time, and the message says so. A test passes only when every one of its cases does.

C is the strictest. canGrade accepts it only when every argument and the return value are a number, a boolean, a string, or a flat array that is not of strings. That holds for 155 of the 222 recordable exercises.

Solved

finishTests marks a problem solved when there is at least one result and every one passed. progress.setExerciseSolved writes it to localStorage, adds to today's activity count the first time, and notifies anything subscribed to progress. Then two things fire.

  • Confetti appends 60 spans in five theme colours, each with a random fall time between 1.6 and 2.8 seconds, a drift of up to 80 pixels either way and a spin, and clears them after 3.2 seconds. It does nothing when the reader prefers reduced motion.
  • A ding from lib/sound.ts: two sine tones made with the Web Audio API, 880 Hz and then 1,318.5 Hz 90 ms later, each fading out within a third of a second. No audio file is downloaded. A button in the top bar mutes it, and the choice is saved.

In the mock interview the same results are reported upward instead, as passed, total, hints used and whether the solution was seen.

React component exercises

A component needs a DOM, so the 99 component exercises run in an iframe rather than a worker. scripts/build-react-sandbox.mjs uses esbuild to bundle lib/reactSandbox/runtime.ts, React 19 and a small testing library in lib/reactSandbox/dom.ts into one minified IIFE, public/wasm/react-sandbox.js, 205,306 bytes. It runs before every dev start and build, so the file always matches the source.

lib/reactRunner.ts compiles the reader's JSX and the tests with TypeScript, inserting the loop guard, then creates an iframe with sandbox="allow-scripts" and a srcdoc that loads the bundle. The sandbox posts ready; the runner posts the compiled source; the sandbox runs it with 55 names passed in as function parameters and posts console lines and results back.

Those 55 names are the test environment. They include React's hooks and helpers, render, renderHook, cleanup, a screen object with 42 queries (seven ways to find an element, such as role, label text and placeholder, each in the get, getAll, query, queryAll, find and findAll forms), within, user events such as click, type, selectOption and tab, waitFor with a 1,500 ms default, and mockFetch with jsonResponse. The names follow Testing Library's, so the tests read like the ones readers will write at work.

Each test starts by rendering and removing a throwaway button, then runs its body, then calls cleanup, which unmounts the root, empties the container and puts the real fetch back. Rendering goes through flushSync and rethrows errors caught by React's root, so a component that throws while rendering fails the test at that line. Links and form submits are cancelled so a test cannot navigate the frame away. Pressing Run instead of Submit skips the tests and mounts the reader's App in the preview pane.

The grader has its own tests

tests/polyglot.test.ts has 15 cases. The most important records every exercise, calls each recordable one's reference solution with every recorded case, and requires the recorded answer back. It also requires at least 200 exercises to be recordable. A recorder change that would mark correct answers wrong fails the test suite instead of a reader's afternoon.