| 1 | import { describe, expect, it, vi } from "@effect/vitest"; |
| 2 | import { |
| 3 | makeWorkspaceFileDropHandlers, |
| 4 | type WorkspaceFileDragEvent, |
| 5 | type WorkspaceFileDropHost, |
| 6 | } from "./workspaceFileDrop"; |
| 7 | |
| 8 | function makeDragEvent(options?: { |
| 9 | types?: string[]; |
| 10 | files?: File[]; |
| 11 | items?: NonNullable<WorkspaceFileDragEvent["dataTransfer"]["items"]>; |
| 12 | movedWithinTarget?: boolean; |
| 13 | }) { |
| 14 | const preventDefault = vi.fn(); |
| 15 | const event = { |
| 16 | dataTransfer: { |
| 17 | types: options?.types ?? ["Files"], |
| 18 | files: options?.files ?? [], |
| 19 | dropEffect: "none", |
| 20 | ...(options?.items === undefined ? {} : { items: options.items }), |
| 21 | }, |
| 22 | relatedTarget: options?.movedWithinTarget ? ({} as EventTarget) : null, |
| 23 | currentTarget: { |
| 24 | contains: () => options?.movedWithinTarget ?? false, |
| 25 | }, |
| 26 | preventDefault, |
| 27 | } satisfies WorkspaceFileDragEvent; |
| 28 | return { event, preventDefault }; |
| 29 | } |
| 30 | |
| 31 | function makeHost() { |
| 32 | const setDragActive = vi.fn(); |
| 33 | const addFiles = vi.fn(); |
| 34 | const addFolders = vi.fn(); |
| 35 | const host = { setDragActive, addFiles, addFolders } satisfies WorkspaceFileDropHost; |
| 36 | return { host, setDragActive, addFiles, addFolders }; |
| 37 | } |
| 38 | |
| 39 | describe("makeWorkspaceFileDropHandlers", () => { |
| 40 | it("activates the target for an external file drag", () => { |
| 41 | const { host, setDragActive } = makeHost(); |
| 42 | const { event, preventDefault } = makeDragEvent(); |
| 43 | |
| 44 | makeWorkspaceFileDropHandlers(host).onDragEnter(event); |
| 45 | |
| 46 | expect(preventDefault).toHaveBeenCalledOnce(); |
| 47 | expect(setDragActive).toHaveBeenCalledWith(true); |
| 48 | }); |
| 49 | |
| 50 | it("ignores non-file drags", () => { |
| 51 | const { host, setDragActive } = makeHost(); |
| 52 | const { event, preventDefault } = makeDragEvent({ types: ["text/plain"] }); |
| 53 | |
| 54 | makeWorkspaceFileDropHandlers(host).onDragOver(event); |
| 55 | |
| 56 | expect(preventDefault).not.toHaveBeenCalled(); |
| 57 | expect(setDragActive).not.toHaveBeenCalled(); |
| 58 | }); |
| 59 | |
| 60 | it("does not flicker when the drag moves between children", () => { |
| 61 | const { host, setDragActive } = makeHost(); |
| 62 | const { event } = makeDragEvent({ movedWithinTarget: true }); |
| 63 | |
| 64 | const handlers = makeWorkspaceFileDropHandlers(host); |
| 65 | handlers.onDragEnter(event); |
| 66 | handlers.onDragLeave(event); |
| 67 | |
| 68 | expect(setDragActive).not.toHaveBeenCalled(); |
| 69 | }); |
| 70 | |
| 71 | it("forwards dropped files and clears the active state", () => { |
| 72 | const file = new File(["contents"], "example.txt", { type: "text/plain" }); |
| 73 | const { host, setDragActive, addFiles } = makeHost(); |
| 74 | const { event } = makeDragEvent({ files: [file] }); |
| 75 | |
| 76 | makeWorkspaceFileDropHandlers(host).onDrop(event); |
| 77 | |
| 78 | expect(setDragActive).toHaveBeenCalledWith(false); |
| 79 | expect(addFiles).toHaveBeenCalledWith([file]); |
| 80 | }); |
| 81 | |
| 82 | it("routes mixed drops to files and folders", () => { |
| 83 | const file = new File(["contents"], "example.txt", { type: "text/plain" }); |
| 84 | const folder = new File([], "project", { type: "" }); |
| 85 | const directory = { |
| 86 | kind: "file", |
| 87 | getAsFile: () => folder, |
| 88 | webkitGetAsEntry: () => ({ isDirectory: true }), |
| 89 | }; |
| 90 | const { host, addFiles, addFolders } = makeHost(); |
| 91 | const { event } = makeDragEvent({ |
| 92 | items: [ |
| 93 | directory, |
| 94 | { |
| 95 | kind: "file", |
| 96 | getAsFile: () => file, |
| 97 | webkitGetAsEntry: () => ({ isDirectory: false }), |
| 98 | }, |
| 99 | ], |
| 100 | }); |
| 101 | |
| 102 | makeWorkspaceFileDropHandlers(host).onDrop(event); |
| 103 | |
| 104 | expect(addFiles).toHaveBeenCalledWith([file]); |
| 105 | expect(addFolders).toHaveBeenCalledWith([folder]); |
| 106 | }); |
| 107 | |
| 108 | it("routes a folder-only drop without attaching files", () => { |
| 109 | const folder = new File([], "project", { type: "" }); |
| 110 | const directory = { |
| 111 | kind: "file", |
| 112 | getAsFile: () => folder, |
| 113 | webkitGetAsEntry: () => ({ isDirectory: true }), |
| 114 | }; |
| 115 | const { host, addFiles, addFolders } = makeHost(); |
| 116 | const { event } = makeDragEvent({ items: [directory] }); |
| 117 | |
| 118 | makeWorkspaceFileDropHandlers(host).onDrop(event); |
| 119 | |
| 120 | expect(addFiles).not.toHaveBeenCalled(); |
| 121 | expect(addFolders).toHaveBeenCalledWith([folder]); |
| 122 | }); |
| 123 | |
| 124 | it("uses files when the browser does not expose drag items", () => { |
| 125 | const file = new File(["contents"], "example.txt", { type: "text/plain" }); |
| 126 | const { host, addFiles, addFolders } = makeHost(); |
| 127 | const { event } = makeDragEvent({ files: [file] }); |
| 128 | |
| 129 | makeWorkspaceFileDropHandlers(host).onDrop(event); |
| 130 | |
| 131 | expect(addFiles).toHaveBeenCalledWith([file]); |
| 132 | expect(addFolders).not.toHaveBeenCalled(); |
| 133 | }); |
| 134 | }); |