1import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright";
2import { expect, test } from "@playwright/test";
3import { readdir, readFile } from "node:fs/promises";
4import * as path from "node:path";
5
6const FIXTURE_VALUE = "hello-from-astro-binding";
7
8for (const mode of Playwright.SERVER_METHODS) {
9 test.describe(mode, () => {
10 const it = Playwright.make(mode);
11
12 it("renders the SSR page with the binding and hydrates the client script", 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("#mode")).toHaveText("on-demand");
19 await expect(page.locator("#binding")).toHaveText(FIXTURE_VALUE);
20 await page.evaluate(() => document.fonts.ready);
21
22 await expect(page).toHaveScreenshot("index.png", {
23 animations: "disabled",
24 maxDiffPixelRatio: 0.03,
25 });
26
27 const button = page.locator("#counter");
28 await expect(button).toHaveText("count is 0");
29 await button.click();
30 await expect(button).toHaveText("count is 1");
31 });
32
33 it("serves the prerendered page", async ({ server }) => {
34 const response = await server.fetch("/about/");
35 expect(response.status).toBe(200);
36 const html = await response.text();
37 expect(html).toContain('id="mode"');
38 expect(html).toContain("prerendered");
39 });
40
41 it("prerenders inside workerd: top-level cloudflare:workers import + binding", async ({
42 server,
43 }) => {
44 // The page is impossible under node prerendering (top-level
45 // `cloudflare:workers` import); the baked binding value proves the
46 // workerd prerenderer ran with the worker's bindings attached.
47 const response = await server.fetch("/prerendered-env/");
48 expect(response.status).toBe(200);
49 const html = await response.text();
50 expect(html).toContain("prerendered-workerd");
51 expect(html).toContain(FIXTURE_VALUE);
52 });
53
54 it("reads the binding from the API route", async ({ server }) => {
55 const json = await server.fetchJson<{
56 value: string | null;
57 hasAssetsBinding: boolean;
58 }>("/api/hello");
59 expect(json).toMatchObject({
60 value: FIXTURE_VALUE,
61 hasAssetsBinding: true,
62 });
63 });
64
65 it("honors astro.config.mjs: user integration route + user vite define", async ({
66 server,
67 }) => {
68 // The /user-integration route only exists because the user's own
69 // astro.config.mjs was loaded natively: a user integration injects it,
70 // and the page renders a value produced by the file's `vite.define`.
71 const response = await server.fetch("/user-integration");
72 expect(response.status).toBe(200);
73 const html = await response.text();
74 expect(html).toContain("injected-by-user-integration");
75 expect(html).toContain("hello-from-user-vite-define");
76 });
77
78 it("runs middleware: locals + response-header mutation", async ({
79 server,
80 }) => {
81 const response = await server.fetch("/locals");
82 expect(response.status).toBe(200);
83 expect(response.headers.get("x-middleware")).toBe("hit");
84 expect(await response.text()).toContain("set-by-middleware");
85 });
86
87 it("serves the configured redirect", async ({ server }) => {
88 if (mode === "dev") {
89 // Dev fetch forwards the init: observe the raw 3xx + location.
90 const response = await server.fetch("/old-about", {
91 redirect: "manual",
92 });
93 expect([301, 302, 308]).toContain(response.status);
94 expect(response.headers.get("location")).toContain("/about");
95 } else {
96 // The live server's fetch (miniflare dispatchFetch) does not forward
97 // the init, so assert the redirect end-to-end: following /old-about
98 // lands on the prerendered about page.
99 const response = await server.fetch("/old-about");
100 expect(response.status).toBe(200);
101 const html = await response.text();
102 expect(html).toContain('id="mode"');
103 expect(html).toContain("prerendered");
104 }
105 });
106
107 it("returns binary data from an endpoint", async ({ server }) => {
108 const response = await server.fetch("/api/binary");
109 expect(response.status).toBe(200);
110 expect(response.headers.get("content-type")).toBe(
111 "application/octet-stream",
112 );
113 const bytes = new Uint8Array(await response.arrayBuffer());
114 expect([...bytes]).toEqual([
115 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x01, 0x02, 0x03,
116 ]);
117 expect(response.headers.get("x-binary-length")).toBe(
118 String(bytes.length),
119 );
120 });
121
122 it("serves the content-collection-driven prerendered page", async ({
123 server,
124 }) => {
125 const response = await server.fetch("/posts/hello-world/");
126 expect(response.status).toBe(200);
127 const html = await response.text();
128 expect(html).toContain('id="post-title"');
129 expect(html).toContain("Hello World");
130 expect(html).toContain("content-collection body");
131 });
132
133 it("round-trips Astro.session across requests (zero-config sessions)", async ({
134 server,
135 }) => {
136 // Plain HTTP against the listening URL in both modes — the live
137 // server's dispatchFetch helper does not forward request init.
138 const first = await fetch(new URL("/session", server.url));
139 expect(first.status).toBe(200);
140 expect(await first.text()).toContain('<p id="count">1</p>');
141 const setCookie = first.headers.get("set-cookie");
142 expect(setCookie).toBeTruthy();
143 const cookie = setCookie!.split(";")[0]!;
144 const second = await fetch(new URL("/session", server.url), {
145 headers: { cookie },
146 });
147 expect(second.status).toBe(200);
148 expect(await second.text()).toContain('<p id="count">2</p>');
149 });
150
151 if (mode === "live") {
152 it("emits _redirects and serves the redirect from the asset layer", async ({
153 server,
154 }) => {
155 const content = await readFile(
156 path.join(process.cwd(), "dist/client/_redirects"),
157 "utf-8",
158 );
159 expect(content).toMatch(/\/old-about\s+\/about\/\s+301/);
160 const response = await fetch(new URL("/old-about", server.url), {
161 redirect: "manual",
162 });
163 expect([301, 302, 308]).toContain(response.status);
164 expect(response.headers.get("location")).toContain("/about");
165 });
166
167 it("emits _headers and serves hashed assets with immutable Cache-Control", async ({
168 server,
169 }) => {
170 const content = await readFile(
171 path.join(process.cwd(), "dist/client/_headers"),
172 "utf-8",
173 );
174 expect(content).toContain("/_astro/*");
175 expect(content).toContain(
176 "Cache-Control: public, max-age=31536000, immutable",
177 );
178 const assets = await readdir(
179 path.join(process.cwd(), "dist/client/_astro"),
180 );
181 expect(assets.length).toBeGreaterThan(0);
182 const response = await fetch(
183 new URL(`/_astro/${assets[0]}`, server.url),
184 );
185 expect(response.status).toBe(200);
186 expect(response.headers.get("cache-control")).toBe(
187 "public, max-age=31536000, immutable",
188 );
189 await response.arrayBuffer();
190 });
191 }
192
193 it("serves public assets", async ({ server }) => {
194 const response = await server.fetch("/robots.txt");
195 expect(response.status).toBe(200);
196 expect(await response.text()).toContain("User-agent");
197 });
198
199 it("falls back through ASSETS to the 404 page", async ({ server }) => {
200 const response = await server.fetch("/definitely-not-a-route");
201 expect(response.status).toBe(404);
202 await response.arrayBuffer();
203 });
204 });
205}