import { cleanup, render } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { renderFileDropControl } from "./controls-panel-media-renderer"; import type { ToolcraftSourceAssetCoordinator } from "../../../source-assets/source-asset-coordinator"; const legacyFileDrop = vi.hoisted(() => vi.fn((_props: unknown, _legacyContext?: unknown) => null), ); vi.mock("@repo/ui", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, FileDrop: legacyFileDrop, }; }); afterEach(() => { cleanup(); legacyFileDrop.mockClear(); }); describe("controls-panel media renderer", () => { it("does not invoke the legacy uploader for model fileDrops", () => { const dispatchCommand = vi.fn(); const idleOperation = { phase: "idle" as const, target: "source.model", }; const sourceAssetCoordinator = { cancelTarget: vi.fn(), clearPresentationFeedback: vi.fn(), dispose: vi.fn(async () => undefined), getOperation: () => idleOperation, getPresentation: vi.fn(() => null), hydrateBinaryMedia: vi.fn(async () => undefined), hydrateModels: vi.fn(async () => undefined), importBatch: vi.fn(), repairModel: vi.fn(), reportPresentationFeedback: vi.fn(), subscribe: () => () => undefined, } satisfies ToolcraftSourceAssetCoordinator; const renderedControl = renderFileDropControl({ canvasSize: { height: 720, unit: "px", width: 1280 }, control: { assetKind: "model", target: "source.model", type: "fileDrop", }, dispatchCommand, id: "source-model", mediaAssets: [], name: "Model", sourceAssetCoordinator, }); const { container } = render(<>{renderedControl}); expect(container.firstChild).toBeNull(); expect(legacyFileDrop).not.toHaveBeenCalled(); expect(dispatchCommand).not.toHaveBeenCalled(); }); it("uses the coordinator presentation delegated by the registered handler", () => { const control = { assetKind: "file" as const, target: "source.files", type: "fileDrop" as const, }; const operation = { phase: "idle" as const, target: control.target }; const presentation = { accept: ".txt", allowsFileBatch: true, allowsFolderSelection: false, emptyDescription: "Drop files", emptyTitle: "Upload files", items: [], presenter: "file-list" as const, secondaryActions: [], }; const getPresentation = vi.fn(() => presentation); const sourceAssetCoordinator = { cancelTarget: vi.fn(), clearPresentationFeedback: vi.fn(), dispose: vi.fn(async () => undefined), getOperation: vi.fn(() => operation), getPresentation, hydrateBinaryMedia: vi.fn(async () => undefined), hydrateModels: vi.fn(async () => undefined), importBatch: vi.fn(), repairModel: vi.fn(), reportPresentationFeedback: vi.fn(), subscribe: () => () => undefined, } satisfies ToolcraftSourceAssetCoordinator; render( <> {renderFileDropControl({ canvasSize: { height: 720, unit: "px", width: 1280 }, control, dispatchCommand: vi.fn(), id: "source-files", mediaAssets: [], name: "Source files", sourceAssetCoordinator, })} , ); expect(getPresentation).toHaveBeenCalledWith(control, [], operation); expect(legacyFileDrop).toHaveBeenCalledWith( expect.objectContaining({ presentation }), undefined, ); expect(legacyFileDrop.mock.calls[0]?.[0]).toEqual( expect.objectContaining({ collectionActions: undefined, onFolderSelect: undefined, }), ); }); it("maps the collection-actions variant to compact FileDrop cardinality controls", () => { const control = { assetKind: "file" as const, itemLabel: "bump map", multiple: true, target: "surface.bumpMaps", type: "fileDrop" as const, variant: "collection-actions", }; const operation = { phase: "idle" as const, target: control.target }; const presentation = { accept: "image/*", allowsFileBatch: true, allowsFolderSelection: false, emptyDescription: "Drop bump maps", emptyTitle: "Upload bump maps", items: [{ assetKind: "file" as const, fileName: "grain.png", id: "bump-grain", }], presenter: "file-list" as const, secondaryActions: [], }; const sourceAssetCoordinator = { cancelTarget: vi.fn(), clearPresentationFeedback: vi.fn(), dispose: vi.fn(async () => undefined), getOperation: vi.fn(() => operation), getPresentation: vi.fn(() => presentation), hydrateBinaryMedia: vi.fn(async () => undefined), hydrateModels: vi.fn(async () => undefined), importBatch: vi.fn(), repairModel: vi.fn(), reportPresentationFeedback: vi.fn(), subscribe: () => () => undefined, } satisfies ToolcraftSourceAssetCoordinator; render( <> {renderFileDropControl({ canvasSize: { height: 720, unit: "px", width: 1280 }, control, dispatchCommand: vi.fn(), id: "surface-bump-maps", mediaAssets: [], name: "Bump maps", sourceAssetCoordinator, })} , ); expect(legacyFileDrop.mock.calls[0]?.[0]).toEqual( expect.objectContaining({ collectionActions: { addLabel: "Add bump map", canAdd: true, itemLabel: "bump map", name: "Bump maps", removeLabel: "Remove bump map", }, }), ); }); it("routes model file and folder picks through one coordinator import path", () => { const control = { assetKind: "model" as const, target: "source.model", type: "fileDrop" as const, }; const operation = { phase: "idle" as const, target: control.target }; const presentation = { accept: ".gltf,.glb", allowsFileBatch: true, allowsFolderSelection: true, emptyDescription: "Drop a model", emptyTitle: "Upload model", items: [], presenter: "model-item" as const, secondaryActions: [], }; const importBatch = vi.fn(); const sourceAssetCoordinator = { cancelTarget: vi.fn(), clearPresentationFeedback: vi.fn(), dispose: vi.fn(async () => undefined), getOperation: vi.fn(() => operation), getPresentation: vi.fn(() => presentation), hydrateBinaryMedia: vi.fn(async () => undefined), hydrateModels: vi.fn(async () => undefined), importBatch, repairModel: vi.fn(), reportPresentationFeedback: vi.fn(), subscribe: () => () => undefined, } satisfies ToolcraftSourceAssetCoordinator; render( <> {renderFileDropControl({ canvasSize: { height: 720, unit: "px", width: 1280 }, control, dispatchCommand: vi.fn(), id: "source-model", mediaAssets: [], name: "Model", sourceAssetCoordinator, })} , ); const fileDropProps = legacyFileDrop.mock.calls[0]?.[0] as unknown as { onFilesSelect: (files: File[]) => void; onFolderSelect: (files: File[]) => void; }; const files = [new File([new Uint8Array([1])], "scene.gltf")]; expect(fileDropProps.onFolderSelect).toBe(fileDropProps.onFilesSelect); fileDropProps.onFolderSelect(files); expect(importBatch).toHaveBeenCalledWith( { files, origin: "panel", target: "source.model" }, control, ); }); it("routes the built-in model repair action to the source coordinator", () => { const control = { assetKind: "model" as const, target: "source.model", type: "fileDrop" as const, }; const operation = { phase: "idle" as const, target: control.target }; const presentation = { accept: ".glb", allowsFileBatch: true, allowsFolderSelection: true, emptyDescription: "Drop a model", emptyTitle: "Upload model", items: [{ assetKind: "model" as const, fileName: "scene.glb", id: "model-1", }], presenter: "model-item" as const, secondaryActions: [{ label: "Fix model", value: "model.fix", }], }; const repairModel = vi.fn(async () => ({ assetIds: ["model-1"], kind: "committed" as const, })); const sourceAssetCoordinator = { cancelTarget: vi.fn(), clearPresentationFeedback: vi.fn(), dispose: vi.fn(async () => undefined), getOperation: vi.fn(() => operation), getPresentation: vi.fn(() => presentation), hydrateBinaryMedia: vi.fn(async () => undefined), hydrateModels: vi.fn(async () => undefined), importBatch: vi.fn(), repairModel, reportPresentationFeedback: vi.fn(), subscribe: () => () => undefined, } satisfies ToolcraftSourceAssetCoordinator; render( <> {renderFileDropControl({ canvasSize: { height: 720, unit: "px", width: 1280 }, control, dispatchCommand: vi.fn(), id: "source-model", mediaAssets: [], name: "Model", sourceAssetCoordinator, })} , ); const fileDropCalls = legacyFileDrop.mock.calls as unknown as Array<[ { onAction?: (value: string) => void }, ]>; const onAction = fileDropCalls[0]?.[0].onAction; expect(onAction).toBeTypeOf("function"); onAction?.("model.fix"); expect(repairModel).toHaveBeenCalledWith("model-1"); }); });