1import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright";
2import { expect, test } from "@playwright/test";
3
4for (const method of Playwright.SERVER_METHODS) {
5 test.describe(method, () => {
6 const it = Playwright.make(method);
7
8 it("server-renders the home page markup (RSC -> SSR)", async ({
9 server,
10 }) => {
11 // Raw fetch, no JS: the worker runs the `rsc` environment and renders
12 // HTML through the `ssr` environment — the server-component copy must
13 // already be in the payload.
14 const response = await server.fetch("/");
15 expect(response.status).toBe(200);
16 const html = await response.text();
17 expect(html).toContain("<h1>Home</h1>");
18 expect(html).toContain("rendered as a React Server Component");
19 });
20
21 it("renders the homepage and hydrates client routes", async ({
22 page,
23 server,
24 }) => {
25 const response = await page.goto(server.url.toString());
26 expect(response?.status()).toBe(200);
27 await page.waitForLoadState("networkidle");
28 await page.evaluate(() => document.fonts.ready);
29
30 await expect(page).toHaveScreenshot("index.png", {
31 animations: "disabled",
32 maxDiffPixelRatio: 0.03,
33 });
34
35 await page.click("a[href='/about']");
36 await page.waitForURL("**/about");
37 await page.evaluate(() => document.fonts.ready);
38
39 await expect(page).toHaveScreenshot("about.png", {
40 animations: "disabled",
41 maxDiffPixelRatio: 0.03,
42 });
43
44 expect(await page.textContent("button.counter")).toBe("Count is 0");
45 await page.click("button.counter");
46 expect(await page.textContent("button.counter")).toBe("Count is 1");
47 });
48
49 it("serves a direct navigation to /about and hydrates the client component", async ({
50 page,
51 server,
52 }) => {
53 // Raw fetch: the "use client" about route is still server-rendered
54 // through the rsc/ssr topology.
55 const raw = await server.fetch("/about");
56 expect(raw.status).toBe(200);
57 expect(await raw.text()).toContain("rendered as a client component");
58
59 // Hard browser navigation straight to the route (no client-side
60 // transition from /) — the client component must hydrate.
61 const response = await page.goto(
62 new URL("/about", server.url).toString(),
63 );
64 expect(response?.status()).toBe(200);
65 await expect(page.locator("h1")).toHaveText("About");
66 const counter = page.locator("button.counter");
67 await expect(counter).toHaveText("Count is 0");
68 // A click can land before hydration attaches the handler on a direct
69 // navigation (observed in dev mode) — retry click-and-observe until
70 // the component is interactive.
71 await expect(async () => {
72 await counter.click();
73 await expect(counter).not.toHaveText("Count is 0", { timeout: 250 });
74 }).toPass();
75 await expect(counter).toHaveText(/Count is [1-9]\d*/);
76 });
77
78 it("returns 404 for an unmatched route", async ({ server }) => {
79 // No catch-all route is configured: react-router's default error
80 // boundary renders with a 404 status.
81 const response = await server.fetch("/definitely/not/a/route");
82 expect(response.status).toBe(404);
83 expect(await response.text()).toContain("404 Not Found");
84 });
85 });
86}