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.
08On this page
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.
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 JavaScript | Exercises |
|---|---|
| A React component | 99 |
| Its tests check properties only JavaScript can replay | 84 |
| It works on JavaScript objects | 61 |
| It is about JavaScript itself (no top-level function) | 27 |
| Its function takes arguments only JavaScript can express | 17 |
| It is about JavaScript's async model | 11 |
| Its arguments change type between calls | 11 |
| Its tests check something besides the return value | 6 |
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:
| Language | A list of integers |
|---|---|
| TypeScript | number[] |
| Python | list[int] |
| C++ | vector<int> |
| C | int* plus a length |
| Java, C# | int[] |
| Go | []int |
| Rust | Vec<i32> |
| Kotlin | IntArray |
| 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.
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.
Finished reading?
0 of 26 chapters read in this series