import { vi } from "vitest"; import { defineToolcraft } from "../schema/define-toolcraft"; import type { ToolcraftControlSchema } from "../schema/types"; import { createToolcraftState } from "../state/create-template-state"; import { toolcraftReducer } from "../state/reducer"; import type { ToolcraftCommand, ToolcraftState, } from "../state/types"; import type { ToolcraftBinaryAssetLease, ToolcraftBinaryAssetRepository, } from "./repository/binary-asset-repository"; import type { ToolcraftFileDropPresentation, ToolcraftPreparedSourceAssetRecord, ToolcraftSourceAssetBatch, ToolcraftSourceAssetHandler, ToolcraftSourceAssetImportPlan, ToolcraftSourceAssetPrepareContext, ToolcraftSourceAssetPresentationContext, } from "./source-asset-types"; export type Deferred = { promise: Promise; reject: (error: unknown) => void; resolve: (value: Value) => void; }; export type TestLease = ToolcraftBinaryAssetLease & { commit: ReturnType; rollback: ReturnType; }; export function deferred(): Deferred { let rejectPromise: ((error: unknown) => void) | undefined; let resolvePromise: ((value: Value) => void) | undefined; const promise = new Promise((resolve, reject) => { rejectPromise = reject; resolvePromise = resolve; }); return { promise, reject: (error) => rejectPromise?.(error), resolve: (value) => resolvePromise?.(value), }; } export function testFile( name: string, type = "application/octet-stream", ): File { return new File([name], name, { type }); } export function fileControl(target = "source.files"): ToolcraftControlSchema { return { assetKind: "file", multiple: true, target, type: "fileDrop", }; } export function fileDraft( fileName: string, sourceTarget?: string, ): ToolcraftPreparedSourceAssetRecord<"file"> { return { assetKind: "file", fileName, lifecycle: "ready", mimeType: "text/plain", position: { x: 0, y: 0 }, resourceRef: `media:file:${fileName}`, ...(sourceTarget ? { sourceTarget } : {}), }; } export function prepared( ...assets: ToolcraftPreparedSourceAssetRecord<"file">[] ) { return { assets, stagedResourceRefs: [] as const }; } type FileHandlerOptions = { plan?: ( batch: ToolcraftSourceAssetBatch, control: ToolcraftControlSchema, claim: ReturnType>["match"]> & {}, ) => ToolcraftSourceAssetImportPlan; present?: ( context: ToolcraftSourceAssetPresentationContext, ) => ToolcraftFileDropPresentation; }; export function createFileHandler( prepare: ( context: ToolcraftSourceAssetPrepareContext, ) => Promise>, options: FileHandlerOptions = {}, ): ToolcraftSourceAssetHandler< "file", ToolcraftPreparedSourceAssetRecord<"file"> > { return { kind: "file", match: vi.fn(( batch: ToolcraftSourceAssetBatch, control: ToolcraftControlSchema, ) => control.assetKind === "file" && batch.files.length > 0 ? { handlerKind: "file" as const, rootFileNames: batch.files.map((file) => file.name), specificity: 1, } : null, ), plan: vi.fn(options.plan ?? ((batch, control, claim) => ({ claim, logicalAssetCount: batch.files.length, replaceExisting: control.multiple !== true, target: control.target, }))), prepare: vi.fn(prepare), present: vi.fn(options.present ?? (() => { throw new Error("presentation not configured for this test"); })), }; } export function createLease( overrides: Partial = {}, ): TestLease { const refs = new Set(); return { commit: vi.fn(async () => undefined), get: vi.fn(async () => null), put: vi.fn(async (ref: string) => { refs.add(ref); }), refs: vi.fn(() => [...refs]), rollback: vi.fn(async () => undefined), ...overrides, } as TestLease; } export function createRepository( leases: readonly TestLease[], overrides: Partial = {}, ): ToolcraftBinaryAssetRepository & { beginLease: ReturnType; collect: ReturnType; dispose: ReturnType; } { let index = 0; return { beginLease: vi.fn(async () => { const lease = leases[index++]; if (!lease) throw new Error("Missing test lease"); return lease; }), collect: vi.fn(async () => []), dispose: vi.fn(async () => undefined), get: vi.fn(async () => null), ...overrides, } as ToolcraftBinaryAssetRepository & { beginLease: ReturnType; collect: ReturnType; dispose: ReturnType; }; } export function createStateHarness(layers = true) { let state = createToolcraftState( defineToolcraft({ canvas: { enabled: true, upload: true }, panels: { layers }, }), ); const dispatch = vi.fn((command: ToolcraftCommand) => { state = toolcraftReducer(state, command); }); return { dispatch, getState: () => state, setState: (nextState: ToolcraftState) => { state = nextState; }, }; }