1import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright";
2import { expect, test } from "@playwright/test";
3
4for (const mode of Playwright.SERVER_METHODS) {
5 test.describe(mode, () => {
6 const it = Playwright.make(mode);
7
8 it("server-renders the homepage markup", async ({ server }) => {
9 // Raw fetch, no JS: the hero copy must already be in the HTML the
10 // worker streamed (SSR), not injected after hydration.
11 const response = await server.fetch("/");
12 expect(response.status).toBe(200);
13 const html = await response.text();
14 expect(html).toContain("Start simple, ship quickly.");
15 expect(html).toContain("TanStack Start Starter");
16 });
17
18 it("renders the homepage", async ({ page, server }) => {
19 const response = await page.goto(server.url.toString());
20 expect(response?.status()).toBe(200);
21 await page.evaluate(() => document.fonts.ready);
22
23 await expect(page).toHaveScreenshot("index.png", {
24 animations: "disabled",
25 maxDiffPixelRatio: 0.03,
26 });
27
28 await page.click("a[href='/about']");
29 await page.waitForURL("**/about");
30 await page.evaluate(() => document.fonts.ready);
31
32 await expect(page).toHaveScreenshot("about.png", {
33 animations: "disabled",
34 maxDiffPixelRatio: 0.03,
35 });
36 });
37
38 it("hydrates: the theme toggle mutates the document theme", async ({
39 page,
40 server,
41 }) => {
42 await page.goto(server.url.toString());
43 const toggle = page.getByRole("button", { name: /Theme mode/ });
44 // The client effect ran (SSR renders "Auto" and hydration keeps it
45 // until clicked).
46 await expect(toggle).toHaveText("Auto");
47 // In dev the click can land before hydration attaches the handler —
48 // retry, but only click while the toggle still reads "Auto" so a
49 // registered-but-slow click never advances the cycle twice.
50 await expect(async () => {
51 if ((await toggle.textContent()) === "Auto") {
52 await toggle.click();
53 }
54 await expect(toggle).toHaveText("Light", { timeout: 500 });
55 }).toPass({ timeout: 15_000 });
56 await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
57 });
58
59 it("serves a direct navigation to /about (SSR)", async ({
60 page,
61 server,
62 }) => {
63 // Raw fetch: the about copy is server-rendered.
64 const raw = await server.fetch("/about");
65 expect(raw.status).toBe(200);
66 expect(await raw.text()).toContain("A small starter with room to grow.");
67
68 // Hard browser navigation straight to the route (no client-side
69 // transition from /).
70 const response = await page.goto(
71 new URL("/about", server.url).toString(),
72 );
73 expect(response?.status()).toBe(200);
74 await expect(page.locator("h1")).toHaveText(
75 "A small starter with room to grow.",
76 );
77 });
78
79 it("serves static assets", async ({ server }) => {
80 // Assets-first routing (`runWorkerFirst` unset, preview
81 // `invoke_user_worker_ahead_of_assets: false`) — static files are
82 // answered by the asset layer without invoking the worker.
83 const response = await server.fetch("/robots.txt");
84 expect(response.status).toBe(200);
85 expect(await response.text()).toContain("User-agent");
86 });
87
88 it("returns 404 for an unmatched route", async ({ server }) => {
89 const response = await server.fetch("/definitely/not/a/route");
90 expect(response.status).toBe(404);
91 });
92
93 // Gated on the `TEST_POSTGRES_URL` secret (wired in ci.yml + turbo.json's
94 // root `test` env list). CI asserts the secret is present, so this can
95 // only skip in local runs without the secret — never silently on CI.
96 const skipIfNoDatabase = process.env.TEST_POSTGRES_URL ? it : it.skip;
97 skipIfNoDatabase("fetches database", async ({ server }) => {
98 const response =
99 await server.fetchJson<[{ "?column?": number }]>("/api/db");
100 expect(response).toMatchObject([{ "?column?": 1 }]);
101 });
102
103 it("fetches WASM", async ({ server }) => {
104 const response = await server.fetchJson<{ result: number }>("/api/wasm");
105 expect(response).toMatchObject({ result: 3 });
106 });
107 });
108}