| 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | |
| 3 | import { COMPOSER_MENTION_DRAG_TYPE } from "~/components/chat/composerMentionDrag"; |
| 4 | import { createFileTreeDragMentionController } from "./fileTreeDragMention.ts"; |
| 5 | |
| 6 | const 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 | |
| 15 | const rowNode = (path: string) => ({ |
| 16 | getAttribute: (name: string) => (name === "data-item-path" ? path : null), |
| 17 | }); |
| 18 | |
| 19 | describe("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 | }); |