Write the Test Runner
The class is already done. Retype assertEqual, test, and main until the recipe is motor memory — then two silly assertions against a given document.
- Pattern
- Throw on mismatch → log PASS / FAIL
- Time
- the recipe
- Space
- O(1)
This is not a design problem. The document is a prop. You are here to type the test recipe until you can produce it cold, the way a live screen expects the first thing you write to be an assert helper — not a class.
The recipe
Say before you type: "Throw on mismatch. Log PASS or FAIL. main() is just the list of tests."
function assertEqual<T>(actual: T, expected: T, label: string): void {
if (actual !== expected) {
throw new Error(`${label}: expected ${expected}, got ${actual}`);
}
}
function assertTrue(actual: boolean, label: string): void {
if (actual !== true) {
throw new Error(`${label}: expected true, got ${actual}`);
}
}
function assertFalse(actual: boolean, label: string): void {
if (actual !== false) {
throw new Error(`${label}: expected false, got ${actual}`);
}
}
function assertDeepEqual<T>(actual: T, expected: T, label: string): void {
const actualJson = JSON.stringify(actual);
const expectedJson = JSON.stringify(expected);
if (actualJson !== expectedJson) {
throw new Error(`${label}: expected ${expectedJson}, got ${actualJson}`);
}
}
function test(name: string, callback: () => void): void {
try {
callback();
console.log(`PASS: ${name}`);
} catch (error) {
console.error(`FAIL: ${name}`);
console.error(error);
}
}
Four assert helpers, one runner. The message shape is the contract — label: expected X, got Y — so a failure reads as a sentence. Keep them as function declarations, not const arrows; the bench wraps the names.
assertEqual is !==. That is enough for strings, numbers, and booleans. Objects need assertDeepEqual because two { text: "hello" } literals are never ===.
Phase 1 — assertEqual, test, main
Fill the three stubs. Then two tests that actually call assertEqual. They can be this silly:
function main(): void {
test("starts with hello", () => {
const doc = new TextDocument("hello");
assertEqual(doc.getText(), "hello", "document text");
});
test("inserts at the end", () => {
const doc = new TextDocument("hello");
doc.apply({ type: "insert", index: 5, text: "!" });
assertEqual(doc.getText(), "hello!", "after insert");
});
}
main();
The bench is scoring the helpers: does assertEqual throw that exact message, does test print PASS: … / FAIL: …, did main run two tests that held. The document only exists so you have something to point the helpers at.
Two mechanics of the bench worth knowing, because both are invisible from your side. It rebinds assertEqual / test / the rest to count how many assertions your tests actually make — which is why they must be function declarations, not const arrows. And it replays main() to do that counting, so build each test's fixture inside its own test, as above. A const doc hoisted out and mutated by test one will not survive the second run, and you get zero passing tests on a suite your own console just printed two PASS lines for.
Phase 2 — assertTrue, assertFalse
Same throw shape, boolean expected values. Two tests that call them. doc.getText() === "hello" is a free assertTrue.
assertTrue(doc.getText() === "hello", "still hello");
assertFalse(doc.getText() === "", "not empty");
Phase 3 — assertDeepEqual
JSON.stringify both sides, compare the strings. Two tests that call it. A snapshot object is the whole point:
assertDeepEqual({ text: doc.getText() }, { text: "hello" }, "snapshot");
assertDeepEqual(["hello!"], [doc.getText()], "as list");
Key order is insertion order. Keep the objects small and literal so you are testing the helper, not JSON.stringify.
Notes
Write the helper before any test, every time. The failure mode in a live screen is console.log you then read with your eyes.
Test through the public API. The document is already correct; mutating value to make an assertion pass is how you fake a pass in the real problems too.
When a helper stumbles, retype the whole recipe — not the one line. The point is the shape, not the patch.