···11+use maud::html;
22+use maudit::route::prelude::*;
33+44+#[route("/")]
55+pub struct Index;
66+77+impl Route for Index {
88+ fn render(&self, _ctx: &mut PageContext) -> impl Into<RenderResult> {
99+ Ok(html! {
1010+ html {
1111+ head {
1212+ title { "Hot Reload Test" }
1313+ }
1414+ body {
1515+ h1 id="title" { "Original Title" }
1616+ div id="content" {
1717+ p id="message" { "Original message" }
1818+ ul id="list" {
1919+ li { "Item 1" }
2020+ li { "Item 2" }
2121+ }
2222+ }
2323+ footer {
2424+ p { "Footer content" }
2525+ }
2626+ }
2727+ }
2828+ })
2929+ }
3030+}
+58
e2e/tests/hot-reload.spec.ts
···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 ({
+42-21
e2e/tests/test-utils.ts
···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";