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