1import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright";
2import { expect, test } from "@playwright/test";
3
4const SECRET = "s3cret-from-binding";
5const UUID_REGEX =
6 /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
7
8for (const mode of Playwright.SERVER_METHODS) {
9 test.describe(mode, () => {
10 const it = Playwright.make(mode);
11
12 it("server-renders the home page with platform.env", async ({
13 page,
14 server,
15 }) => {
16 const response = await page.goto(server.url.toString());
17 expect(response?.status()).toBe(200);
18 await expect(page.locator("#secret")).toHaveText(`secret:${SECRET}`);
19 await expect(page.locator("#ctx")).toHaveText("ctx:yes");
20 await expect(page.locator("#devalued")).toHaveText("devalued:{n:1}");
21 });
22
23 it("hydrates the client-interactive counter", async ({ page, server }) => {
24 await page.goto(new URL("/counter", server.url).toString());
25 await expect(page.locator("#count")).toHaveText("count:0");
26 // wait for hydration before interacting (the effect flips the marker)
27 await expect(page.locator("#increment")).toHaveAttribute(
28 "data-hydrated",
29 "true",
30 );
31 await page.click("#increment");
32 await expect(page.locator("#count")).toHaveText("count:1");
33 await page.click("#increment");
34 await expect(page.locator("#count")).toHaveText("count:2");
35 });
36
37 it("serves the prerendered page", async ({ server }) => {
38 const response = await server.fetch("/prerendered");
39 expect(response.status).toBe(200);
40 expect(await response.text()).toContain("this-page-is-prerendered");
41 });
42
43 it("serves static assets", async ({ server }) => {
44 const response = await server.fetch("/robots.txt");
45 expect(response.status).toBe(200);
46 expect(await response.text()).toContain("User-agent: *");
47 });
48
49 it("runs the server endpoint (cookie, uuid, node:crypto, platform.env)", async ({
50 server,
51 }) => {
52 const body = await server.fetchJson<{
53 uuid: string;
54 nodeUuid: string;
55 cookie: string;
56 secret: string;
57 }>("/api/hello");
58 expect(body.cookie).toBe("fixture=ok");
59 expect(body.secret).toBe(SECRET);
60 expect(body.uuid).toMatch(UUID_REGEX);
61 expect(body.nodeUuid).toMatch(UUID_REGEX);
62 });
63
64 it("honors the user vite.config.ts (kit alias, user plugin, adapter override)", async ({
65 server,
66 }) => {
67 // `$fixture` (a user kit alias) and `virtual:fixture-marker` (a user
68 // Vite plugin) both live in the fixture's own vite.config.ts — this
69 // endpoint only works when that file is loaded natively. The config
70 // also declares a user adapter that THROWS from `adapt()`, so live
71 // builds succeeding at all proves the deploy target's adapter replaced
72 // it.
73 const body = await server.fetchJson<{ marker: string; greeting: string }>(
74 "/api/user-config",
75 );
76 expect(body.marker).toBe("user-vite-plugin-active");
77 expect(body.greeting).toBe("greeting-via-user-alias");
78 });
79
80 it("runs a form action via progressive enhancement (no reload)", async ({
81 page,
82 server,
83 }) => {
84 await page.goto(new URL("/form", server.url).toString());
85 await expect(page.locator("#form-result")).toHaveText("result:none");
86 // wait for hydration, then plant a marker a full-page navigation would lose
87 await expect(page.locator("#submit")).toHaveAttribute(
88 "data-hydrated",
89 "true",
90 );
91 await page.evaluate(() => {
92 (window as { __enhanced?: boolean }).__enhanced = true;
93 });
94 await page.fill("#name", "playwright");
95 await page.click("#submit");
96 await expect(page.locator("#form-result")).toHaveText(
97 "result:hello playwright",
98 );
99 await expect(page.locator("#form-secret")).toHaveText(`secret:${SECRET}`);
100 const marker = await page.evaluate(
101 () => (window as { __enhanced?: boolean }).__enhanced,
102 );
103 expect(marker).toBe(true);
104 });
105
106 it("runs the form action without JavaScript (plain POST re-render)", async ({
107 request,
108 server,
109 }) => {
110 // a real HTTP POST (no client-side kit runtime involved), as a
111 // JavaScript-less browser would submit it
112 const response = await request.post(
113 new URL("/form?/greet", server.url).toString(),
114 {
115 form: { name: "curl" },
116 headers: {
117 // kit's CSRF check requires a same-origin `origin` header on form
118 // posts; `accept: text/html` selects the no-JS HTML re-render (kit
119 // answers JSON for enhanced submissions)
120 origin: server.url.origin,
121 accept: "text/html",
122 },
123 },
124 );
125 expect(response.status()).toBe(200);
126 expect(await response.text()).toContain("hello curl");
127 });
128
129 it("round-trips cookies through the +page.server load", async ({
130 page,
131 server,
132 }) => {
133 await page.goto(new URL("/cookies", server.url).toString());
134 await expect(page.locator("#visits")).toHaveText("visits:1");
135 await page.reload();
136 await expect(page.locator("#visits")).toHaveText("visits:2");
137 await page.reload();
138 await expect(page.locator("#visits")).toHaveText("visits:3");
139 });
140
141 it("serves a binary endpoint response intact", async ({ server }) => {
142 const response = await server.fetch("/api/binary");
143 expect(response.status).toBe(200);
144 expect(response.headers.get("content-type")).toBe(
145 "application/octet-stream",
146 );
147 expect(response.headers.get("x-binary-length")).toBe("256");
148 const bytes = new Uint8Array(await response.arrayBuffer());
149 expect(bytes.byteLength).toBe(256);
150 expect(bytes[0]).toBe(0);
151 expect(bytes[128]).toBe(128);
152 expect(bytes[255]).toBe(255);
153 });
154
155 it("serves a route-group page with layout data (group not in URL)", async ({
156 page,
157 server,
158 }) => {
159 const response = await page.goto(
160 new URL("/about", server.url).toString(),
161 );
162 expect(response?.status()).toBe(200);
163 await expect(page.locator("#layout-section")).toHaveText(
164 "section:marketing",
165 );
166 await expect(page.locator("#layout-secret")).toHaveText(
167 `layout-secret:${SECRET}`,
168 );
169 await expect(page.locator("#about-marker")).toHaveText(
170 "about-inside-group",
171 );
172 // the group directory name itself is not routable
173 const literal = await server.fetch("/(marketing)/about");
174 expect(literal.status).toBe(404);
175 });
176
177 it("round-trips platform.caches (put on miss, hit on the second request)", async ({
178 server,
179 }) => {
180 // a fresh key per invocation so retries never see a warm cache
181 const key = crypto.randomUUID();
182 type CacheProbe = { supported: boolean; cached: boolean; key: string };
183 const first = await server.fetchJson<CacheProbe>(`/api/cache?key=${key}`);
184 expect(first.supported).toBe(true);
185 expect(first.cached).toBe(false);
186 expect(first.key).toBe(key);
187 const second = await server.fetchJson<CacheProbe>(
188 `/api/cache?key=${key}`,
189 );
190 expect(second.supported).toBe(true);
191 // live runs on workerd with the real Cache API; dev serves `caches`
192 // through cloudflare-runtime's platform proxy, which round-trips too
193 // (unlike wrangler's no-op dev cache).
194 expect(second.cached).toBe(true);
195 });
196
197 it("serves real bindings, env overrides, and cf through platform", async ({
198 page,
199 server,
200 }) => {
201 const response = await page.goto(
202 new URL("/platform", server.url).toString(),
203 );
204 expect(response?.status()).toBe(200);
205 // FIXTURE_KV is a real KV namespace binding — in dev the put/get pair
206 // round-trips through the platform proxy's workerd instance
207 await expect(page.locator("#kv")).toHaveText("kv:kv-round-trip");
208 // FIXTURE_OVERRIDE: the dev `env` literal wins over the proxied Text
209 // binding; live serves the binding value
210 await expect(page.locator("#override")).toHaveText(
211 mode === "dev" ? "override:literal-override" : "override:proxied-value",
212 );
213 // `cf` is populated in both modes (wrangler-parity mock in dev)
214 await expect(page.locator("#cf")).toHaveText("cf-colo:present");
215 });
216 });
217}