1import { describe, expect, it } from "@effect/vitest";
2
3import { COMPOSER_MENTION_DRAG_TYPE } from "~/components/chat/composerMentionDrag";
4import { createFileTreeDragMentionController } from "./fileTreeDragMention.ts";
5
6const makeTransfer = (plainText = "") => {
7 const data = new Map<string, string>([["text/plain", plainText]]);
8 return {
9 setData: (format: string, value: string) => void data.set(format, value),
10 getData: (format: string) => data.get(format) ?? "",
11 data,
12 };
13};
14
15const rowNode = (path: string) => ({
16 getAttribute: (name: string) => (name === "data-item-path" ? path : null),
17});
18
19describe("createFileTreeDragMentionController", () => {
20 it("tags a row drag with the mention payload and flags the drag", () => {
21 const controller = createFileTreeDragMentionController({ deselect: () => {} });
22 const transfer = makeTransfer();
23 controller.handleDragStart({
24 dataTransfer: transfer,
25 composedPath: () => [{}, rowNode("docs/index.md"), {}],
26 });
27 expect(transfer.getData(COMPOSER_MENTION_DRAG_TYPE)).toBe("[index.md](docs/index.md)");
28 expect(controller.isDragInProgress()).toBe(true);
29 });
30
31 it("strips the trailing slash from directory rows", () => {
32 const controller = createFileTreeDragMentionController({ deselect: () => {} });
33 const transfer = makeTransfer();
34 controller.handleDragStart({
35 dataTransfer: transfer,
36 composedPath: () => [rowNode("docs/architecture/")],
37 });
38 expect(transfer.getData(COMPOSER_MENTION_DRAG_TYPE)).toBe("[architecture](docs/architecture)");
39 });
40
41 it("does not tag drags of selected text from the panel chrome", () => {
42 // Only a drag that originates on a tree row is a mention; dragging a text
43 // selection also carries text/plain, and tagging it would drop an invalid
44 // pill into the composer.
45 const controller = createFileTreeDragMentionController({ deselect: () => {} });
46 const transfer = makeTransfer("selected text");
47 controller.handleDragStart({ dataTransfer: transfer, composedPath: () => [{}] });
48 expect(transfer.data.has(COMPOSER_MENTION_DRAG_TYPE)).toBe(false);
49 expect(controller.isDragInProgress()).toBe(false);
50 });
51
52 it("ignores drags that carry no row path", () => {
53 const controller = createFileTreeDragMentionController({ deselect: () => {} });
54 const transfer = makeTransfer();
55 controller.handleDragStart({ dataTransfer: transfer, composedPath: () => [{}] });
56 expect(transfer.data.has(COMPOSER_MENTION_DRAG_TYPE)).toBe(false);
57 expect(controller.isDragInProgress()).toBe(false);
58 });
59
60 it("deselects the dragged row exactly once when the drag ends", () => {
61 const deselected: Array<string> = [];
62 const controller = createFileTreeDragMentionController({
63 deselect: (path) => deselected.push(path),
64 });
65 controller.handleDragStart({
66 dataTransfer: makeTransfer(),
67 composedPath: () => [rowNode("src/app.ts")],
68 });
69 controller.handleDragEnd();
70 controller.handleDragEnd();
71 expect(deselected).toEqual(["src/app.ts"]);
72 expect(controller.isDragInProgress()).toBe(false);
73 });
74
75 it("drags the whole selection when the dragged row is part of it", () => {
76 const deselected: Array<string> = [];
77 const controller = createFileTreeDragMentionController({
78 deselect: (path) => deselected.push(path),
79 });
80 controller.handleSelectionChange(["docs/index.md", "docs/api.md", "src/app.ts"]);
81 const transfer = makeTransfer();
82 controller.handleDragStart({
83 dataTransfer: transfer,
84 composedPath: () => [rowNode("docs/api.md")],
85 });
86 expect(transfer.getData(COMPOSER_MENTION_DRAG_TYPE)).toBe(
87 "[index.md](docs/index.md) [api.md](docs/api.md) [app.ts](src/app.ts)",
88 );
89 controller.handleDragEnd();
90 expect(deselected).toEqual(["docs/index.md", "docs/api.md", "src/app.ts"]);
91 });
92
93 it("drags only the row under the cursor when it is outside the selection", () => {
94 const controller = createFileTreeDragMentionController({ deselect: () => {} });
95 controller.handleSelectionChange(["docs/index.md"]);
96 const transfer = makeTransfer();
97 controller.handleDragStart({
98 dataTransfer: transfer,
99 composedPath: () => [rowNode("src/app.ts")],
100 });
101 expect(transfer.getData(COMPOSER_MENTION_DRAG_TYPE)).toBe("[app.ts](src/app.ts)");
102 });
103
104 it("does not deselect anything when no drag was started", () => {
105 const deselected: Array<string> = [];
106 const controller = createFileTreeDragMentionController({
107 deselect: (path) => deselected.push(path),
108 });
109 controller.handleDragEnd();
110 expect(deselected).toEqual([]);
111 });
112});