1import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright";
2import { expect, test } from "@playwright/test";
3import * as NodeFs from "node:fs";
4import * as NodePath from "node:path";
5import { greeting, LIB_VERSION } from "../lib/src/greeting.ts";
6
7const FIXTURE_ROOT = NodePath.join(import.meta.dirname, "..");
8const LIB_ROOT = NodePath.join(FIXTURE_ROOT, "lib");
9const BUILD_JSON = NodePath.join(FIXTURE_ROOT, "dist", "build.json");
10
11for (const method of Playwright.SERVER_METHODS) {
12 test.describe(method, () => {
13 const it = Playwright.make(method);
14
15 it("renders SSR content imported across the workspace boundary", async ({
16 page,
17 server,
18 }) => {
19 const response = await page.goto(server.url.toString());
20 expect(response?.status()).toBe(200);
21 await expect(page.locator("#greeting")).toHaveText(greeting("ssr"));
22 await expect(page.locator("#lib-version")).toHaveText(LIB_VERSION);
23 });
24
25 it("serves the JSON api backed by lib", async ({ server }) => {
26 const body = await server.fetchJson<{
27 greeting: string;
28 libVersion: string;
29 }>("/api/greeting");
30 expect(body.greeting).toBe(greeting("api"));
31 expect(body.libVersion).toBe(LIB_VERSION);
32 });
33 });
34}
35
36// The real assertion of this fixture: the build-output collector classifies
37// lib/ (a sibling of the app's Vite root, with its own package.json) as an
38// external workspace, persisted in dist/build.json. Runs under the `live`
39// server fixture because that guarantees dist/build.json exists (live mode
40// builds and persists it when absent).
41test.describe("build output", () => {
42 const it = Playwright.make("live");
43
44 it("records lib/ in dist/build.json externalWorkspaces", async ({
45 server,
46 }) => {
47 void server; // the live fixture guarantees the build ran and was persisted
48 const parsed = JSON.parse(NodeFs.readFileSync(BUILD_JSON, "utf8")) as {
49 externalWorkspaces?: Array<string>;
50 serverModules?: Array<{ name: string }>;
51 };
52 // externalWorkspaces serializes as a sorted array of workspace roots
53 // (framework-core stringifyBuildOutput).
54 expect(parsed.externalWorkspaces ?? []).toContain(LIB_ROOT);
55 // And the app root itself must NOT be classified as external.
56 expect(parsed.externalWorkspaces ?? []).not.toContain(
57 NodePath.join(FIXTURE_ROOT, "app"),
58 );
59 expect(parsed.externalWorkspaces ?? []).not.toContain(FIXTURE_ROOT);
60 // Sanity: the worker entry made it into the server modules.
61 expect(parsed.serverModules?.length ?? 0).toBeGreaterThan(0);
62 });
63
64 // Memo-bust behavior ("editing lib/src busts the rebuild memo; an untouched
65 // rebuild stays memoized") is deliberately NOT asserted here: the e2e
66 // harness has no build memoization by design (`e2e build` rebuilds
67 // unconditionally). The input-hash memo that consumes `externalWorkspaces`
68 // lives in alchemy's `Website`/`Command.Memo` machinery, and the bust/keep
69 // behavior over this exact fixture shape is asserted by
70 // `alchemy/packages/alchemy/test/Cloudflare/Website/WorkspaceMemo.test.ts`.
71 // This fixture pins the prerequisite: the collector reports `lib/` so the
72 // memo layer has the right inputs (the assertion above).
73});