/** * Draft-preview wiring in `createDecoPage`. * * Separate from `createDecoPage.test.tsx` because this file has to mock * `next/headers`, and that mock would otherwise apply to the plain * (non-draft) cases too — which are worth keeping honest about never * touching a dynamic API at all. */ import { registerSections, setBlocks } from "@decocms/blocks/cms"; import { renderToString } from "react-dom/server"; import { afterEach, describe, expect, it, vi } from "vitest"; // The gate must stop us reaching cookies() at all when the feature is off. const cookiesMock = vi.fn(async () => ({ get: () => undefined })); vi.mock("next/headers", () => ({ cookies: () => cookiesMock(), headers: async () => new Headers(), })); // `connection()` is Next's runtime opt-out from static rendering; outside a // request scope it throws, so it is stubbed to a no-op here. const connectionMock = vi.fn(async () => {}); vi.mock("next/server", () => ({ connection: () => connectionMock() })); const { createDecoPage } = await import("./createDecoPage"); function Hero({ label }: { label?: string }) { return

{`hero-${label ?? "none"}`}

; } function seedPublishedHome() { registerSections({ "site/sections/DraftHero.tsx": async () => ({ default: Hero }) }); setBlocks({ "pages-home": { path: "/", sections: [{ __resolveType: "site/sections/DraftHero.tsx", label: "published" }], }, }); } afterEach(() => { cookiesMock.mockClear(); connectionMock.mockClear(); delete process.env.DECO_ALLOWED_PREVIEW_HOSTS; delete process.env.DECO_SANDBOX_ORIGIN_SUFFIXES; }); describe("createDecoPage — draft preview gate", () => { it("never reads cookies() when the feature is off", async () => { // Load-bearing, not a micro-optimisation: cookies() is a dynamic API, so // calling it unconditionally would opt EVERY page of EVERY site out of // static/ISR rendering the moment this package is upgraded. seedPublishedHome(); const { default: Page } = createDecoPage({ siteName: "test-site" }); const html = renderToString(await Page({ params: Promise.resolve({ slug: [] }) })); expect(html).toContain("hero-published"); expect(cookiesMock).not.toHaveBeenCalled(); expect(connectionMock).not.toHaveBeenCalled(); }); it("stays off when the flag is set but no origin suffix is configured", async () => { // Both halves are required — a bare flag with nowhere to fetch from is // inert, mirroring Fast Deploy's opt-in. process.env.DECO_DRAFT_PREVIEW = "1"; seedPublishedHome(); const { default: Page } = createDecoPage({ siteName: "test-site" }); const html = renderToString(await Page({ params: Promise.resolve({ slug: [] }) })); expect(html).toContain("hero-published"); expect(cookiesMock).not.toHaveBeenCalled(); }); it("reads cookies() once the feature is fully configured", async () => { process.env.DECO_ALLOWED_PREVIEW_HOSTS = "preview.example"; seedPublishedHome(); const { default: Page } = createDecoPage({ siteName: "test-site" }); const html = renderToString(await Page({ params: Promise.resolve({ slug: [] }) })); // No draft pointer on the request, so the page still renders published — // enabling the feature must not change what an ordinary visitor sees. expect(html).toContain("hero-published"); expect(cookiesMock).toHaveBeenCalled(); // Nothing was bound, so no need to force the page dynamic. expect(connectionMock).not.toHaveBeenCalled(); }); });