import { beforeEach, describe, expect, it, vi } from "vitest"; import { createToolcraftVideoEncoderBackend } from "./video-encoding-backend"; const hooks = vi.hoisted(() => ({ add: vi.fn(async () => undefined), addTrack: vi.fn(), cancel: vi.fn<() => Promise>(async () => undefined), close: vi.fn(), finalize: vi.fn(async () => undefined), start: vi.fn(async () => undefined), })); vi.mock("mediabunny", () => ({ BufferTarget: class { buffer = new Uint8Array([1, 2, 3]).buffer; on() {} }, CanvasSource: class { add = hooks.add; close = hooks.close; }, Mp4OutputFormat: class {}, WebMOutputFormat: class {}, canEncodeVideo: async () => true, Output: class { state = "pending"; addVideoTrack = hooks.addTrack; async start() { this.state = "started"; await hooks.start(); } async cancel() { this.state = "canceled"; await hooks.cancel(); } async finalize() { this.state = "finalizing"; await hooks.finalize(); this.state = "finalized"; } }, })); beforeEach(() => { vi.resetAllMocks(); hooks.add.mockResolvedValue(undefined); hooks.cancel.mockResolvedValue(undefined); hooks.finalize.mockResolvedValue(undefined); hooks.start.mockResolvedValue(undefined); }); const request = () => ({ canvas: document.createElement("canvas"), durationSeconds: 1, height: 128, requestedFormat: "mp4" as const, signal: new AbortController().signal, width: 128, }); describe("video backend lifetime", () => { it("cancels partially started output when startup rejects", async () => { const failure = new Error("startup failed"); hooks.start.mockRejectedValue(failure); await expect(createToolcraftVideoEncoderBackend(request())).rejects.toBe(failure); expect(hooks.cancel).toHaveBeenCalledOnce(); }); it("cancels output if adding a track fails before start", async () => { const failure = new Error("track setup failed"); hooks.addTrack.mockImplementation(() => { throw failure; }); await expect(createToolcraftVideoEncoderBackend(request())).rejects.toBe(failure); expect(hooks.start).not.toHaveBeenCalled(); expect(hooks.cancel).toHaveBeenCalledOnce(); }); it("retains the startup failure when cleanup also fails", async () => { const primary = new Error("startup failed"); const cleanup = new Error("cancel failed"); hooks.start.mockRejectedValue(primary); hooks.cancel.mockRejectedValue(cleanup); await expect(createToolcraftVideoEncoderBackend(request())).rejects.toMatchObject({ cause: primary, errors: [primary, cleanup], }); }); it("shares cancellation settlement instead of returning early for a second cancel", async () => { let finish!: () => void; hooks.cancel.mockImplementation(() => new Promise((resolve) => { finish = resolve; })); const backend = await createToolcraftVideoEncoderBackend(request()); const cancellation = backend.cancel(); const repeated = backend.cancel(); expect(repeated).toBe(cancellation); await Promise.resolve(); expect(hooks.cancel).toHaveBeenCalledOnce(); finish(); await Promise.all([cancellation, repeated]); }); });