| 1 | import * as Playwright from "@alchemy.run/cloudflare-test-tools/e2e/Playwright"; |
| 2 | import { expect, test } from "@playwright/test"; |
| 3 | import * as NodeFs from "node:fs"; |
| 4 | import * as NodePath from "node:path"; |
| 5 | import { greeting, LIB_VERSION } from "../lib/src/greeting.ts"; |
| 6 | |
| 7 | const FIXTURE_ROOT = NodePath.join(import.meta.dirname, ".."); |
| 8 | const LIB_ROOT = NodePath.join(FIXTURE_ROOT, "lib"); |
| 9 | const BUILD_JSON = NodePath.join(FIXTURE_ROOT, "dist", "build.json"); |
| 10 | |
| 11 | for (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). |
| 41 | test.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 | }); |