| 1 | import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright"; |
| 2 | import { expect, test } from "@playwright/test"; |
| 3 | import { readFile } from "node:fs/promises"; |
| 4 | |
| 5 | // The static-output path: a pure `output: "static"` Astro site deploys |
| 6 | // ASSETS-ONLY (no worker, `BuildOutput.serverModules` undefined/empty). |
| 7 | // |
| 8 | // The live-only assertions live INSIDE the loop's `live` describe (not a |
| 9 | // separate describe block): each describe can land on its own Playwright |
| 10 | // worker, and the worker-scoped `server` fixture builds when |
| 11 | // `dist/build.json` is missing — two describes owning `live` servers means |
| 12 | // two workers racing the same astro build in the same dist/ on a fresh |
| 13 | // checkout (astro's prerender cleanup in one build deletes chunks the |
| 14 | // other's manifest injection still reads). |
| 15 | for (const mode of Playwright.SERVER_METHODS) { |
| 16 | test.describe(mode, () => { |
| 17 | const it = Playwright.make(mode); |
| 18 | |
| 19 | it("serves the prerendered home page", async ({ page, server }) => { |
| 20 | const response = await page.goto(server.url.toString()); |
| 21 | expect(response?.status()).toBe(200); |
| 22 | await expect(page.locator("#title")).toHaveText("Astro Static Fixture"); |
| 23 | await expect(page.locator("#mode")).toHaveText("static"); |
| 24 | }); |
| 25 | |
| 26 | it("navigates the whole site through the nav (sitemap-ish)", async ({ |
| 27 | page, |
| 28 | server, |
| 29 | }) => { |
| 30 | await page.goto(server.url.toString()); |
| 31 | await page.locator("#nav-about").click(); |
| 32 | await expect(page.locator("#title")).toHaveText("About"); |
| 33 | await page.locator("#nav-blog").click(); |
| 34 | await expect(page.locator("#title")).toHaveText("Blog"); |
| 35 | await expect(page.locator("#post-list li")).toHaveCount(2); |
| 36 | await page.locator("#nav-first-post").click(); |
| 37 | await expect(page.locator("#slug")).toHaveText("first-post"); |
| 38 | await page.locator("#nav-second-post").click(); |
| 39 | await expect(page.locator("#slug")).toHaveText("second-post"); |
| 40 | await page.locator("#nav-home").click(); |
| 41 | await expect(page.locator("#title")).toHaveText("Astro Static Fixture"); |
| 42 | }); |
| 43 | |
| 44 | it("serves getStaticPaths-enumerated routes", async ({ server }) => { |
| 45 | const first = await server.fetch("/blog/first-post/"); |
| 46 | expect(first.status).toBe(200); |
| 47 | expect(await first.text()).toContain("First post"); |
| 48 | |
| 49 | const second = await server.fetch("/blog/second-post/"); |
| 50 | expect(second.status).toBe(200); |
| 51 | expect(await second.text()).toContain("Second post"); |
| 52 | }); |
| 53 | |
| 54 | it("hydrates the client-side island", async ({ page, server }) => { |
| 55 | await page.goto(new URL("/counter/", server.url).toString()); |
| 56 | // The prerendered HTML says "no"; the bundled client script flips it. |
| 57 | await expect(page.locator("#hydrated")).toHaveText("yes"); |
| 58 | await expect(page.locator("#count")).toHaveText("0"); |
| 59 | await page.locator("#increment").click(); |
| 60 | await page.locator("#increment").click(); |
| 61 | await expect(page.locator("#count")).toHaveText("2"); |
| 62 | }); |
| 63 | |
| 64 | it("serves public assets", async ({ server }) => { |
| 65 | const response = await server.fetch("/robots.txt"); |
| 66 | expect(response.status).toBe(200); |
| 67 | expect(await response.text()).toContain("User-agent"); |
| 68 | }); |
| 69 | |
| 70 | it("returns the custom 404 page for unmatched routes", async ({ |
| 71 | server, |
| 72 | }) => { |
| 73 | const response = await server.fetch("/definitely-not-a-route"); |
| 74 | expect(response.status).toBe(404); |
| 75 | expect(await response.text()).toContain("Page not found"); |
| 76 | }); |
| 77 | |
| 78 | // Live-only assertions: the production artifact itself. |
| 79 | if (mode === "live") { |
| 80 | it("serves build-frozen HTML (no per-request rendering)", async ({ |
| 81 | server, |
| 82 | }) => { |
| 83 | // `#built-at` is stamped at BUILD time; two requests must be |
| 84 | // byte-identical. (Dev renders on demand by design, so live-only.) |
| 85 | const first = await (await server.fetch("/")).text(); |
| 86 | const second = await (await server.fetch("/")).text(); |
| 87 | expect(first).toBe(second); |
| 88 | expect(first).toContain('id="built-at"'); |
| 89 | }); |
| 90 | |
| 91 | // THE POINT of this fixture: a fully-static build must be assets-only. |
| 92 | it("build output contains NO server modules (assets-only)", async ({ |
| 93 | server, |
| 94 | }) => { |
| 95 | // The harness builds before serving live mode, so dist/build.json |
| 96 | // exists once the worker-scoped server fixture is up. |
| 97 | void server; |
| 98 | const raw = await readFile( |
| 99 | new URL("../dist/build.json", import.meta.url), |
| 100 | "utf8", |
| 101 | ); |
| 102 | const build = JSON.parse(raw) as { |
| 103 | serverModules?: Array<{ name: string }> | undefined; |
| 104 | }; |
| 105 | expect(build.serverModules ?? []).toEqual([]); |
| 106 | }); |
| 107 | } |
| 108 | }); |
| 109 | } |