import { afterEach, describe, expect, it, vi } from "vitest"; import { decodeToolcraftArtifactImage } from "./artifact-export-image"; afterEach(() => vi.unstubAllGlobals()); function browserImage() { const instances: ImageDouble[] = []; class ImageDouble { onload: (() => void) | null = null; onerror: (() => void) | null = null; src = ""; constructor() { instances.push(this); } } const createObjectURL = vi.fn(() => "blob:export-image"); const revokeObjectURL = vi.fn(); vi.stubGlobal("Image", ImageDouble); vi.stubGlobal("URL", { createObjectURL, revokeObjectURL }); return { createObjectURL, instances, revokeObjectURL }; } describe("export image decoding", () => { it("retains a decoded source until explicit idempotent disposal", async () => { const browser = browserImage(); const controller = new AbortController(); const pending = decodeToolcraftArtifactImage(new Uint8Array([1]), "image/png", controller.signal); const image = browser.instances[0]!; expect(image.src).toBe("blob:export-image"); image.onload!(); const decoded = await pending; expect(decoded.source).toBe(image); expect(image.onload).toBeNull(); expect(image.onerror).toBeNull(); controller.abort(); expect(image.src).toBe("blob:export-image"); expect(browser.revokeObjectURL).not.toHaveBeenCalled(); decoded.dispose(); decoded.dispose(); expect(image.src).toBe(""); expect(browser.revokeObjectURL).toHaveBeenCalledExactlyOnceWith("blob:export-image"); }); it("cancels pending decoding with the owning signal reason and detaches handlers", async () => { const browser = browserImage(); const controller = new AbortController(); const pending = decodeToolcraftArtifactImage(new Uint8Array([1]), "image/png", controller.signal); const image = browser.instances[0]!; controller.abort(); await expect(pending).rejects.toBe(controller.signal.reason); expect(image.src).toBe(""); expect(image.onload).toBeNull(); expect(image.onerror).toBeNull(); expect(browser.revokeObjectURL).toHaveBeenCalledOnce(); }); it("releases failed sources and allocates nothing for an already aborted request", async () => { const browser = browserImage(); const controller = new AbortController(); const pending = decodeToolcraftArtifactImage(new Uint8Array([1]), "image/png", controller.signal); browser.instances[0]!.onerror!(); await expect(pending).rejects.toThrow("decode"); expect(browser.revokeObjectURL).toHaveBeenCalledOnce(); controller.abort(); expect(() => decodeToolcraftArtifactImage(new Uint8Array([1]), "image/png", controller.signal)).toThrow(); expect(browser.createObjectURL).toHaveBeenCalledOnce(); }); });