import { cleanup, render, waitFor } from "@testing-library/react"; import * as React from "react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createToolcraftSourceAssetPresentationResources, type ToolcraftPresentationResourceHandle, } from "../../source-assets/source-asset-presentation-resources"; import type { ToolcraftMediaAsset } from "../../state/types"; import { ToolcraftMediaPresentationProvider, useToolcraftMediaPresentationUrls, } from "./toolcraft-media-presentation"; afterEach(cleanup); function imageAsset( id: string, resourceRef = `media:image:${id}`, ): ToolcraftMediaAsset { return { assetKind: "image", fileName: `${id}.png`, id, layerId: `layer-${id}`, lifecycle: "ready", mimeType: "image/png", position: { x: 0, y: 0 }, resourceRef, }; } describe("Toolcraft media presentation context", () => { it("releases a resource that resolves after its consumer unmounts", async () => { let resolveRetain: ((handle: ToolcraftPresentationResourceHandle) => void) | undefined; const retain = vi.fn( () => new Promise((resolve) => { resolveRetain = resolve; }), ); const release = vi.fn(); function Probe(): React.JSX.Element { useToolcraftMediaPresentationUrls([ { assetKind: "image", fileName: "image.png", id: "image", layerId: "layer", lifecycle: "ready", mimeType: "image/png", position: { x: 0, y: 0 }, resourceRef: "media:image:test", }, ]); return ; } const rendered = render( , ); expect(retain).toHaveBeenCalledTimes(1); rendered.unmount(); if (!resolveRetain) { throw new Error("Expected retain to expose its pending resolver."); } resolveRetain({ release, url: "blob:test" }); await waitFor(() => expect(release).toHaveBeenCalledTimes(1)); }); it("keeps a shared object URL retained when one logical asset is removed", async () => { const createObjectUrl = vi.fn(() => "blob:shared"); const revokeObjectUrl = vi.fn(); const releaseResourceRef = vi.fn(); const resources = createToolcraftSourceAssetPresentationResources({ createObjectUrl, resolveResource: vi.fn(async () => new Uint8Array([1, 2, 3])), retainResourceRef: vi.fn(() => releaseResourceRef), revokeObjectUrl, }); const sharedRef = "media:image:shared"; const first = imageAsset("first", sharedRef); const second = imageAsset("second", sharedRef); function Probe({ assets, }: { assets: readonly ToolcraftMediaAsset[]; }): React.JSX.Element { const urls = useToolcraftMediaPresentationUrls(assets); return ; } const rendered = render( , ); await waitFor(() => expect( rendered.container.firstElementChild?.getAttribute("data-count"), ).toBe("2"), ); rendered.rerender( , ); await waitFor(() => expect( rendered.container.firstElementChild?.getAttribute("data-count"), ).toBe("1"), ); expect(createObjectUrl).toHaveBeenCalledTimes(1); expect(revokeObjectUrl).not.toHaveBeenCalled(); rendered.unmount(); expect(revokeObjectUrl).toHaveBeenCalledTimes(1); expect(releaseResourceRef).toHaveBeenCalledTimes(1); }); });