--- description: Writing HMR/Dev Server tests globs: test/bake/* --- # Writing HMR/Dev Server tests Dev server tests validate that hot-reloading is robust, correct, and reliable. Remember to write thorough, yet concise tests. ## File Structure - `test/bake/bake-harness.ts` - shared utilities and test harness - primary test functions `devTest` / `prodTest` / `devAndProductionTest` - class `Dev` (controls subprocess for dev server) - class `Client` (controls a happy-dom subprocess for having the page open) - more helpers - `test/bake/client-fixture.mjs` - subprocess for what `Client` controls. it loads a page and uses IPC to query parts of the page, run javascript, and much more. - `test/bake/dev/*.test.ts` - these call `devTest` to test dev server and hot reloading - `test/bake/dev-and-prod.ts` - these use `devAndProductionTest` to run the same test on dev and production mode. these tests cannot really test hot reloading for obvious reasons. ## Categories bundle.test.ts - Bundle tests are tests concerning bundling bugs that only occur in DevServer. css.test.ts - CSS tests concern bundling bugs with CSS files plugins.test.ts - Plugin tests concern plugins in development mode. ecosystem.test.ts - These tests involve ensuring certain libraries are correct. It is preferred to test more concrete bugs than testing entire packages. esm.test.ts - ESM tests are about various esm features in development mode. html.test.ts - HTML tests are tests relating to HTML files themselves. react-spa.test.ts - Tests relating to React, our react-refresh transform, and basic server component transforms. sourcemap.test.ts - Tests verifying source-maps are correct. ## `devTest` Basics A test takes in two primary inputs: `files` and `async test(dev) {` ```ts import { devTest, emptyHtmlFile } from "../bake-harness"; devTest("html file is watched", { files: { "index.html": emptyHtmlFile({ scripts: ["/script.ts"], body: "

Hello

", }), "script.ts": ` console.log("hello"); `, }, async test(dev) { await dev.fetch("/").expect.toInclude("

Hello

"); await dev.fetch("/").expect.toInclude("

Hello

"); await dev.patch("index.html", { find: "Hello", replace: "World", }); await dev.fetch("/").expect.toInclude("

World

"); // Works await using c = await dev.client("/"); await c.expectMessage("hello"); // Editing HTML reloads await c.expectReload(async () => { await dev.patch("index.html", { find: "World", replace: "Hello", }); await dev.fetch("/").expect.toInclude("

Hello

"); }); await c.expectMessage("hello"); await c.expectReload(async () => { await dev.patch("index.html", { find: "Hello", replace: "Bar", }); await dev.fetch("/").expect.toInclude("

Bar

"); }); await c.expectMessage("hello"); await c.expectReload(async () => { await dev.patch("script.ts", { find: "hello", replace: "world", }); }); await c.expectMessage("world"); }, }); ``` `files` holds the initial state, and the callback runs with the server running. `dev.fetch()` runs HTTP requests, while `dev.client()` opens a browser instance to the code. Functions `dev.write` and `dev.patch` and `dev.delete` mutate the filesystem. Do not use `node:fs` APIs, as the dev server ones are hooked to wait for hot-reload, and all connected clients to receive changes. When a change performs a hard-reload, that must be explicitly annotated with `expectReload`. This tells `client-fixture.mjs` that the test is meant to reload the page once; All other hard reloads automatically fail the test. Client's have `console.log` instrumented, so that any unasserted logs fail the test. This makes it more obvious when an extra reload or re-evaluation. Messages are awaited via `c.expectMessage("log")` or with multiple arguments if there are multiple logs. ## Testing for bundling errors By default, a client opening a page to an error will fail the test. This makes testing errors explicit. ```ts devTest("import then create", { files: { "index.html": ` `, "script.ts": ` import data from "./data"; console.log(data); `, }, async test(dev) { const c = await dev.client("/", { errors: ['script.ts:1:18: error: Could not resolve: "./data"'], }); await c.expectReload(async () => { await dev.write("data.ts", "export default 'data';"); }); await c.expectMessage("data"); }, }); ``` Many functions take an options value to allow specifying it will produce errors. For example, this delete is going to cause a resolution failure. ```ts await dev.delete("other.ts", { errors: ['index.ts:1:16: error: Could not resolve: "./other"'], }); ```