84. Testing the lab, extending it, and where to go next
The lab's own test suite
tests/lab.spec.js runs every test under both projects, http1
(http://localhost:18080) and h2 (https://localhost:18443, with
ignoreHTTPSErrors for the self-signed cert). Because the same assertions
run over both protocols, they double as protocol tests:
test("request burst: protocol decides how many connections", async ({ page }, testInfo) => {
await open(page, "network");
await page.getByLabel("Number of requests").fill("12");
await page.getByLabel("Server delay (ms)").fill("300");
await page.getByRole("button", { name: "Fire the burst" }).click();
const readout = (label) => page.locator(".readout", { hasText: label }).locator("b");
if (testInfo.project.name === "h2") {
await expect(readout("Protocol")).toHaveText("h2");
await expect(readout("Connections used")).toHaveText("1");
} else {
await expect(readout("Protocol")).toHaveText("http/1.1");
await expect(readout("Connections used")).toHaveText(/^[2-7]$/);
}
});
test("the event-loop model matches the real engine for every snippet", async ({ page }) => {
await open(page, "eventloop");
for (const tab of await page.getByRole("tab").all()) {
await tab.click();
await page.getByRole("button", { name: "Run it for real" }).click();
await expect(page.locator("[data-match]")).toHaveAttribute("data-match", "true");
}
});
Other tests cover: every station renders without unexpected page errors,
the page is cross-origin isolated, the render tree drops display:none,
thrashing is slower than batching, Atomics loses 0 updates, the worker
keeps the page responsive, IndexedDB round-trips and the
TransactionInactiveError pitfall, a late ad pushes CLS past 0.1, a
350 ms handler registers as a slow interaction, errors and CSP violations
are captured, and missions persist across a reload. The suite is 46 tests,
and it passed 225 out of 225 runs with --repeat-each=5 (the HTTPS-only check is skipped on HTTP/1.1).
Extend it
Each extension below exercises a different Part of the guide:
- A Service Worker station (§40): register a worker, show the cache filling, take the lab offline in DevTools, and watch it still load.
- A WebGPU station (§57): run the vector-add shader against a CPU loop and chart the crossover point where the GPU wins.
- An HTTP/3 lane (§26): put the lab behind a QUIC-capable proxy
(Caddy, or a CDN) and add
h3to the waterfall comparison. - A memory-leak hunt mission: make the lab leak in three hidden ways (a listener, a closure, a timer) and have the user find each with heap snapshots.
- A field-data panel: send the vitals with
navigator.sendBeaconto a/rumendpoint on the server and chart p75 across visits. - Your own station: pick the concept you understood last, build the station that would have taught it to you, then write the Playwright test that proves it works.
Where to go from here: a path to expert
You've now built and tested, rather than just read about, every layer between a keypress and a pixel: language, engine, event loop, network, rendering, threads, storage, security, AI integration and testing. Expert JavaScript engineers keep going in three directions:
- Depth: read the specs when docs disagree. ECMA-262 for the language, the WHATWG HTML spec's "event loops" section for scheduling, and Fetch for networking. Read V8's blog (v8.dev) and engine release notes.
- Breadth across runtimes: Node, Deno, Bun and edge isolates (§62) share V8/JSC and Web APIs but differ in I/O, permissions and module loading. Port Browser Lab's server to each.
- Teaching: build the station, write the test, explain the bug you hit. This guide's war stories came from exactly that process. Every one of them was found by a test that someone bothered to write.