// @vitest-environment jsdom import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { FileDropControl } from "./file-drop-control"; import type { FileDropPresentation } from "./file-drop-types"; afterEach(() => { cleanup(); }); const modelPresentation = { accept: ".gltf,.glb", allowsFileBatch: true, allowsFolderSelection: true, emptyDescription: "or drop its local dependencies", emptyTitle: "Upload model", items: [], presenter: "model-item", secondaryActions: [], } satisfies FileDropPresentation<"model">; describe("FileDropControl folder selection", () => { it("exposes typed presentation feedback for browser evidence", () => { render( , ); const alert = screen.getByRole("alert"); expect(alert.textContent).toBe("A referenced texture is unavailable."); expect(alert.getAttribute("data-file-drop-feedback-code")).toBe("missing-appearance-resource"); }); it("preserves a selected model folder as one physical file batch", () => { const onFilesSelect = vi.fn(); const onFolderSelect = vi.fn(); const files = [ new File([new Uint8Array([1])], "scene.gltf"), new File([new Uint8Array([2])], "mesh.bin"), ]; Object.defineProperty(files[0], "webkitRelativePath", { value: "robot/scene.gltf", }); Object.defineProperty(files[1], "webkitRelativePath", { value: "robot/mesh.bin", }); render( , ); const folderInput = screen.getByLabelText("Upload model folder") as HTMLInputElement; fireEvent.change(folderInput, { target: { files } }); expect(folderInput.hasAttribute("webkitdirectory")).toBe(true); expect(folderInput.hasAttribute("directory")).toBe(true); expect(folderInput.accept).toBe(""); expect(onFolderSelect).toHaveBeenCalledWith(files); expect(onFilesSelect).not.toHaveBeenCalled(); expect(files.map((file) => file.webkitRelativePath)).toEqual([ "robot/scene.gltf", "robot/mesh.bin", ]); }); it("does not expose folder selection for an ordinary file presentation", () => { render( , ); expect(screen.queryByRole("button", { name: "Choose folder" })).toBeNull(); expect(screen.queryByLabelText("Upload file folder")).toBeNull(); }); }); describe("FileDropControl collection actions", () => { const filePresentation = { accept: "image/*", allowsFileBatch: true, allowsFolderSelection: false, emptyDescription: "or drop bump maps", emptyTitle: "Upload bump maps", items: [], presenter: "file-list", secondaryActions: [], } satisfies FileDropPresentation<"file">; it("adds an empty upload slot before selecting another file", () => { const onFilesSelect = vi.fn(); render( , ); fireEvent.click(screen.getByRole("button", { name: "Add bump map" })); expect(onFilesSelect).not.toHaveBeenCalled(); expect( (screen.getByRole("button", { name: "Add bump map" }) as HTMLButtonElement) .disabled, ).toBe(true); const input = screen.getByLabelText("Upload bump map 2") as HTMLInputElement; const file = new File([new Uint8Array([1])], "grain.png", { type: "image/png", }); fireEvent.change(input, { target: { files: [file] } }); expect(onFilesSelect).toHaveBeenCalledWith([file]); }); it("removes the final item and suppresses duplicate list cardinality actions", () => { const onItemRemove = vi.fn(); const finalItem = { assetKind: "file" as const, fileName: "fine.png", id: "bump-fine", }; render( , ); expect(screen.queryByRole("button", { name: "Add a new file" })).toBeNull(); expect(screen.queryByRole("button", { name: "Remove broad.png" })).toBeNull(); expect(screen.queryByRole("button", { name: "Remove fine.png" })).toBeNull(); fireEvent.click(screen.getByRole("button", { name: "Remove bump map" })); expect(onItemRemove).toHaveBeenCalledWith(finalItem, 1); }); it("removes a pending final slot before deleting an attached file", () => { const onItemRemove = vi.fn(); render( , ); fireEvent.click(screen.getByRole("button", { name: "Add bump map" })); expect(screen.getByLabelText("Upload bump map 2")).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Remove bump map" })); expect(screen.queryByLabelText("Upload bump map 2")).toBeNull(); expect(onItemRemove).not.toHaveBeenCalled(); }); it("renders item-owned settings only under an attached upload", () => { const renderItemContent = vi.fn((item: { id: string }) => (
Settings for {item.id}
)); render( , ); expect(screen.getByText("Settings for bump-broad")).toBeTruthy(); fireEvent.click(screen.getByRole("button", { name: "Add bump map" })); expect(screen.getByLabelText("Upload bump map 2")).toBeTruthy(); expect(renderItemContent).toHaveBeenCalled(); expect( renderItemContent.mock.calls.every(([item]) => item.id === "bump-broad"), ).toBe(true); }); it("separates attached logical file groups with one between-group line", () => { const { container } = render(
Settings for {item.id}
, }} onFilesSelect={vi.fn()} onItemRemove={vi.fn()} presentation={{ ...filePresentation, items: [ { assetKind: "file", fileName: "broad.png", id: "broad" }, { assetKind: "file", fileName: "fine.png", id: "fine" }, ], }} />, ); const groups = container.querySelectorAll( '[data-slot="collection-item-group"]', ); expect(groups).toHaveLength(2); expect(groups[0]?.className).not.toContain("border-t"); expect(groups[1]?.className).toContain("border-t"); expect(container.textContent).not.toMatch(/Item 1|Item 2/u); }); });