import { describe, expect, it, vi } from "vitest"; import { layersModule } from "../../../modules/built-ins/layers/layers-module"; import { mediaSourceModule } from "../../../modules/built-ins/media-source/media-source-module"; import type { ToolcraftSourceAssetCoordinator } from "../../../source-assets/source-asset-coordinator"; import { ToolcraftSourceAssetCoordinatorContext } from "../../app-shell/toolcraft-source-asset-context"; import { ControlsPanel, defineToolcraft, fireEvent, render, renderControlsPanelWithSchema, screen, ToolcraftRoot, } from "../testing/controls-panel-test-utils"; describe("ControlsPanel media upload multi-image grid", () => { it("submits one complete multi-image selection to the coordinator", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true, upload: true }, panels: { controls: { sections: [ { id: "test-section-1", controls: { source: { applicability: { mode: "always" as const }, label: "Images", multiple: true, target: "input.sources", type: "fileDrop", }, }, title: "Input", }, ], title: "Generation Controls", }, }, }, modules: [mediaSourceModule()], }); const importBatch = vi.fn( async () => ({ assetIds: [] as const, kind: "committed" as const, }), ); const idleOperation = Object.freeze({ phase: "idle" as const, target: "input.sources", }); const coordinator: ToolcraftSourceAssetCoordinator = { cancelTarget: vi.fn(), clearPresentationFeedback: vi.fn(), dispose: vi.fn(async () => undefined), getOperation: () => idleOperation, supportsKind: (kind) => kind === "image", hydrateBinaryMedia: vi.fn(async () => undefined), hydrateModels: vi.fn(async () => undefined), importBatch, repairModel: vi.fn(async () => ({ kind: "cancelled" as const })), reportPresentationFeedback: vi.fn(), subscribe: () => () => undefined, }; const { container } = render( , ); const input = container.querySelector('input[type="file"]'); const files = [ new File(["one"], "one.png", { type: "image/png" }), new File(["two"], "two.png", { type: "image/png" }), ]; fireEvent.change(input as HTMLInputElement, { target: { files }, }); expect(importBatch).toHaveBeenCalledTimes(1); expect(importBatch.mock.calls[0]?.[0]).toMatchObject({ files, origin: "panel", target: "input.sources", }); expect(importBatch.mock.calls[0]?.[1]).toMatchObject({ target: "input.sources", }); }); it("renders multi-image file uploads as an add-last thumbnail grid", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true, upload: true }, panels: { controls: { sections: [ { id: "test-section-2", controls: { source: { applicability: { mode: "always" as const }, accept: "PNG, SVG", label: "Images", multiple: true, target: "input.sources", type: "fileDrop", }, }, layout: "standalone", title: "Input", }, ], title: "Generation Controls", }, }, }, modules: [mediaSourceModule()], }); const { container } = renderControlsPanelWithSchema(schema, undefined, { layers: [ { id: "layer-1", kind: "layer", name: "one", visible: true }, { id: "layer-2", kind: "layer", name: "two", visible: true }, ], mediaAssets: [ { sourceSize: { height: 96, unit: "px", width: 96 }, assetKind: "image", lifecycle: "ready", resourceRef: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' fill='%23777'/%3E%3C/svg%3E", fileName: "one.svg", id: "media-1", layerId: "layer-1", mimeType: "image/svg+xml", position: { x: 0, y: 0 }, size: { height: 96, unit: "px", width: 96 }, }, { sourceSize: { height: 96, unit: "px", width: 96 }, assetKind: "image", lifecycle: "ready", resourceRef: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' fill='%23999'/%3E%3C/svg%3E", fileName: "two.svg", id: "media-2", layerId: "layer-2", mimeType: "image/svg+xml", position: { x: 0, y: 0 }, size: { height: 96, unit: "px", width: 96 }, }, ], selectedLayerId: "layer-2", }); const addImageFiles = screen.getByRole("button", { name: "Add image files", }); const previewGrid = container.querySelector( '[data-slot="file-upload-preview-grid"]', ); expect(previewGrid?.className).toContain("grid-cols-4"); expect(previewGrid?.lastElementChild).toBe(addImageFiles); expect(addImageFiles.getAttribute("data-slot")).toBe("button"); expect(addImageFiles.getAttribute("data-variant")).toBe("outline"); expect(addImageFiles.getAttribute("data-size")).toBe("icon"); const fileInput = container.querySelector('input[type="file"]'); if (!fileInput) { throw new Error("Expected multi-image file input to render"); } const fileInputClick = vi.spyOn(fileInput, "click"); fireEvent.click(addImageFiles); expect(fileInputClick).toHaveBeenCalledTimes(1); expect(screen.getByRole("img", { name: "one.svg" })).toBeTruthy(); expect(screen.getByRole("img", { name: "two.svg" })).toBeTruthy(); expect(screen.getByTestId("media-ids").textContent).toBe("media-1,media-2"); expect(screen.queryByRole("button", { name: "90° Left" })).toBeNull(); const firstPreview = screen .getByRole("img", { name: "one.svg" }) .closest('[data-slot="file-upload-preview-item"]'); const secondPreview = screen .getByRole("img", { name: "two.svg" }) .closest('[data-slot="file-upload-preview-item"]'); if (!firstPreview || !secondPreview) { throw new Error("Expected multi-image previews to render"); } expect(firstPreview.getAttribute("role")).toBeNull(); expect(screen.getByRole("button", { name: "Select one.svg" })).toBeTruthy(); expect( screen.getByRole("button", { name: "Reorder one.svg" }), ).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Select one.svg" })); expect(firstPreview.getAttribute("data-selected")).toBe("true"); expect(screen.getByRole("button", { name: "90° Right" })).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "90° Right" })); expect(screen.getByTestId("media-transforms").textContent).toBe( '[{"rotationDeg":90},null]', ); fireEvent.click(screen.getByRole("button", { name: "Remove two.svg" })); expect(screen.getByTestId("media-count").textContent).toBe("1"); expect(screen.queryByRole("img", { name: "two.svg" })).toBeNull(); }); it("uses Layers selection for image transforms without rendering a duplicate media collection", () => { const schema = defineToolcraft({ base: { identity: { id: "runtime-test", title: "Toolcraft App" }, canvas: { enabled: true, upload: true }, panels: { controls: { sections: [ { id: "test-section-3", controls: { source: { applicability: { mode: "always" as const }, assetKind: "image", label: "Images", multiple: true, target: "input.sources", type: "fileDrop", }, }, title: "Input", }, ], title: "Generation Controls", }, }, }, modules: [layersModule(), mediaSourceModule()], }); const { container } = renderControlsPanelWithSchema(schema, undefined, { layers: [ { id: "layer-1", kind: "layer", name: "one", visible: true }, { id: "layer-2", kind: "layer", name: "two", visible: true }, ], mediaAssets: [ { sourceSize: { height: 96, unit: "px", width: 96 }, assetKind: "image", lifecycle: "ready", resourceRef: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' fill='%23777'/%3E%3C/svg%3E", fileName: "one.svg", id: "media-1", layerId: "layer-1", mimeType: "image/svg+xml", position: { x: 0, y: 0 }, size: { height: 96, unit: "px", width: 96 }, sourceTarget: "input.sources", }, { sourceSize: { height: 96, unit: "px", width: 96 }, assetKind: "image", lifecycle: "ready", resourceRef: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96'%3E%3Crect width='96' height='96' fill='%23999'/%3E%3C/svg%3E", fileName: "two.svg", id: "media-2", layerId: "layer-2", mimeType: "image/svg+xml", position: { x: 0, y: 0 }, size: { height: 96, unit: "px", width: 96 }, sourceTarget: "input.sources", }, ], selectedLayerId: "layer-2", }); expect(container.querySelector('input[type="file"]')).toBeTruthy(); expect( container.querySelector('[data-slot="file-upload-preview-grid"]'), ).toBeNull(); expect( container.querySelector('[data-slot="file-upload-file-list"]'), ).toBeNull(); expect(screen.queryByRole("img", { name: "one.svg" })).toBeNull(); expect(screen.queryByRole("img", { name: "two.svg" })).toBeNull(); fireEvent.click(screen.getByRole("button", { name: "90° Right" })); fireEvent.click(screen.getByRole("button", { name: "Flip horizontal" })); fireEvent.click(screen.getByRole("button", { name: "Flip vertical" })); expect(screen.getByTestId("media-transforms").textContent).toBe( '[null,{"rotationDeg":90,"flipHorizontal":true,"flipVertical":true}]', ); }); });