1import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright";
2import { expect, test } from "@playwright/test";
3
4/** Marker baked into `index.html` — present in any response that is the SPA shell. */
5const SHELL_MARKER = "solid-spa-shell";
6
7for (const method of Playwright.SERVER_METHODS) {
8 test.describe(method, () => {
9 const it = Playwright.make(method);
10
11 it("renders the homepage", async ({ page, server }) => {
12 const response = await page.goto(server.url.toString());
13 expect(response?.status()).toBe(200);
14 await page.waitForLoadState("networkidle");
15 await page.evaluate(() => document.fonts.ready);
16
17 await expect(page).toHaveScreenshot("index.png", {
18 animations: "disabled",
19 maxDiffPixelRatio: 0.03,
20 });
21
22 expect(await page.textContent("output")).toBe("Count: 0");
23 await page.click("button:has-text('+')");
24 expect(await page.textContent("output")).toBe("Count: 1");
25
26 await page.click("a[href='/about']");
27 await page.waitForURL("**/about");
28 await page.evaluate(() => document.fonts.ready);
29
30 await expect(page).toHaveScreenshot("about.png", {
31 animations: "disabled",
32 maxDiffPixelRatio: 0.03,
33 });
34 });
35
36 it("deep link to a client route serves the SPA shell, then hydrates the right view", async ({
37 page,
38 server,
39 }) => {
40 // Raw response: the app shell (not_found_handling:
41 // "single-page-application" live; Vite's SPA fallback in dev) — a
42 // client-only app has no /about asset, so the shell must come back.
43 const raw = await server.fetch("/about");
44 expect(raw.status).toBe(200);
45 const html = await raw.text();
46 expect(html).toContain(SHELL_MARKER);
47 // The shell is not pre-rendered: no route content in the raw HTML.
48 expect(html).not.toContain("A page all about this website");
49
50 // Browser: the same deep link hydrates into the About view.
51 await page.goto(new URL("/about", server.url).toString());
52 await expect(page.locator("h1")).toHaveText("About");
53 await expect(page.locator("main")).toContainText(
54 "A page all about this website",
55 );
56 });
57
58 it("serves the SPA shell (200) for a raw fetch of an unknown path", async ({
59 page,
60 server,
61 }) => {
62 const raw = await server.fetch("/definitely/not/a/route");
63 expect(raw.status).toBe(200);
64 expect(await raw.text()).toContain(SHELL_MARKER);
65
66 // The shell hydrates into the router's catch-all 404 view.
67 await page.goto(
68 new URL("/definitely/not/a/route", server.url).toString(),
69 );
70 await expect(page.locator("h1")).toHaveText("404: Not Found");
71 });
72
73 it("client-side routes between views without a full navigation", async ({
74 page,
75 server,
76 }) => {
77 await page.goto(server.url.toString());
78 await expect(page.locator("h1")).toHaveText("Home");
79 // Plant a marker a full-page navigation would lose.
80 await page.evaluate(() => {
81 (window as { __spa?: boolean }).__spa = true;
82 });
83 await page.click("a[href='/about']");
84 await expect(page.locator("h1")).toHaveText("About");
85 await page.click("a[href='/']");
86 await expect(page.locator("h1")).toHaveText("Home");
87 expect(
88 await page.evaluate(() => (window as { __spa?: boolean }).__spa),
89 ).toBe(true);
90 });
91
92 it("serves static assets directly", async ({ server }) => {
93 const response = await server.fetch("/robots.txt");
94 expect(response.status).toBe(200);
95 expect(await response.text()).toContain("User-agent: *");
96 });
97 });
98}