mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
140 lines
4.9 KiB
Plaintext
140 lines
4.9 KiB
Plaintext
---
|
|
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: "<h1>Hello</h1>",
|
|
}),
|
|
"script.ts": `
|
|
console.log("hello");
|
|
`,
|
|
},
|
|
async test(dev) {
|
|
await dev.fetch("/").expect.toInclude("<h1>Hello</h1>");
|
|
await dev.fetch("/").expect.toInclude("<h1>Hello</h1>");
|
|
await dev.patch("index.html", {
|
|
find: "Hello",
|
|
replace: "World",
|
|
});
|
|
await dev.fetch("/").expect.toInclude("<h1>World</h1>");
|
|
|
|
// 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("<h1>Hello</h1>");
|
|
});
|
|
await c.expectMessage("hello");
|
|
|
|
await c.expectReload(async () => {
|
|
await dev.patch("index.html", {
|
|
find: "Hello",
|
|
replace: "Bar",
|
|
});
|
|
await dev.fetch("/").expect.toInclude("<h1>Bar</h1>");
|
|
});
|
|
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 recieve 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": `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head></head>
|
|
<body>
|
|
<script type="module" src="/script.ts"></script>
|
|
</body>
|
|
</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"'],
|
|
});
|
|
```
|