import { act, type ReactNode } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ExtensionSlotBoundary } from "./components/ExtensionSlotBoundary"; import { mergeExtensionBridges, normalizeExtensionBridge, registerExtensionBridgeOnce, } from "./extensionSlots"; let root: Root | null = null; let container: HTMLDivElement | null = null; let consoleError: ReturnType; function render(node: ReactNode): HTMLDivElement { container = document.createElement("div"); document.body.append(container); root = createRoot(container); act(() => root?.render(node)); return container; } beforeEach(() => { consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined); }); afterEach(() => { act(() => root?.unmount()); root = null; container?.remove(); container = null; consoleError.mockRestore(); }); describe("Swatches admin extension registry", () => { it("drops malformed slots and bounds runtime descriptors", () => { const StatusBadge = () => Status; const bridge = normalizeExtensionBridge({ statusBadge: StatusBadge, previewSlot: "not-a-component", heroDescription: " x ".repeat(1001), tabs: [ null, { id: "", label: "Missing id" }, { id: "shop", label: " Shop ", Icon: "invalid" }, ], searchSchemaOverrides: Object.fromEntries( Array.from({ length: 205 }, (_, index) => [ index === 0 ? "x".repeat(121) : `field-${index}`, { label: index === 1 ? " Extension field " : "y".repeat(201), keywords: Array.from({ length: 30 }, (__, keywordIndex) => keywordIndex === 0 ? "z".repeat(101) : ` keyword-${keywordIndex} `, ), }, ]), ), }); expect(bridge.statusBadge).toBe(StatusBadge); expect(bridge.previewSlot).toBeUndefined(); expect(bridge.heroDescription).toBeUndefined(); expect(bridge.tabs).toEqual([ { id: "shop", label: "Shop", Icon: undefined }, ]); expect(Object.keys(bridge.searchSchemaOverrides ?? {})).toHaveLength(199); expect(bridge.searchSchemaOverrides?.["field-1"]).toMatchObject({ label: "Extension field", }); expect(bridge.searchSchemaOverrides?.["field-1"].keywords).toHaveLength(23); }); it("composes render slots and providers without replacing earlier bundles", () => { const ExistingBadge = () => Existing; const IncomingBadge = () => Incoming; const ExistingProvider = ({ children }: { children: ReactNode }) => (
{children}
); const IncomingProvider = ({ children }: { children: ReactNode }) => (
{children}
); const existingSync = vi.fn(); const incomingSync = vi.fn(); const merged = mergeExtensionBridges( { statusBadge: ExistingBadge, accessProvider: ExistingProvider, tabs: [{ id: "shop", label: "Shop" }], syncViewState: existingSync, }, { statusBadge: IncomingBadge, accessProvider: IncomingProvider, tabs: [ { id: "shop", label: "Shop refined" }, { id: "cart", label: "Cart" }, ], syncViewState: incomingSync, }, ); const StatusBadge = merged.statusBadge; const AccessProvider = merged.accessProvider; const view = render( <> {StatusBadge ? : null} {AccessProvider ? ( Shell ) : null} , ); expect(view.textContent).toContain("ExistingIncoming"); const shell = Array.from(view.querySelectorAll("span")).find( (item) => item.textContent === "Shell", ); expect(shell?.closest('[data-provider="incoming"]')).not.toBeNull(); expect(shell?.closest('[data-provider="existing"]')).not.toBeNull(); expect(merged.tabs?.map(({ id, label }) => ({ id, label }))).toEqual([ { id: "shop", label: "Shop refined" }, { id: "cart", label: "Cart" }, ]); const syncArgs = { target: { tabId: "shop" }, setViewState: vi.fn(), }; merged.syncViewState?.(syncArgs); expect(existingSync).toHaveBeenCalledWith(syncArgs); expect(incomingSync).toHaveBeenCalledWith(syncArgs); }); it("keeps healthy additive contributions when siblings fail and recovers replacements", () => { const HealthyBadge = () => Healthy badge; const ThrowingBadge = (): never => { throw new Error("badge failed"); }; const RecoveredBadge = () => Recovered badge; const HealthyProvider = ({ children }: { children: ReactNode }) => (
{children}
); const ThrowingProvider = (): never => { throw new Error("provider failed"); }; const healthySync = vi.fn(); const throwingSync = vi.fn(() => { throw new Error("sync failed"); }); const merged = mergeExtensionBridges( { statusBadge: HealthyBadge, accessProvider: HealthyProvider, syncViewState: throwingSync, renderExtensionTab: () => Healthy tab, }, { statusBadge: ThrowingBadge, accessProvider: ThrowingProvider, syncViewState: healthySync, renderExtensionTab: () => { throw new Error("tab failed"); }, }, ); const StatusBadge = merged.statusBadge; const AccessProvider = merged.accessProvider; const tab = merged.renderExtensionTab?.({} as never); const view = render( <> {StatusBadge ? : null} {AccessProvider ? ( Provider child ) : null} {tab} , ); expect(view.textContent).toContain("Healthy badge"); expect(view.textContent).toContain("Provider child"); expect( view.querySelector('[data-provider="healthy"]')?.textContent, ).toContain("Provider child"); expect(view.textContent).toContain("Healthy tab"); const syncArgs = { target: { tabId: "shop" }, setViewState: vi.fn(), }; merged.syncViewState?.(syncArgs); expect(throwingSync).toHaveBeenCalledWith(syncArgs); expect(healthySync).toHaveBeenCalledWith(syncArgs); const RecoveredSlot = mergeExtensionBridges( { statusBadge: HealthyBadge }, { statusBadge: RecoveredBadge }, ).statusBadge; act(() => root?.render(RecoveredSlot ? : null)); expect(view.textContent).toBe("Healthy badgeRecovered badge"); }); it("registers one bundle once without duplicate composition", () => { const host: Record = { slots: { statusBadge: () => Existing }, }; const options = { host, slotsKey: "slots", appliedKey: "applied", bridge: { statusBadge: () => Incoming }, }; registerExtensionBridgeOnce(options); const first = host.slots; registerExtensionBridgeOnce(options); expect(host.applied).toBe(true); expect(host.slots).toBe(first); const StatusBadge = normalizeExtensionBridge(host.slots).statusBadge; const view = render(StatusBadge ? : null); expect(view.textContent).toBe("ExistingIncoming"); }); }); describe("ExtensionSlotBoundary", () => { function ThrowingSlot(): never { throw new Error("slot failed"); } it("contains a failed slot and recovers when its component changes", () => { const onError = vi.fn(); const view = render( Fallback} onError={onError} resetKey="bad" > , ); expect(view.textContent).toBe("Fallback"); expect(onError).toHaveBeenCalledOnce(); act(() => { root?.render( Fallback} onError={onError} resetKey="good" > Recovered , ); }); expect(view.textContent).toBe("Recovered"); }); it("preserves the slot failure when a custom reporter also throws", () => { const reportingError = new Error("reporter failed"); render( { throw reportingError; }} > , ); expect(consoleError).toHaveBeenCalledWith( "[b3-swatches] Extension error reporter failed.", reportingError, ); expect(consoleError).toHaveBeenCalledWith( "[b3-swatches] Extension-rendered admin UI failed.", expect.objectContaining({ message: "slot failed" }), expect.any(String), ); }); });