1import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright";
2import { expect, test } from "@playwright/test";
3
4for (const method of Playwright.SERVER_METHODS) {
5 test.describe(method, () => {
6 const it = Playwright.make(method);
7
8 it("server-renders the homepage markup", async ({ server }) => {
9 // Raw fetch, no JS: the page content (including the counter's initial
10 // state) must already be in the server-rendered HTML.
11 const response = await server.fetch("/");
12 expect(response.status).toBe(200);
13 const html = await response.text();
14 expect(html).toContain("Hello world!");
15 // The counter's SSR markup — solid's hydration markers split the text
16 // ("Clicks: <!--...-->0"), so match the label and the button class.
17 expect(html).toContain("Clicks:");
18 expect(html).toContain('class="increment"');
19 });
20
21 it("renders the homepage", async ({ page, server }) => {
22 const response = await page
23 .goto(server.url.toString())
24 .then(async (response) => {
25 // solidstart has a bug where the first request is not always successful in dev mode.
26 // Retry the request if it is not successful as a temporary workaround.
27 if (response?.ok()) return response;
28 return page.goto(server.url.toString());
29 });
30 expect(response?.status()).toBe(200);
31 await page.waitForLoadState("networkidle");
32 await page.evaluate(() => document.fonts.ready);
33
34 await expect(page).toHaveScreenshot("index.png", {
35 animations: "disabled",
36 maxDiffPixelRatio: 0.03,
37 });
38
39 // Hydration: the SSR'd counter button responds to clicks.
40 expect(await page.textContent("button.increment")).toBe("Clicks: 0");
41 await page.click("button.increment");
42 expect(await page.textContent("button.increment")).toBe("Clicks: 1");
43
44 await page.click("a[href='/about']");
45 await page.waitForURL("**/about");
46 await page.evaluate(() => document.fonts.ready);
47
48 await expect(page).toHaveScreenshot("about.png", {
49 animations: "disabled",
50 maxDiffPixelRatio: 0.03,
51 });
52 });
53
54 it("serves a direct navigation to /about (SSR)", async ({
55 page,
56 server,
57 }) => {
58 // Raw fetch: the about markup is server-rendered.
59 const raw = await server.fetch("/about");
60 expect(raw.status).toBe(200);
61 expect(await raw.text()).toContain(">About</h1>");
62
63 // Hard browser navigation straight to the route.
64 const response = await page.goto(
65 new URL("/about", server.url).toString(),
66 );
67 expect(response?.status()).toBe(200);
68 await expect(page.locator("h1")).toHaveText("About");
69 });
70
71 it("renders the [...404] route for unmatched paths", async ({
72 page,
73 server,
74 }) => {
75 // The [...404].tsx catch-all renders server-side. NOTE: despite the
76 // route's <HttpStatusCode code={404} />, SolidStart 2.0.0-alpha.3's
77 // streaming response commits status 200 in both modes (observed live
78 // and dev), so this asserts the route's markup, not the status code.
79 const raw = await server.fetch("/definitely/not/a/route");
80 expect(await raw.text()).toContain("Page Not Found");
81
82 await page.goto(
83 new URL("/definitely/not/a/route", server.url).toString(),
84 );
85 await expect(page.locator("h1")).toHaveText("Page Not Found");
86 });
87
88 it("serves static assets from public/", async ({ server }) => {
89 const response = await server.fetch("/favicon.ico");
90 expect(response.status).toBe(200);
91 expect(response.headers.get("content-type")).toContain("image");
92 });
93
94 it("runs the API route server-side (GET)", async ({ server }) => {
95 const body = await server.fetchJson<{ method: string; server: boolean }>(
96 "/api/echo",
97 );
98 expect(body).toMatchObject({ method: "GET", server: true });
99 });
100
101 it("round-trips a POST through the API route", async ({ server }) => {
102 // Plain HTTP (not `server.fetch`): the live-mode dispatchFetch wrapper
103 // in the harness drops the RequestInit, losing POST bodies (see the
104 // same pattern in packages/frontend-frameworks/fixtures/waku/test/smoke.test.ts). Both modes listen
105 // on a real socket, so this exercises the same worker path.
106 const response = await fetch(new URL("/api/echo", server.url), {
107 method: "POST",
108 headers: { "content-type": "application/json" },
109 body: JSON.stringify({ ping: "pong" }),
110 });
111 expect(response.status).toBe(200);
112 expect(await response.json()).toMatchObject({
113 method: "POST",
114 echoed: { ping: "pong" },
115 });
116 });
117 });
118}