1import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright";
2import { expect, test } from "@playwright/test";
3
4const FIXTURE_VALUE = "hello-from-astro-ssr-binding";
5
6// This suite exercises the user-config wave's behavior
7// (packages/frontend-frameworks/fixtures/astro-ssr/astro.config.mjs honored by the integration).
8for (const mode of Playwright.SERVER_METHODS) {
9 test.describe(mode, () => {
10 const it = Playwright.make(mode);
11
12 it("renders the SSR page per request: binding + fresh request id", 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
21 // Per-request rendering: two requests must carry different
22 // middleware-stamped request ids.
23 const firstId = await page.locator("#request-id").textContent();
24 expect(firstId).toBeTruthy();
25 expect(firstId).not.toBe("missing");
26 await page.reload();
27 const secondId = await page.locator("#request-id").textContent();
28 expect(secondId).toBeTruthy();
29 expect(secondId).not.toBe(firstId);
30 });
31
32 it("renders dynamic param routes on demand", async ({ server }) => {
33 const first = await server.fetch("/greet/world");
34 expect(first.status).toBe(200);
35 expect(await first.text()).toContain("Hello, world!");
36
37 // No getStaticPaths: any param renders — the defining SSR behavior.
38 const second = await server.fetch("/greet/another-visitor");
39 expect(second.status).toBe(200);
40 expect(await second.text()).toContain("Hello, another-visitor!");
41 });
42
43 it("handles a form POST server-side (browser submission)", async ({
44 page,
45 server,
46 }) => {
47 await page.goto(new URL("/feedback", server.url).toString());
48 await expect(page.locator("#feedback-result")).toHaveCount(0);
49 await page.locator("#message").fill("hello from playwright");
50 await page.locator("#submit").click();
51 await expect(page.locator("#feedback-result")).toHaveText(
52 "received: hello from playwright",
53 );
54 });
55
56 it("handles a direct form POST (checkOrigin: false from the user config)", async ({
57 server,
58 }) => {
59 // Plain HTTP against the listening URL in both modes — the live
60 // server's dispatchFetch helper does not forward request init. No
61 // Origin header is sent: this passing proves the user config's
62 // `security.checkOrigin: false` is honored (the default would 403).
63 const response = await fetch(new URL("/feedback", server.url), {
64 method: "POST",
65 headers: { "content-type": "application/x-www-form-urlencoded" },
66 body: new URLSearchParams({ message: "direct-post" }).toString(),
67 });
68 expect(response.status).toBe(200);
69 expect(await response.text()).toContain("received: direct-post");
70 });
71
72 it("round-trips Astro.session across requests (zero-config sessions)", async ({
73 server,
74 }) => {
75 const first = await fetch(new URL("/session", server.url));
76 expect(first.status).toBe(200);
77 expect(await first.text()).toContain('<p id="count">1</p>');
78 const setCookie = first.headers.get("set-cookie");
79 expect(setCookie).toBeTruthy();
80 const cookie = setCookie!.split(";")[0]!;
81 const second = await fetch(new URL("/session", server.url), {
82 headers: { cookie },
83 });
84 expect(second.status).toBe(200);
85 expect(await second.text()).toContain('<p id="count">2</p>');
86 });
87
88 it("renders the streaming page (early + late chunks)", async ({
89 server,
90 }) => {
91 const response = await server.fetch("/stream");
92 expect(response.status).toBe(200);
93 const html = await response.text();
94 const early = html.indexOf('id="early"');
95 const late = html.indexOf('id="late"');
96 expect(early).toBeGreaterThan(-1);
97 expect(late).toBeGreaterThan(early);
98 });
99
100 it("serves the single prerendered exception (hybrid)", async ({
101 server,
102 }) => {
103 const response = await server.fetch("/about/");
104 expect(response.status).toBe(200);
105 const html = await response.text();
106 expect(html).toContain('id="mode"');
107 expect(html).toContain("prerendered");
108 });
109
110 it("reads the binding from the JSON API route", async ({ server }) => {
111 const json = await server.fetchJson<{
112 value: string | null;
113 hasAssetsBinding: boolean;
114 }>("/api/hello");
115 expect(json).toMatchObject({
116 value: FIXTURE_VALUE,
117 hasAssetsBinding: true,
118 });
119 });
120
121 it("echoes a JSON POST body through the API route", async ({ server }) => {
122 const response = await fetch(new URL("/api/hello", server.url), {
123 method: "POST",
124 headers: { "content-type": "application/json" },
125 body: JSON.stringify({ ping: "pong" }),
126 });
127 expect(response.status).toBe(200);
128 expect(await response.json()).toEqual({
129 echoed: { ping: "pong" },
130 method: "POST",
131 });
132 });
133
134 it("returns binary data from an endpoint", async ({ server }) => {
135 const response = await server.fetch("/api/binary");
136 expect(response.status).toBe(200);
137 expect(response.headers.get("content-type")).toBe(
138 "application/octet-stream",
139 );
140 const bytes = new Uint8Array(await response.arrayBuffer());
141 expect([...bytes]).toEqual([
142 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x04, 0x05, 0x06, 0x07,
143 ]);
144 expect(response.headers.get("x-binary-length")).toBe(
145 String(bytes.length),
146 );
147 });
148
149 it("runs middleware on on-demand routes: locals + response headers", async ({
150 server,
151 }) => {
152 const response = await server.fetch("/greet/middleware-check");
153 expect(response.status).toBe(200);
154 expect(response.headers.get("x-middleware")).toBe("hit");
155 const requestId = response.headers.get("x-request-id");
156 expect(requestId).toBeTruthy();
157 // The header mirrors the locals value rendered into the page body.
158 expect(await response.text()).toContain(requestId!);
159 });
160
161 it("serves the redirect declared in astro.config.mjs", async ({
162 server,
163 }) => {
164 // The redirect target (/greet/astro) is an on-demand route, so the
165 // redirect must be handled dynamically by the worker in BOTH modes —
166 // there is no prerendered target for the asset layer to serve.
167 const response = await fetch(new URL("/legacy-greeting", server.url), {
168 redirect: "manual",
169 });
170 expect([301, 302, 308]).toContain(response.status);
171 expect(response.headers.get("location")).toContain("/greet/astro");
172
173 const followed = await fetch(new URL("/legacy-greeting", server.url));
174 expect(followed.status).toBe(200);
175 expect(await followed.text()).toContain("Hello, astro!");
176 });
177
178 it("serves public assets", async ({ server }) => {
179 const response = await server.fetch("/robots.txt");
180 expect(response.status).toBe(200);
181 expect(await response.text()).toContain("User-agent");
182 });
183
184 it("returns 404 for unmatched routes", async ({ server }) => {
185 const response = await server.fetch("/definitely-not-a-route");
186 expect(response.status).toBe(404);
187 await response.arrayBuffer();
188 });
189 });
190}