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 } 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("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("supports the deprecated error reporter alias for all owner failures", async () => { const operationalFailure = new Error("model hydration failed through alias"); const disposalFailure = new Error("owner disposal failed through alias"); const onDisposeError = vi.fn(); const coordinator = createTestCoordinator({ dispose: vi.fn(async () => Promise.reject(disposalFailure)), hydrateModels: vi.fn(async () => Promise.reject(operationalFailure)), }); const rendered = render( coordinator} onDisposeError={onDisposeError} store={createTestStore()} > , ); await waitFor(() => expect(onDisposeError).toHaveBeenCalledWith( operationalFailure, )); rendered.unmount(); await waitFor(() => expect(onDisposeError).toHaveBeenCalledWith( disposalFailure, )); }); it("prefers onError over the deprecated reporter alias", async () => { const failure = new Error("primary reporter failure"); const onError = vi.fn(); const onDisposeError = vi.fn(); const rendered = render( createTestCoordinator({ hydrateModels: vi.fn(async () => Promise.reject(failure)), })} onDisposeError={onDisposeError} onError={onError} store={createTestStore()} > , ); await waitFor(() => expect(onError).toHaveBeenCalledWith(failure)); expect(onDisposeError).not.toHaveBeenCalled(); rendered.unmount(); }); });