1import * as Cloudflare from "alchemy/Cloudflare";
2import * as Test from "alchemy/Test/Bun";
3import { expect } from "bun:test";
4import * as Console from "effect/Console";
5import * as Effect from "effect/Effect";
6import Stack from "../alchemy.run.ts";
7
8// Fresh `workers.dev` URLs transiently 404 while the route propagates.
9// `Test.getWhenReady` fails on that cold-start window and retries until the
10// worker serves a real response.
11const { getWhenReady } = Test;
12
13const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
14 providers: Cloudflare.providers(),
15 state: Cloudflare.state(),
16});
17
18// The first deploy runs the full Waku build, so give the hook more headroom
19// than the default 120s.
20const stack = beforeAll(deploy(Stack).pipe(Effect.tap(Console.log)), {
21 timeout: 600_000,
22});
23afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack));
24
25const base = Effect.map(stack, ({ url }) => {
26 if (!url) throw new Error("expected the site to expose a workers.dev url");
27 return url.replace(/\/+$/, "");
28});
29
30test(
31 "deploys and exposes a url",
32 Effect.gen(function* () {
33 const { url } = yield* stack;
34 expect(url).toBeString();
35 }),
36 { timeout: 180_000 },
37);
38
39test(
40 "serves the server-rendered home page",
41 Effect.gen(function* () {
42 const url = yield* base;
43 const res = yield* getWhenReady(url);
44 expect(res.status).toBe(200);
45 const html = yield* res.text;
46 // The `GREETING` env value from alchemy.run.ts, read via `getEnv` in the
47 // dynamic RSC page — proves the Worker rendered it at request time.
48 expect(html).toContain("Hello from Waku on Cloudflare!");
49 expect(html).toContain(
50 "This page is rendered by the Worker on every request.",
51 );
52 }),
53 { timeout: 180_000 },
54);
55
56test(
57 "compiles tailwind from waku.config.ts",
58 Effect.gen(function* () {
59 const url = yield* base;
60 const res = yield* getWhenReady(url);
61 expect(res.status).toBe(200);
62 const html = yield* res.text;
63 // The utility class from src/pages/index.tsx made it into the markup.
64 expect(html).toContain("text-3xl");
65
66 // Locate the emitted stylesheet. Waku links the compiled CSS bundle in
67 // the document head; the @tailwindcss/vite plugin registered via
68 // waku.config.ts's `vite` field is what compiles it.
69 const links = [...html.matchAll(/<link\b[^>]*>/g)]
70 .map((m) => m[0])
71 .filter((tag) => /rel="stylesheet"/.test(tag))
72 .map((tag) => /href="([^"]+)"/.exec(tag)?.[1])
73 .filter((href): href is string => !!href);
74 expect(links.length).toBeGreaterThan(0);
75
76 let compiled = "";
77 for (const href of links) {
78 const cssUrl = href.startsWith("http")
79 ? href
80 : `${url}${href.startsWith("/") ? "" : "/"}${href}`;
81 const cssRes = yield* getWhenReady(cssUrl);
82 expect(cssRes.status).toBe(200);
83 compiled += yield* cssRes.text;
84 }
85 // The compiled rule proves tailwind ran through the plugin from
86 // waku.config.ts — not just that the class name appears in markup.
87 expect(compiled).toContain(".text-3xl");
88 expect(compiled).toContain("font-bold");
89 }),
90 { timeout: 180_000 },
91);
92
93test(
94 "serves a static asset from public/",
95 Effect.gen(function* () {
96 const url = yield* base;
97 const res = yield* getWhenReady(`${url}/hello.txt`);
98 expect(res.status).toBe(200);
99 expect(yield* res.text).toContain("hello from public/");
100 }),
101 { timeout: 180_000 },
102);