| 1 | import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright"; |
| 2 | import { expect, test } from "@playwright/test"; |
| 3 | |
| 4 | for (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 | } |