import { registerSection, registerSections, registerSectionsSync } from "@decocms/blocks/cms"; import { renderToString } from "react-dom/server"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { SectionRenderer } from "./SectionRenderer"; function Hero({ label }: { label?: string }) { return

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

; } // The test environment runs under jsdom (see root vitest.config.ts), where // `window` is defined. next/dynamic's internals branch on // `typeof window === "undefined"` to decide server vs. client behavior, so // under jsdom it always takes the client path — `{ ssr: false }` vs. // `{ ssr: true }` produce IDENTICAL rendered output here (both effectively // render `null` during a synchronous renderToString call, since the loader's // promise can't resolve in time regardless of the ssr option). Asserting on // rendered HTML would therefore pass even if ClientOnlySection passed // `{ ssr: true }` by mistake — it exercises no real server/window-undefined // code path in this environment. // // Instead we verify the actual contract ClientOnlySection is responsible // for: that it calls next/dynamic with `{ ssr: false }`. We spy on the // `next/dynamic` module import via `vi.mock` + `vi.hoisted`. const { dynamicSpy } = vi.hoisted(() => ({ dynamicSpy: vi.fn() })); vi.mock("next/dynamic", () => ({ default: (loader: unknown, options: unknown) => { dynamicSpy(loader, options); return function DynamicStub() { return null; }; }, })); beforeEach(() => { dynamicSpy.mockClear(); }); describe("SectionRenderer (next)", () => { it("renders a sync-registered section directly", async () => { const KEY = "site/sections/NextSyncA.tsx"; registerSectionsSync({ [KEY]: { default: Hero } }); const element = await SectionRenderer({ resolved: { key: KEY, component: KEY, props: { label: "a" } } as any, }); const html = renderToString(element); expect(html).toContain("hero-a"); }); it("awaits a code-split (non-sync) section's loader", async () => { const KEY = "site/sections/NextLazyA.tsx"; registerSections({ [KEY]: () => Promise.resolve({ default: Hero }) }); const element = await SectionRenderer({ resolved: { key: KEY, component: KEY, props: { label: "b" } } as any, }); const html = renderToString(element); expect(html).toContain("hero-b"); }); it("routes a clientOnly-registered section through next/dynamic with ssr:false", async () => { const KEY = "site/sections/NextClientOnlyA.tsx"; const loader = () => Promise.resolve({ default: Hero }); registerSection(KEY, loader, { clientOnly: true }); const element = await SectionRenderer({ resolved: { key: KEY, component: KEY, props: { label: "c" } } as any, }); const html = renderToString(element); // The actual contract: ClientOnlySection must call next/dynamic with // { ssr: false } for this section's loader. This fails if the // implementation is changed to pass { ssr: true } (or omits the option). expect(dynamicSpy).toHaveBeenCalledWith(loader, { ssr: false }); // The section shell (wrapping
+ manifest key) still renders // server-side regardless of the dynamic child's ssr option. expect(html).toContain(`data-manifest-key="${KEY}"`); }); it("warns and renders nothing for an unregistered section", async () => { const KEY = "site/sections/NextMissingA.tsx"; const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); const element = await SectionRenderer({ resolved: { key: KEY, component: KEY, props: {} } as any, }); expect(element).toBeNull(); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining(`No component registered for: ${KEY}`), ); warnSpy.mockRestore(); }); });