1import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright";
2import { expect, test } from "@playwright/test";
3
4const get = (server: { url: URL }, path: string) =>
5 fetch(new URL(path, server.url));
6
7const stampOf = (body: string, prefix: string): string | undefined =>
8 body.match(new RegExp(`${prefix}:(?:<!-- -->)?(\\d+)`))?.[1];
9
10/** GET `path` until `predicate(stamp)` holds — bounded polling. */
11const pollStamp = async (
12 server: { url: URL },
13 path: string,
14 prefix: string,
15 predicate: (stamp: string) => boolean,
16 { times = 40, delayMs = 500 }: { times?: number; delayMs?: number } = {},
17): Promise<string> => {
18 let last: string | undefined;
19 for (let i = 0; i < times; i++) {
20 const response = await get(server, path);
21 if (response.status === 200) {
22 last = stampOf(await response.text(), prefix);
23 if (last !== undefined && predicate(last)) return last;
24 }
25 await new Promise((resolve) => setTimeout(resolve, delayMs));
26 }
27 throw new Error(`stamp never satisfied predicate (last: ${last})`);
28};
29
30for (const mode of Playwright.SERVER_METHODS) {
31 test.describe(mode, () => {
32 const it = Playwright.make(mode);
33
34 it("caches the ISR page in KV across requests", async ({ server }) => {
35 const first = await get(server, "/isr");
36 expect(first.status).toBe(200);
37 const stamp = stampOf(await first.text(), "isr-stamp");
38 expect(stamp).toBeDefined();
39
40 // Within the (long) revalidate window the KV-cached payload serves
41 // as-is — same stamp on every hit.
42 const second = await get(server, "/isr");
43 expect(stampOf(await second.text(), "isr-stamp")).toBe(stamp);
44 });
45
46 it("revalidatePath purges the writable cache (on-demand ISR)", async ({
47 server,
48 }) => {
49 // Prime the cache and capture the current stamp.
50 const primed = await pollStamp(server, "/isr", "isr-stamp", () => true);
51
52 const revalidate = await fetch(new URL("/api/revalidate", server.url), {
53 method: "POST",
54 });
55 expect(revalidate.status).toBe(200);
56 expect((await revalidate.json()) as object).toEqual({
57 revalidated: true,
58 });
59
60 // The purge lands in KV; the next render produces a NEW stamp. With
61 // the read-only static-assets cache this would never change — this
62 // assertion is the proof the cache is writable.
63 const fresh = await pollStamp(
64 server,
65 "/isr",
66 "isr-stamp",
67 (s) => s !== primed,
68 );
69 expect(fresh).not.toBe(primed);
70 });
71
72 it("time-based revalidation regenerates through the DO queue", async ({
73 server,
74 }) => {
75 test.setTimeout(90_000);
76 // Prime the 2s-revalidate page.
77 const primed = await pollStamp(
78 server,
79 "/fast-isr",
80 "fast-isr-stamp",
81 () => true,
82 );
83
84 // Let the window lapse, then keep hitting: the first stale hit
85 // enqueues a background revalidation (DO queue → self-reference
86 // fetch → KV write); a later hit serves the regenerated payload.
87 await new Promise((resolve) => setTimeout(resolve, 2_500));
88 const fresh = await pollStamp(
89 server,
90 "/fast-isr",
91 "fast-isr-stamp",
92 (s) => s !== primed,
93 {
94 times: 60,
95 },
96 );
97 expect(fresh).not.toBe(primed);
98 });
99 });
100}