import { type Mock, vi } from "vitest"; import { layersModule } from "../modules/built-ins/layers/layers-module"; import { mediaSourceModule } from "../modules/built-ins/media-source/media-source-module"; import { model3dModule } from "../modules/built-ins/model-3d/model-3d-module"; import { defineToolcraft } from "../schema/define-toolcraft"; import type { ToolcraftControlSchema } from "../schema/types"; import { createToolcraftState } from "../state/create-template-state"; import { toolcraftReducer } from "../composition/public-state"; import type { ToolcraftCommand, ToolcraftState } from "../state/types"; import type { ToolcraftBinaryAssetLease, ToolcraftBinaryAssetRepository, } from "./repository/binary-asset-repository"; import type { ToolcraftPreparedSourceAssetRecord, ToolcraftSourceAssetBatch, ToolcraftSourceAssetHandler, ToolcraftSourceAssetImportPlan, ToolcraftSourceAssetPrepareContext, } from "./source-asset-types"; export type Deferred = { promise: Promise; reject: (error: unknown) => void; resolve: (value: Value) => void; }; export type TestLease = ToolcraftBinaryAssetLease & { commit: Mock; rollback: Mock; }; type TestRepository = ToolcraftBinaryAssetRepository & { beginLease: Mock; collect: Mock; dispose: Mock; }; 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", ): Extract< ToolcraftControlSchema, { type: "fileDrop"; assetKind?: "file" | "image" } > { return { applicability: { mode: "always" as const }, 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< ToolcraftSourceAssetHandler< "file", ToolcraftPreparedSourceAssetRecord<"file"> >["match"] > & {}, ) => ToolcraftSourceAssetImportPlan; }; 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), }; } export function createLease(overrides: Partial = {}): TestLease { const refs = new Set(); return { commit: overrides.commit ?? vi.fn(async () => undefined), get: overrides.get ?? vi.fn(async () => null), put: overrides.put ?? vi.fn(async (ref: string) => { refs.add(ref); }), refs: overrides.refs ?? vi.fn(() => [...refs]), rollback: overrides.rollback ?? vi.fn(async () => undefined), } satisfies TestLease; } export function createRepository( leases: readonly TestLease[], overrides: Partial = {}, ): TestRepository { let index = 0; return { beginLease: vi.fn(async (jobId) => { if (overrides.beginLease) return overrides.beginLease(jobId); const lease = leases[index++]; if (!lease) throw new Error("Missing test lease"); return lease; }), collect: vi.fn( (reachableRefs) => overrides.collect?.(reachableRefs) ?? Promise.resolve([]), ), dispose: vi.fn(() => overrides.dispose?.() ?? Promise.resolve()), get: vi.fn((ref) => overrides.get?.(ref) ?? Promise.resolve(null)), } satisfies TestRepository; } export function createStateHarness(layers = true, model = false) { let state = createToolcraftState( defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true, upload: true }, panels: model ? { controls: { title: "Source", sections: [{ id: "source", controls: { model: { type: "fileDrop", assetKind: "model", target: "source.model", applicability: { mode: "always" } }, } }] } } : {}, }, modules: [mediaSourceModule(), ...(layers ? [layersModule()] : []), ...(model ? [model3dModule()] : [])], }), ); const dispatch = vi.fn((command: ToolcraftCommand) => { state = toolcraftReducer(state, command); }); return { dispatch, getState: () => state, setState: (nextState: ToolcraftState) => { state = nextState; }, }; }