···1313## Running Tests
14141515The tests will automatically:
1616+16171. Build the prefetch.js bundle (via `cargo xtask build-maudit-js`)
17182. Start the Maudit dev server on the test fixture site
18193. Run the tests
···4647## Features Tested
47484849### Basic Prefetch
5050+4951- Creating link elements with `rel="prefetch"`
5052- Preventing duplicate prefetches
5153- Skipping current page prefetch
5254- Blocking cross-origin prefetches
53555456### Prerendering (Chromium only)
5757+5558- Creating `<script type="speculationrules">` elements
5659- Different eagerness levels (immediate, eager, moderate, conservative)
5760- Fallback to link prefetch on non-Chromium browsers
···11+import { expect } from "@playwright/test";
22+import { createTestWithFixture } from "./test-utils";
33+import { readFileSync, writeFileSync } from "node:fs";
44+import { resolve, dirname } from "node:path";
55+import { fileURLToPath } from "node:url";
66+77+const __filename = fileURLToPath(import.meta.url);
88+const __dirname = dirname(__filename);
99+1010+// Create test instance with hot-reload fixture
1111+const test = createTestWithFixture("hot-reload");
1212+1313+test.describe.configure({ mode: "serial" });
1414+1515+test.describe("Hot Reload", () => {
1616+ const fixturePath = resolve(__dirname, "..", "fixtures", "hot-reload");
1717+ const indexPath = resolve(fixturePath, "src", "pages", "index.rs");
1818+ let originalContent: string;
1919+2020+ test.beforeAll(async () => {
2121+ // Save original content
2222+ originalContent = readFileSync(indexPath, "utf-8");
2323+ });
2424+2525+ test.afterEach(async () => {
2626+ // Restore original content after each test
2727+ writeFileSync(indexPath, originalContent, "utf-8");
2828+ // Wait a bit for the rebuild
2929+ await new Promise((resolve) => setTimeout(resolve, 2000));
3030+ });
3131+3232+ test.afterAll(async () => {
3333+ // Restore original content
3434+ writeFileSync(indexPath, originalContent, "utf-8");
3535+ });
3636+3737+ test("should show updated content after file changes", async ({ page, devServer }) => {
3838+ await page.goto(devServer.url);
3939+4040+ // Verify initial content
4141+ await expect(page.locator("#title")).toHaveText("Original Title");
4242+4343+ // Prepare to wait for actual reload by waiting for the same URL to reload
4444+ const currentUrl = page.url();
4545+4646+ // Modify the file
4747+ const modifiedContent = originalContent.replace(
4848+ 'h1 id="title" { "Original Title" }',
4949+ 'h1 id="title" { "Another Update" }',
5050+ );
5151+ writeFileSync(indexPath, modifiedContent, "utf-8");
5252+5353+ // Wait for the page to actually reload on the same URL
5454+ await page.waitForURL(currentUrl, { timeout: 15000 });
5555+ // Verify the updated content
5656+ await expect(page.locator("#title")).toHaveText("Another Update", { timeout: 15000 });
5757+ });
5858+});
+3-1
e2e/tests/prefetch.spec.ts
···11-import { test, expect } from "./test-utils";
11+import { createTestWithFixture, expect } from "./test-utils";
22import { prefetchScript } from "./utils";
33+44+const test = createTestWithFixture("prefetch-prerender");
3546test.describe("Prefetch", () => {
57 test("should create prefetch via speculation rules on Chromium or link element elsewhere", async ({
+3-1
e2e/tests/prerender.spec.ts
···11-import { test, expect } from "./test-utils";
11+import { createTestWithFixture, expect } from "./test-utils";
22import { prefetchScript } from "./utils";
33+44+const test = createTestWithFixture("prefetch-prerender");
3546test.describe("Prefetch - Speculation Rules (Prerender)", () => {
57 test("should create speculation rules on Chromium or link prefetch elsewhere when prerender is enabled", async ({
+44-23
e2e/tests/test-utils.ts
···11-import { spawn, execFile, type ChildProcess } from "node:child_process";
22-import { join, resolve, dirname } from "node:path";
11+import { spawn } from "node:child_process";
22+import { resolve, dirname } from "node:path";
33import { existsSync } from "node:fs";
44import { fileURLToPath } from "node:url";
55import { test as base } from "@playwright/test";
···136136}
137137138138// Worker-scoped server pool - one server per worker, shared across all tests in that worker
139139-const workerServers = new Map<number, DevServer>();
139139+// Key format: "workerIndex-fixtureName"
140140+const workerServers = new Map<string, DevServer>();
140141141141-// Extend Playwright's test with a devServer fixture
142142-export const test = base.extend<{ devServer: DevServer }>({
143143- devServer: async ({}, use, testInfo) => {
144144- // Use worker index to get or create a server for this worker
145145- const workerIndex = testInfo.workerIndex;
142142+/**
143143+ * Create a test instance with a devServer fixture for a specific fixture.
144144+ * This allows each test file to use a different fixture while sharing the same pattern.
145145+ *
146146+ * @param fixtureName - Name of the fixture directory under e2e/fixtures/
147147+ * @param basePort - Starting port number (default: 1864). Each worker gets basePort + workerIndex
148148+ *
149149+ * @example
150150+ * ```ts
151151+ * import { createTestWithFixture } from "./test-utils";
152152+ * const test = createTestWithFixture("my-fixture");
153153+ *
154154+ * test("my test", async ({ devServer }) => {
155155+ * // devServer is automatically started for "my-fixture"
156156+ * });
157157+ * ```
158158+ */
159159+export function createTestWithFixture(fixtureName: string, basePort = 1864) {
160160+ return base.extend<{ devServer: DevServer }>({
161161+ // oxlint-disable-next-line no-empty-pattern
162162+ devServer: async ({}, use, testInfo) => {
163163+ // Use worker index to get or create a server for this worker
164164+ const workerIndex = testInfo.workerIndex;
165165+ const serverKey = `${workerIndex}-${fixtureName}`;
146166147147- let server = workerServers.get(workerIndex);
167167+ let server = workerServers.get(serverKey);
148168149149- if (!server) {
150150- // Assign unique port based on worker index
151151- const port = 1864 + workerIndex;
169169+ if (!server) {
170170+ // Assign unique port based on worker index
171171+ const port = basePort + workerIndex;
152172153153- server = await startDevServer({
154154- fixture: "prefetch-prerender",
155155- port,
156156- });
173173+ server = await startDevServer({
174174+ fixture: fixtureName,
175175+ port,
176176+ });
157177158158- workerServers.set(workerIndex, server);
159159- }
178178+ workerServers.set(serverKey, server);
179179+ }
160180161161- await use(server);
181181+ await use(server);
162182163163- // Don't stop the server here - it stays alive for all tests in this worker
164164- // Playwright will clean up when the worker exits
165165- },
166166-});
183183+ // Don't stop the server here - it stays alive for all tests in this worker
184184+ // Playwright will clean up when the worker exits
185185+ },
186186+ });
187187+}
167188168189export { expect } from "@playwright/test";
+1-1
e2e/tests/utils.ts
···44// Find the actual prefetch bundle file (hash changes on each build)
55const distDir = join(process.cwd(), "../crates/maudit/js/dist");
66const prefetchFile = readdirSync(distDir).find(
77- (f) => f.startsWith("prefetch-") && f.endsWith(".js"),
77+ (f) => f.startsWith("prefetch") && f.endsWith(".js"),
88);
99if (!prefetchFile) throw new Error("Could not find prefetch bundle");
1010
···214214215215```markdown
216216---
217217-title: {{ enhance title="Super Title" /}}
217217+title: { { enhance title="Super Title" / } }
218218---
219219220220Here's an image with a caption:
+1-1
website/content/docs/images.md
···9696impl Route for ImagePage {
9797 fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
9898 let image = ctx.assets.add_image("path/to/image.jpg")?;
9999- let placeholder = image.placeholder();
9999+ let placeholder = image.placeholder()?;
100100101101 Ok(format!("<img src=\"{}\" alt=\"Image with placeholder\" style=\"background-image: url('{}'); background-size: cover;\" />", image.url(), placeholder.data_uri()))
102102 }
+1-1
website/content/docs/prefetching.md
···49495050Note that prerendering, unlike prefetching, may require rethinking how the JavaScript on your pages works, as it'll run JavaScript from pages that the user hasn't visited yet. For example, this might result in analytics reporting incorrect page views.
51515252-## Possible risks
5252+## Possible risks
53535454Prefetching pages in static websites is typically always safe. In more traditional apps, an issue can arise if your pages cause side effects to happen on the server. For instance, if you were to prefetch `/logout`, your user might get disconnected on hover, or worse as soon as the log out link appear in the viewport. In modern times, it is typically not recommended to have links cause such side effects anyway, reducing the risk of this happening.
5555
+2-2
website/content/news/2026-in-the-cursed-lands.md
···5555impl Route for ImagePage {
5656 fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
5757 let image = ctx.assets.add_image("path/to/image.jpg")?;
5858- let placeholder = image.placeholder();
5858+ let placeholder = image.placeholder()?;
59596060 Ok(format!("<img src=\"{}\" alt=\"Image with placeholder\" style=\"background-image: url('{}'); background-size: cover;\" />", image.url(), placeholder.data_uri()))
6161 }
···70707171### Shortcodes
72727373-Embedding a YouTube video typically means copying a long, ugly iframe tag and configuring several attributes to ensure proper rendering. It'd be nice to have something friendlier, a code that would be short, you will.
7373+Embedding a YouTube video typically means copying a long, ugly iframe tag and configuring several attributes to ensure proper rendering. It'd be nice to have something friendlier, a code that would be short, if you will.
74747575```md
7676Here's my cool video:
+1-1
xtask/Cargo.toml
···55publish = false
6677[dependencies]
88-rolldown = { package = "brk_rolldown", version = "0.2.3" }
88+rolldown = { package = "brk_rolldown", version = "0.8.0" }
99tokio = { version = "1", features = ["rt"] }