1import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright";
2import { expect, test } from "@playwright/test";
3
4const MESSAGE = "hello-from-sveltekit-spa-binding";
5/** Marker baked into `src/app.html` — present in any response that is the SPA shell. */
6const SHELL_MARKER = "sveltekit-spa-shell";
7
8for (const mode of Playwright.SERVER_METHODS) {
9 test.describe(mode, () => {
10 // Same Windows-runner socket-buffer exhaustion as the sibling sveltekit
11 // fixture's gate (see packages/frontend-frameworks/fixtures/sveltekit/scripts/e2e.mjs for the full
12 // evidence trail, incl. the failed port-range-widening experiment): the
13 // live serve's miniflare loopback churn hits kernel AFD buffer limits
14 // late in the suite and every fallback-dependent route 404s. Dev mode is
15 // unaffected and stays covered on Windows.
16 test.skip(
17 mode === "live" && process.platform === "win32" && !!process.env.CI,
18 "Windows CI runner socket-buffer exhaustion (see packages/frontend-frameworks/fixtures/sveltekit/scripts/e2e.mjs)",
19 );
20 const it = Playwright.make(mode);
21
22 it("hydrates the home page (no SSR) and honors the user svelte config", async ({
23 page,
24 server,
25 }) => {
26 const response = await page.goto(server.url.toString());
27 expect(response?.status()).toBe(200);
28 await expect(page.locator("#home-title")).toHaveText(
29 "sveltekit-spa-home",
30 );
31 await expect(page.locator("#hydrated")).toHaveText("hydrated:yes");
32 // The user preprocess registered in vite.config.ts's `sveltekit(...)`
33 // call (kit v3 forbids svelte.config.js) rewrote the literal — only
34 // observable if the user's config file was loaded.
35 await expect(page.locator("#svelte-config")).toHaveText(
36 "marker:svelte-config-loaded",
37 );
38 });
39
40 it("deep link to a client route serves the SPA shell, then hydrates the right view", async ({
41 page,
42 server,
43 }) => {
44 // Raw response: the app shell (fallback / CSR shell), NOT the widgets
45 // markup — pages are ssr = false.
46 const raw = await server.fetch("/widgets");
47 expect(raw.status).toBe(200);
48 const html = await raw.text();
49 expect(html).toContain(SHELL_MARKER);
50 expect(html).not.toContain("sprocket");
51
52 // Browser: same deep link hydrates into the widgets view, whose
53 // universal +page.ts load ran CLIENT-side and fetched the server-run
54 // API endpoint.
55 await page.goto(new URL("/widgets", server.url).toString());
56 await expect(page.locator("#widgets-title")).toHaveText(
57 "sveltekit-spa-widgets",
58 );
59 await expect(page.locator("#widgets-server")).toHaveText("server:yes");
60 await expect(page.locator("#widgets-message")).toHaveText(
61 `message:${MESSAGE}`,
62 );
63 // `$spa/widgets` — user alias from vite.config.ts resolved.
64 await expect(page.locator("#widgets-description")).toHaveText(
65 "widgets-via-user-alias:3",
66 );
67 await expect(page.locator("#widgets-list li")).toHaveCount(3);
68 });
69
70 it("client-side routes between views without a full navigation", async ({
71 page,
72 server,
73 }) => {
74 await page.goto(server.url.toString());
75 await expect(page.locator("#hydrated")).toHaveText("hydrated:yes");
76 // Plant a marker a full-page navigation would lose.
77 await page.evaluate(() => {
78 (window as { __spa?: boolean }).__spa = true;
79 });
80 await page.click("#nav-about");
81 await expect(page.locator("#about-title")).toHaveText(
82 "sveltekit-spa-about",
83 );
84 await page.click("#nav-widgets");
85 await expect(page.locator("#widgets-title")).toHaveText(
86 "sveltekit-spa-widgets",
87 );
88 await expect(page.locator("#widgets-list li")).toHaveCount(3);
89 expect(
90 await page.evaluate(() => (window as { __spa?: boolean }).__spa),
91 ).toBe(true);
92 });
93
94 it("runs the API endpoint server-side despite ssr=false pages", async ({
95 server,
96 }) => {
97 const body = await server.fetchJson<{
98 server: boolean;
99 message: string | null;
100 widgets: Array<{ id: string; name: string }>;
101 }>("/api/widgets");
102 expect(body.server).toBe(true);
103 // The Text binding is only reachable server-side — proves endpoints
104 // execute in the worker (live) / kit's dev SSR (dev), not the browser.
105 expect(body.message).toBe(MESSAGE);
106 expect(body.widgets).toHaveLength(3);
107 });
108
109 it("serves static assets directly", async ({ server }) => {
110 const response = await server.fetch("/robots.txt");
111 expect(response.status).toBe(200);
112 expect(await response.text()).toContain("User-agent: *");
113 });
114
115 it("serves the SPA fallback for unknown paths and renders the 404 view", async ({
116 page,
117 server,
118 }) => {
119 // not_found_handling: "single-page-application" — an unmatched path
120 // must come back as the app shell (the adapter's generated
121 // index.html fallback live; kit's CSR shell in dev), which then
122 // hydrates into kit's client-side 404 error view.
123 const raw = await server.fetch("/definitely/not/a/route");
124 expect(await raw.text()).toContain(SHELL_MARKER);
125
126 await page.goto(
127 new URL("/definitely/not/a/route", server.url).toString(),
128 );
129 await expect(page.locator("#error-title")).toHaveText("spa-error:404");
130 });
131 });
132}