import * as React from "react"; import { cleanup, render, waitFor } from "@testing-library/react"; import { renderToString } from "react-dom/server"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { ToolcraftSourceAssetCoordinator } from "../../source-assets/source-asset-coordinator"; import { ToolcraftSourceAssetProvider, useToolcraftSourceAssetRetention } from "./toolcraft-source-asset-context"; import { ToolcraftSourceAssetCoordinatorProbe as CoordinatorProbe, createTestSourceAssetCoordinator as createTestCoordinator, createTestSourceAssetStore as createTestStore, } from "./toolcraft-source-asset-test-support"; afterEach(cleanup); function createDeferred() { let reject!: (reason?: unknown) => void; const promise = new Promise((_resolve, rejectPromise) => { reject = rejectPromise; }); return { promise, reject }; } describe("Toolcraft source asset provider ownership", () => { it("retains the same source owner through unmount until export cleanup releases it", async () => { const store = createTestStore(); const coordinator = createTestCoordinator(); let retain!: () => () => void; function RetentionProbe() { retain = useToolcraftSourceAssetRetention(); return null; } const createCoordinator = vi.fn(() => coordinator); const view = render( ); const release = retain(); view.unmount(); await Promise.resolve(); expect(coordinator.dispose).not.toHaveBeenCalled(); expect(createCoordinator).toHaveBeenCalledOnce(); release(); release(); await waitFor(() => expect(coordinator.dispose).toHaveBeenCalledOnce()); }); it("does not acquire or cache an owner for an uncommitted render", () => { const store = createTestStore(); const coordinator = createTestCoordinator(); const createCoordinator = vi.fn(() => coordinator); expect( renderToString( Server probe , ), ).toBe(""); expect(createCoordinator).not.toHaveBeenCalled(); const rendered = render( , ); expect(rendered.getByTestId("committed-source-asset-child")).toBeDefined(); expect(createCoordinator).toHaveBeenCalledTimes(1); }); it("preserves the injected coordinator receiver through the provider", async () => { type ReceiverAwareCoordinator = ToolcraftSourceAssetCoordinator & { resolverBytes: Uint8Array; }; const resolverBytes = new Uint8Array([4, 5, 6]); const coordinator: ReceiverAwareCoordinator = { ...createTestCoordinator(), resolverBytes, resolveResource: vi.fn(async function (this: ReceiverAwareCoordinator) { expect(this).toBe(coordinator); return this.resolverBytes; }), }; const observed: ToolcraftSourceAssetCoordinator[] = []; render( coordinator} store={createTestStore()} > observed.push(value)} /> , ); const resolveResource = observed[0]?.resolveResource; if (!resolveResource) throw new Error("Expected a resource resolver."); await expect( resolveResource("media:image:receiver-aware", { signal: new AbortController().signal, }), ).resolves.toBe(resolverBytes); }); it("reports hydration failures through the contextual owner channel", async () => { const failure = new Error("model hydration failed"); const onError = vi.fn(); const coordinator = createTestCoordinator({ hydrateModels: vi.fn(async () => Promise.reject(failure)), }); const rendered = render( coordinator} onError={onError} store={createTestStore()} > , ); await waitFor(() => expect(onError).toHaveBeenCalledWith(failure)); rendered.unmount(); expect(onError).toHaveBeenCalledTimes(1); }); it("keeps same-store construction inputs stable across prop identity changes", async () => { const store = createTestStore(); const firstReporter = vi.fn(); const replacementReporter = vi.fn(); const hydration = createDeferred(); const hydrationFailure = new Error("original owner hydration failed"); const coordinator = createTestCoordinator({ hydrateModels: vi.fn(() => hydration.promise), }); const observed: ToolcraftSourceAssetCoordinator[] = []; const firstFactory = vi.fn(() => coordinator); const replacementFactory = vi.fn(() => createTestCoordinator()); const rendered = render( observed.push(value)} /> , ); rendered.rerender( observed.push(value)} /> , ); expect(rendered.getByTestId("stable-source-asset-child")).toBeDefined(); expect(observed.length).toBeGreaterThan(0); expect(observed.every((value) => value === coordinator)).toBe(true); expect(firstFactory).toHaveBeenCalledTimes(1); expect(replacementFactory).not.toHaveBeenCalled(); hydration.reject(hydrationFailure); await waitFor(() => expect(firstReporter).toHaveBeenCalledWith(hydrationFailure), ); expect(replacementReporter).not.toHaveBeenCalled(); }); it("uses current construction inputs when the store changes", async () => { const firstStore = createTestStore(); const secondStore = createTestStore(); const firstReporter = vi.fn(); const secondReporter = vi.fn(); const secondFailure = new Error("second store hydration failed"); const firstCoordinator = createTestCoordinator(); const secondCoordinator = createTestCoordinator({ hydrateModels: vi.fn(async () => Promise.reject(secondFailure)), }); const firstFactory = vi.fn(() => firstCoordinator); const secondFactory = vi.fn(() => secondCoordinator); const rendered = render( , ); rendered.rerender( , ); await waitFor(() => expect(secondReporter).toHaveBeenCalledWith(secondFailure), ); expect(rendered.getByTestId("switched-source-asset-child")).toBeDefined(); expect(firstFactory).toHaveBeenCalledWith(firstStore); expect(secondFactory).toHaveBeenCalledWith(secondStore); expect(firstReporter).not.toHaveBeenCalledWith(secondFailure); }); it("does not expose the deprecated disposal reporter alias", () => { const props: React.ComponentProps = { children: , // @ts-expect-error Source asset failures use the single onError authority. onDisposeError: vi.fn(), store: createTestStore(), }; expect("onDisposeError" in props).toBe(true); }); });