// @vitest-environment jsdom import { registerBoardRowHeights } from "./board-windowing"; import { afterEach, describe, expect, it, vi } from "vitest"; import { captureBoardDragPreview, createBoardBoundsModifier, measureBoardDragBounds, measureBoardDropBundleHeight, measureBoardDropVisualTarget, } from "./board-dnd"; function rect({ height, left, top, width, }: { height: number; left: number; top: number; width: number; }) { return { bottom: top + height, height, left, right: left + width, top, width, } as DOMRect; } describe("board drag overlay", () => { afterEach(() => { document.body.replaceChildren(); vi.restoreAllMocks(); }); it("reserves the full bundle using column-owned offscreen geometry", () => { const root = document.createElement("div"), column = document.createElement("div"); column.dataset.boardColumnScroller = ""; column.dataset.boardColumnStatus = "done"; column.style.rowGap = "8px"; root.append(column); document.body.append(root); const dispose = registerBoardRowHeights(column, (id) => id === "first" ? 300 : id === "second" ? 120 : null); try { expect(measureBoardDropBundleHeight({ root, activeIds: ["first", "second"], status: "done" })).toBe(428); expect(measureBoardDropBundleHeight({ root: document.createElement("div"), activeIds: ["first"], status: "done" })).toBeNull(); } finally { dispose(); } }); it("captures a static card without duplicate interactive identity", () => { document.body.innerHTML = `
`; const html = captureBoardDragPreview("issue"); expect(html).toContain("Card"); expect(html).toContain("pointer-events-none"); expect(html).not.toContain("data-issue-id"); expect(html).not.toContain("opacity-40"); expect(html).not.toContain('id="title"'); expect(html).not.toContain("aria-"); expect(html).not.toContain("tabindex"); }); it("keeps duplicate retained issue IDs and destination columns outside the active board", () => { const hidden = document.createElement("div"); const active = document.createElement("div"); for (const [board, title, size] of [[hidden, "Hidden card", 900], [active, "Active card", 60]] as const) { const card = document.createElement("article"); card.dataset.issueId = "issue"; card.textContent = title; card.getBoundingClientRect = () => rect({ height: size, left: 20, top: 100, width: 280 }); const column = document.createElement("div"); column.dataset.boardColumnScroller = ""; column.dataset.boardColumnStatus = "done"; column.style.rowGap = "8px"; column.getBoundingClientRect = () => rect({ height: 500, left: 350, top: 70, width: 300 }); const marker = document.createElement("div"); marker.dataset.boardDropIndicator = ""; column.append(marker); board.append(card, column); } hidden.style.display = "none"; document.body.append(hidden, active); expect(captureBoardDragPreview("issue", active)).toContain("Active card"); expect(captureBoardDragPreview("issue", active)).not.toContain("Hidden card"); expect(measureBoardDropBundleHeight({ root: active, activeIds: ["issue"], status: "done" })).toBe(60); expect(measureBoardDropVisualTarget({ root: active, activeId: "issue", activeIds: ["issue"], status: "done", bounds: null })) .toEqual({ height: 60, left: 350, top: 70, width: 300 }); expect(captureBoardDragPreview("issue", null)).toBeNull(); }); it("measures the card viewport below column headers", () => { const board = document.createElement("div"); const column = document.createElement("div"); column.dataset.boardColumnScroller = ""; board.appendChild(column); vi.spyOn(board, "getBoundingClientRect").mockReturnValue({ left: 10, right: 610, top: 20, bottom: 620, } as DOMRect); vi.spyOn(column, "getBoundingClientRect").mockReturnValue({ left: 10, right: 310, top: 70, bottom: 590, } as DOMRect); expect(measureBoardDragBounds(board)).toEqual({ left: 10, right: 610, top: 70, bottom: 590, }); }); it("clamps the overlay inside cached bounds", () => { const modifier = createBoardBoundsModifier({ current: { left: 0, right: 300, top: 50, bottom: 400 }, }); const transform = modifier({ draggingNodeRect: { left: 20, right: 120, top: 100, bottom: 180, }, transform: { x: -50, y: 300, scaleX: 1, scaleY: 1 }, } as never); expect(transform).toEqual({ x: -19, y: 219, scaleX: 1, scaleY: 1 }); }); it("measures the rendered marker as an immediate cross-column target", () => { const source = document.createElement("article"); source.dataset.issueId = "issue"; source.getBoundingClientRect = () => rect({ height: 60, left: 20, top: 100, width: 280 }); const column = document.createElement("div"); column.dataset.boardColumnScroller = ""; column.dataset.boardColumnStatus = "done"; column.style.rowGap = "8px"; column.getBoundingClientRect = () => rect({ height: 500, left: 350, top: 70, width: 300 }); const marker = document.createElement("div"); marker.dataset.boardDropIndicator = ""; const anchor = document.createElement("article"); anchor.getBoundingClientRect = () => rect({ height: 70, left: 360, top: 200, width: 280 }); column.append(marker, anchor); document.body.append(source, column); expect( measureBoardDropVisualTarget({ activeId: "issue", activeIds: ["issue"], bounds: null, status: "done", }), ).toEqual({ height: 60, left: 360, top: 200, width: 280 }); }); it("removes the source slot when measuring a same-column reorder", () => { const column = document.createElement("div"); column.dataset.boardColumnScroller = ""; column.dataset.boardColumnStatus = "todo"; column.style.rowGap = "8px"; const source = document.createElement("article"); source.dataset.issueId = "issue"; source.getBoundingClientRect = () => rect({ height: 60, left: 20, top: 100, width: 280 }); const marker = document.createElement("div"); marker.dataset.boardDropIndicator = ""; const anchor = document.createElement("article"); anchor.getBoundingClientRect = () => rect({ height: 70, left: 20, top: 300, width: 280 }); column.append(source, marker, anchor); document.body.append(column); expect( measureBoardDropVisualTarget({ activeId: "issue", activeIds: ["issue"], bounds: null, status: "todo", })?.top, ).toBe(232); }); it("measures an end-of-column marker without a footer anchor", () => { const source = document.createElement("article"); source.dataset.issueId = "issue"; source.getBoundingClientRect = () => rect({ height: 60, left: 20, top: 100, width: 280 }); const column = document.createElement("div"); column.dataset.boardColumnScroller = ""; column.dataset.boardColumnStatus = "done"; column.style.rowGap = "8px"; const previous = document.createElement("article"); previous.getBoundingClientRect = () => rect({ height: 70, left: 360, top: 200, width: 280 }); const marker = document.createElement("div"); marker.dataset.boardDropIndicator = ""; column.append(previous, marker); document.body.append(source, column); expect( measureBoardDropVisualTarget({ activeId: "issue", activeIds: ["issue"], bounds: null, status: "done", }), ).toEqual({ height: 60, left: 360, top: 278, width: 280 }); }); it("uses the content padding for an empty destination column", () => { const source = document.createElement("article"); source.dataset.issueId = "issue"; source.getBoundingClientRect = () => rect({ height: 60, left: 20, top: 100, width: 280 }); const column = document.createElement("div"); column.dataset.boardColumnScroller = ""; column.dataset.boardColumnStatus = "canceled"; column.style.padding = "8px"; column.getBoundingClientRect = () => rect({ height: 500, left: 350, top: 70, width: 300 }); const marker = document.createElement("div"); marker.dataset.boardDropIndicator = ""; column.append(marker); document.body.append(source, column); expect( measureBoardDropVisualTarget({ activeId: "issue", activeIds: ["issue"], bounds: null, status: "canceled", }), ).toEqual({ height: 60, left: 358, top: 78, width: 284 }); }); it("reserves the complete bundle height including internal stack gaps", () => { const first = document.createElement("article"); first.dataset.issueId = "first"; first.getBoundingClientRect = () => rect({ height: 60, left: 20, top: 100, width: 280 }); const second = document.createElement("article"); second.dataset.issueId = "second"; second.getBoundingClientRect = () => rect({ height: 80, left: 20, top: 168, width: 280 }); const column = document.createElement("div"); column.dataset.boardColumnScroller = ""; column.dataset.boardColumnStatus = "done"; column.style.rowGap = "8px"; document.body.append(first, second, column); expect( measureBoardDropBundleHeight({ activeIds: ["first", "second"], status: "done", }), ).toBe(148); }); });