1import { describe, expect, it, vi } from "@effect/vitest";
2import {
3 makeWorkspaceFileDropHandlers,
4 type WorkspaceFileDragEvent,
5 type WorkspaceFileDropHost,
6} from "./workspaceFileDrop";
7
8function 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
31function 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
39describe("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});