import { describe, expect, it, vi } from "vite-plus/test";import { closestCenter, type CollisionDetection } from "@dnd-kit/core";import { verticalListSortingStrategy, type SortingStrategy } from "@dnd-kit/sortable";import { createSidebarCollisionDetection, createSidebarSortingStrategy, restrictBelowSidebarLabel,} from "./Sidebar.drag";import { resolveSidebarDropTarget, sidebarListItemId, sidebarMarkerId, type SidebarListItem, type SidebarListMarker, type SidebarSection,} from "./Sidebar.logic"; const thread = (key: string, section: SidebarSection): SidebarListItem => ({ kind: "thread", key, section,});const marker = (marker: SidebarListMarker): SidebarListItem => ({ kind: "marker", marker });const pinnedHeader = marker("pinned-header");const divider = marker("pinned-divider");const settledHeader = marker("settled-header");const stationary = { x: 0, y: 0, scaleX: 1, scaleY: 1 }; function layout( items: readonly SidebarListItem[], active: string, over: string, scale = 1, cardHeight = 82,) { let top = 100; const rects = items.map((item) => { const height = item.kind === "thread" ? (item.section === "pinned" || item.section === "active" ? cardHeight : 36) * scale : item.marker === "pinned-header" || item.marker === "pinned-divider" ? 0 : (item.marker.endsWith("placeholder") ? 0 : 32) * scale; const rect = { top, height, bottom: top + height, left: 0, right: 260, width: 260 }; top += height + 1; return rect; }); const activeIndex = items.findIndex((item) => sidebarListItemId(item) === active); return { activeIndex, overIndex: items.findIndex((item) => sidebarListItemId(item) === over), activeNodeRect: rects[activeIndex]!, rects, index: 0, } satisfies Parameters<SortingStrategy>[0];} function preview( input: Parameters<typeof createSidebarSortingStrategy>[0], active: string, over: string, scale = 1,) { const strategy = createSidebarSortingStrategy(input); const args = layout(input.items, active, over, scale); return new Map( input.items.map((item, index) => [sidebarListItemId(item), strategy({ ...args, index })]), );} describe("sidebar collision detection", () => { function collisionArgs(blockedAboveSource = false) { const rows = [thread("source", "active"), thread("blocked", "active")]; const items = [ pinnedHeader, divider, ...(blockedAboveSource ? rows.toReversed() : rows), settledHeader, marker("settled-placeholder"), ]; const { rects, activeIndex, overIndex } = layout(items, "source", "blocked"); const collisionRect = rects[overIndex]!; return { active: { id: "source", data: { current: {} }, rect: { current: { initial: rects[activeIndex]!, translated: collisionRect } }, }, collisionRect, droppableRects: new Map(items.map((item, index) => [sidebarListItemId(item), rects[index]!])), droppableContainers: items.map((item, index) => ({ id: sidebarListItemId(item), key: sidebarListItemId(item), disabled: false, data: { current: {} }, node: { current: null }, rect: { current: rects[index]! }, })), pointerCoordinates: null, } satisfies Parameters<CollisionDetection>[0]; } it.each([ [false, sidebarMarkerId("settled-header")], [true, sidebarMarkerId("pinned-divider")], ] as const)( "rejects unsupported Active instead of selecting %s / %s", (blockedAboveSource, nearbyTarget) => { const args = collisionArgs(blockedAboveSource); const detector = createSidebarCollisionDetection((id) => id !== "blocked"); const filtered = closestCenter({ ...args, droppableContainers: args.droppableContainers.filter( (container) => container.id !== "blocked", ), }); expect(filtered[0]?.id).toBe(nearbyTarget); expect(detector(args).map((collision) => collision.id)).toEqual(["source"]); }, ); it("selects the nearest supported target", () => { const detector = createSidebarCollisionDetection(() => true); expect(detector(collisionArgs())[0]?.id).toBe("blocked"); }); it.each([ { sourceSection: "active", pins: 0 }, { sourceSection: "active", pins: 1 }, { sourceSection: "pinned", pins: 1 }, { sourceSection: "settled", pins: 1 }, ] as const)( "switches on crossing the divider row from $sourceSection with $pins pins", ({ sourceSection, pins }) => { const items = [ pinnedHeader, ...(pins ? [thread("p", "pinned")] : []), ...(sourceSection === "pinned" ? [thread("source", "pinned")] : []), divider, thread("a", "active"), ...(sourceSection === "active" ? [thread("source", "active")] : []), settledHeader, ...(sourceSection === "settled" ? [thread("source", "settled")] : []), ]; const { rects, activeIndex } = layout(items, "source", "a"); const sourceRect = rects[activeIndex]!; let boundaryTop = 300; const boundaryNode = { querySelector: () => ({ getBoundingClientRect: () => ({ top: boundaryTop, bottom: boundaryTop + 16, left: 0, right: 260, }), }), } as unknown as HTMLElement; const detector = createSidebarCollisionDetection(() => true, { items, activationY: sourceSection === "pinned" ? 200 : 600, }); const at = (center: number) => { const collisionRect = { ...sourceRect, top: center - sourceRect.height / 2, bottom: center + sourceRect.height / 2, }; const args = { ...collisionArgs(), active: { id: "source", data: { current: {} }, rect: { current: { initial: sourceRect, translated: collisionRect } }, }, collisionRect, pointerCoordinates: { x: 130, y: center }, droppableRects: new Map( items.map((item, index) => [sidebarListItemId(item), rects[index]!]), ), droppableContainers: items.map((item, index) => ({ id: sidebarListItemId(item), key: sidebarListItemId(item), disabled: false, data: { current: {} }, node: { current: item === divider ? boundaryNode : item === settledHeader ? ({ getBoundingClientRect: () => ({ top: 600 }) } as unknown as HTMLElement) : null, }, rect: { current: rects[index]! }, })), }; const over = detector(args)[0]; return over ? resolveSidebarDropTarget(items, "source", String(over.id))?.section : null; }; expect(at(330)).toBe("active"); expect(at(317)).toBe("active"); expect(at(316)).toBe("pinned"); // The preview moves the divider; a stationary pointer must not undo the drop target. boundaryTop = 400; expect(at(316)).toBe("pinned"); expect(at(399)).toBe("pinned"); expect(at(400)).toBe("active"); boundaryTop = 300; expect(at(400)).toBe("active"); expect(at(317)).toBe("active"); expect(at(316)).toBe("pinned"); }, ); it("returns no collision if an unsupported target has no source fallback", () => { const args = collisionArgs(); const detector = createSidebarCollisionDetection(() => false); expect( detector({ ...args, droppableContainers: args.droppableContainers.filter( (container) => container.id !== "source", ), }), ).toEqual([]); }); it("validates each hovered target once and always allows returning to the source", () => { const args = collisionArgs(); const isValid = vi.fn((id: string) => id !== "blocked"); const detector = createSidebarCollisionDetection(isValid); expect(detector(args)[0]?.id).toBe("source"); expect( detector({ ...args, collisionRect: { ...args.collisionRect, top: args.collisionRect.top + 3, bottom: args.collisionRect.bottom + 3, }, })[0]?.id, ).toBe("source"); expect(detector({ ...args, collisionRect: args.droppableRects.get("source")! })[0]?.id).toBe( "source", ); expect( detector({ ...args, collisionRect: args.droppableRects.get(sidebarMarkerId("settled-placeholder"))!, })[0]?.id, ).toBe(sidebarMarkerId("settled-placeholder")); expect(isValid.mock.calls).toEqual([["blocked"], [sidebarMarkerId("settled-placeholder")]]); });}); describe("sidebar drag projection", () => { const pinned = [ pinnedHeader, thread("p1", "pinned"), thread("p2", "pinned"), divider, thread("a1", "active"), settledHeader, thread("s1", "settled"), ]; it.each([ ["p1", "p2"], ["p2", "p1"], ])("preserves existing pinned transforms from %s to %s", (active, over) => { const strategy = createSidebarSortingStrategy({ items: pinned, settledOrder: [], settledExpanded: true, }); const args = layout(pinned, active, over); // dnd-kit moves the lifted row by the pointer delta, so only peers matter. for (let index = 0; index < pinned.length; index += 1) { if (index === args.activeIndex) continue; expect(strategy({ ...args, index })).toEqual(verticalListSortingStrategy({ ...args, index })); } }); it("keeps the pinned header above the gap when a lower pin moves to the top", () => { const result = preview( { items: pinned, settledOrder: [], settledExpanded: true }, "p2", sidebarMarkerId("pinned-header"), ); expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary); expect(result.get("p1")).toEqual({ ...stationary, y: 83 }); expect(result.get(sidebarMarkerId("pinned-divider"))).toEqual(stationary); expect(result.get("a1")).toEqual(stationary); }); it.each([ ["a1", "a2"], ["a2", "a1"], ])("uses pinned dragging behavior for Active from %s to %s", (active, over) => { const items = [ pinnedHeader, thread("p", "pinned"), divider, thread("a1", "active"), thread("a2", "active"), settledHeader, thread("s", "settled"), ]; const strategy = createSidebarSortingStrategy({ items, settledOrder: [], settledExpanded: true, }); const args = layout(items, active, over); for (let index = 0; index < items.length; index += 1) { if (index === args.activeIndex) continue; expect(strategy({ ...args, index })).toEqual(verticalListSortingStrategy({ ...args, index })); } }); it("preserves settled order while opening the zero-height Active target", () => { const items = [ pinnedHeader, divider, marker("active-placeholder"), settledHeader, thread("first", "settled"), thread("second", "settled"), ]; const result = preview( { items, settledOrder: ["first", "second"], settledExpanded: true }, "second", "first", ); expect(result.get(sidebarMarkerId("active-placeholder"))).toEqual(stationary); expect(result.get(sidebarMarkerId("settled-header"))).toEqual({ ...stationary, y: 36 }); expect(result.get("first")).toEqual({ ...stationary, y: 36 }); expect(result.get("second")).toEqual(stationary); }); it.each([ [sidebarMarkerId("pinned-divider"), 0, 0], ["a1", -83, 0], ["a2", -83, -83], ] as const)( "opens the active pointer slot over %s without adding an empty pinned row", (over, a1Offset, a2Offset) => { const items = [ pinnedHeader, thread("p", "pinned"), divider, thread("a1", "active"), thread("a2", "active"), settledHeader, thread("s", "settled"), ]; const result = preview({ items, settledOrder: [], settledExpanded: true }, "p", over); expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(-83); expect(result.get("a1")?.y).toBe(a1Offset); expect(result.get("a2")?.y).toBe(a2Offset); expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(0); }, ); it("opens label space below each pinned boundary while dragging", () => { const items = [ pinnedHeader, thread("p", "pinned"), divider, thread("a1", "active"), thread("a2", "active"), settledHeader, thread("s", "settled"), ]; // Reorder inside active: the header gap shifts every row, the divider // gap shifts the active rows and the shelf below by a second label. const result = preview( { items, settledOrder: [], settledExpanded: true, boundaryLabelHeight: 16 }, "a2", "a1", ); expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary); expect(result.get("p")?.y).toBe(16); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(16); expect(result.get("a2")).toEqual(stationary); expect(result.get("a1")?.y).toBe(32 + 83); expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(32); expect(result.get("s")?.y).toBe(32); }); it.each(["s1", "missing-target"])( "keeps label clearance when a settled drag is over %s", (over) => { const items = [ pinnedHeader, thread("p", "pinned"), divider, thread("a", "active"), settledHeader, thread("s1", "settled"), thread("s2", "settled"), ]; const result = preview( { items, settledOrder: ["s1", "s2"], settledExpanded: true, boundaryLabelHeight: 24, }, "s2", over, ); expect(result.get("p")?.y).toBe(24); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(24); expect(result.get("a")?.y).toBe(48); expect(result.get("s1")?.y).toBe(48); expect(result.get("s2")).toEqual(stationary); }, ); it("stacks the labels with their gaps when the pinned section is empty", () => { const items = [ pinnedHeader, divider, thread("a1", "active"), thread("a2", "active"), settledHeader, thread("s", "settled"), ]; const result = preview( { items, settledOrder: [], settledExpanded: true, boundaryLabelHeight: 16 }, "a2", "a1", ); expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(16); expect(result.get("a1")?.y).toBe(32 + 83); expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(32); }); it("scales the label space with the measured root scale", () => { const items = [pinnedHeader, thread("p", "pinned"), divider, thread("a1", "active")]; const result = preview( { items, settledOrder: [], settledExpanded: true, boundaryLabelHeight: 16 }, "a1", "p", 2, ); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(32 + 165); }); it("keeps the pinned header above the first arriving pin", () => { const items = [ pinnedHeader, divider, thread("a1", "active"), thread("a2", "active"), settledHeader, thread("s", "settled"), ]; const result = preview( { items, settledOrder: [], settledExpanded: true }, "a2", sidebarMarkerId("pinned-header"), ); expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(83); expect(result.get("a1")?.y).toBe(83); expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(0); }); it.each([ ["p", -83, -1], ["s", 0, 82], ] as const)( "replaces the empty Active target when %s enters", (active, dividerOffset, settledOffset) => { const items = [ pinnedHeader, thread("p", "pinned"), divider, marker("active-placeholder"), settledHeader, thread("s", "settled"), ]; const result = preview( { items, settledOrder: [], settledExpanded: true }, active, sidebarMarkerId("active-placeholder"), ); expect(result.get(sidebarMarkerId("active-placeholder"))?.scaleY).toBe(0); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(dividerOffset); expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(settledOffset); }, ); it("uses the canonical settled rank and the destination's slim height", () => { const items = [ pinnedHeader, thread("p", "pinned"),Lines 1–500 of 806. Load more
import { describe, expect, it, vi } from "vite-plus/test"; import { closestCenter, type CollisionDetection } from "@dnd-kit/core"; import { verticalListSortingStrategy, type SortingStrategy } from "@dnd-kit/sortable"; import { createSidebarCollisionDetection, createSidebarSortingStrategy, restrictBelowSidebarLabel, } from "./Sidebar.drag"; import { resolveSidebarDropTarget, sidebarListItemId, sidebarMarkerId, type SidebarListItem, type SidebarListMarker, type SidebarSection, } from "./Sidebar.logic"; const thread = (key: string, section: SidebarSection): SidebarListItem => ({ kind: "thread", key, section, }); const marker = (marker: SidebarListMarker): SidebarListItem => ({ kind: "marker", marker }); const pinnedHeader = marker("pinned-header"); const divider = marker("pinned-divider"); const settledHeader = marker("settled-header"); const stationary = { x: 0, y: 0, scaleX: 1, scaleY: 1 }; function layout( items: readonly SidebarListItem[], active: string, over: string, scale = 1, cardHeight = 82, ) { let top = 100; const rects = items.map((item) => { const height = item.kind === "thread" ? (item.section === "pinned" || item.section === "active" ? cardHeight : 36) * scale : item.marker === "pinned-header" || item.marker === "pinned-divider" ? 0 : (item.marker.endsWith("placeholder") ? 0 : 32) * scale; const rect = { top, height, bottom: top + height, left: 0, right: 260, width: 260 }; top += height + 1; return rect; }); const activeIndex = items.findIndex((item) => sidebarListItemId(item) === active); return { activeIndex, overIndex: items.findIndex((item) => sidebarListItemId(item) === over), activeNodeRect: rects[activeIndex]!, rects, index: 0, } satisfies Parameters<SortingStrategy>[0]; } function preview( input: Parameters<typeof createSidebarSortingStrategy>[0], active: string, over: string, scale = 1, ) { const strategy = createSidebarSortingStrategy(input); const args = layout(input.items, active, over, scale); return new Map( input.items.map((item, index) => [sidebarListItemId(item), strategy({ ...args, index })]), ); } describe("sidebar collision detection", () => { function collisionArgs(blockedAboveSource = false) { const rows = [thread("source", "active"), thread("blocked", "active")]; const items = [ pinnedHeader, divider, ...(blockedAboveSource ? rows.toReversed() : rows), settledHeader, marker("settled-placeholder"), ]; const { rects, activeIndex, overIndex } = layout(items, "source", "blocked"); const collisionRect = rects[overIndex]!; return { active: { id: "source", data: { current: {} }, rect: { current: { initial: rects[activeIndex]!, translated: collisionRect } }, }, collisionRect, droppableRects: new Map(items.map((item, index) => [sidebarListItemId(item), rects[index]!])), droppableContainers: items.map((item, index) => ({ id: sidebarListItemId(item), key: sidebarListItemId(item), disabled: false, data: { current: {} }, node: { current: null }, rect: { current: rects[index]! }, })), pointerCoordinates: null, } satisfies Parameters<CollisionDetection>[0]; } it.each([ [false, sidebarMarkerId("settled-header")], [true, sidebarMarkerId("pinned-divider")], ] as const)( "rejects unsupported Active instead of selecting %s / %s", (blockedAboveSource, nearbyTarget) => { const args = collisionArgs(blockedAboveSource); const detector = createSidebarCollisionDetection((id) => id !== "blocked"); const filtered = closestCenter({ ...args, droppableContainers: args.droppableContainers.filter( (container) => container.id !== "blocked", ), }); expect(filtered[0]?.id).toBe(nearbyTarget); expect(detector(args).map((collision) => collision.id)).toEqual(["source"]); }, ); it("selects the nearest supported target", () => { const detector = createSidebarCollisionDetection(() => true); expect(detector(collisionArgs())[0]?.id).toBe("blocked"); }); it.each([ { sourceSection: "active", pins: 0 }, { sourceSection: "active", pins: 1 }, { sourceSection: "pinned", pins: 1 }, { sourceSection: "settled", pins: 1 }, ] as const)( "switches on crossing the divider row from $sourceSection with $pins pins", ({ sourceSection, pins }) => { const items = [ pinnedHeader, ...(pins ? [thread("p", "pinned")] : []), ...(sourceSection === "pinned" ? [thread("source", "pinned")] : []), divider, thread("a", "active"), ...(sourceSection === "active" ? [thread("source", "active")] : []), settledHeader, ...(sourceSection === "settled" ? [thread("source", "settled")] : []), ]; const { rects, activeIndex } = layout(items, "source", "a"); const sourceRect = rects[activeIndex]!; let boundaryTop = 300; const boundaryNode = { querySelector: () => ({ getBoundingClientRect: () => ({ top: boundaryTop, bottom: boundaryTop + 16, left: 0, right: 260, }), }), } as unknown as HTMLElement; const detector = createSidebarCollisionDetection(() => true, { items, activationY: sourceSection === "pinned" ? 200 : 600, }); const at = (center: number) => { const collisionRect = { ...sourceRect, top: center - sourceRect.height / 2, bottom: center + sourceRect.height / 2, }; const args = { ...collisionArgs(), active: { id: "source", data: { current: {} }, rect: { current: { initial: sourceRect, translated: collisionRect } }, }, collisionRect, pointerCoordinates: { x: 130, y: center }, droppableRects: new Map( items.map((item, index) => [sidebarListItemId(item), rects[index]!]), ), droppableContainers: items.map((item, index) => ({ id: sidebarListItemId(item), key: sidebarListItemId(item), disabled: false, data: { current: {} }, node: { current: item === divider ? boundaryNode : item === settledHeader ? ({ getBoundingClientRect: () => ({ top: 600 }) } as unknown as HTMLElement) : null, }, rect: { current: rects[index]! }, })), }; const over = detector(args)[0]; return over ? resolveSidebarDropTarget(items, "source", String(over.id))?.section : null; }; expect(at(330)).toBe("active"); expect(at(317)).toBe("active"); expect(at(316)).toBe("pinned"); // The preview moves the divider; a stationary pointer must not undo the drop target. boundaryTop = 400; expect(at(316)).toBe("pinned"); expect(at(399)).toBe("pinned"); expect(at(400)).toBe("active"); boundaryTop = 300; expect(at(400)).toBe("active"); expect(at(317)).toBe("active"); expect(at(316)).toBe("pinned"); }, ); it("returns no collision if an unsupported target has no source fallback", () => { const args = collisionArgs(); const detector = createSidebarCollisionDetection(() => false); expect( detector({ ...args, droppableContainers: args.droppableContainers.filter( (container) => container.id !== "source", ), }), ).toEqual([]); }); it("validates each hovered target once and always allows returning to the source", () => { const args = collisionArgs(); const isValid = vi.fn((id: string) => id !== "blocked"); const detector = createSidebarCollisionDetection(isValid); expect(detector(args)[0]?.id).toBe("source"); expect( detector({ ...args, collisionRect: { ...args.collisionRect, top: args.collisionRect.top + 3, bottom: args.collisionRect.bottom + 3, }, })[0]?.id, ).toBe("source"); expect(detector({ ...args, collisionRect: args.droppableRects.get("source")! })[0]?.id).toBe( "source", ); expect( detector({ ...args, collisionRect: args.droppableRects.get(sidebarMarkerId("settled-placeholder"))!, })[0]?.id, ).toBe(sidebarMarkerId("settled-placeholder")); expect(isValid.mock.calls).toEqual([["blocked"], [sidebarMarkerId("settled-placeholder")]]); }); }); describe("sidebar drag projection", () => { const pinned = [ pinnedHeader, thread("p1", "pinned"), thread("p2", "pinned"), divider, thread("a1", "active"), settledHeader, thread("s1", "settled"), ]; it.each([ ["p1", "p2"], ["p2", "p1"], ])("preserves existing pinned transforms from %s to %s", (active, over) => { const strategy = createSidebarSortingStrategy({ items: pinned, settledOrder: [], settledExpanded: true, }); const args = layout(pinned, active, over); // dnd-kit moves the lifted row by the pointer delta, so only peers matter. for (let index = 0; index < pinned.length; index += 1) { if (index === args.activeIndex) continue; expect(strategy({ ...args, index })).toEqual(verticalListSortingStrategy({ ...args, index })); } }); it("keeps the pinned header above the gap when a lower pin moves to the top", () => { const result = preview( { items: pinned, settledOrder: [], settledExpanded: true }, "p2", sidebarMarkerId("pinned-header"), ); expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary); expect(result.get("p1")).toEqual({ ...stationary, y: 83 }); expect(result.get(sidebarMarkerId("pinned-divider"))).toEqual(stationary); expect(result.get("a1")).toEqual(stationary); }); it.each([ ["a1", "a2"], ["a2", "a1"], ])("uses pinned dragging behavior for Active from %s to %s", (active, over) => { const items = [ pinnedHeader, thread("p", "pinned"), divider, thread("a1", "active"), thread("a2", "active"), settledHeader, thread("s", "settled"), ]; const strategy = createSidebarSortingStrategy({ items, settledOrder: [], settledExpanded: true, }); const args = layout(items, active, over); for (let index = 0; index < items.length; index += 1) { if (index === args.activeIndex) continue; expect(strategy({ ...args, index })).toEqual(verticalListSortingStrategy({ ...args, index })); } }); it("preserves settled order while opening the zero-height Active target", () => { const items = [ pinnedHeader, divider, marker("active-placeholder"), settledHeader, thread("first", "settled"), thread("second", "settled"), ]; const result = preview( { items, settledOrder: ["first", "second"], settledExpanded: true }, "second", "first", ); expect(result.get(sidebarMarkerId("active-placeholder"))).toEqual(stationary); expect(result.get(sidebarMarkerId("settled-header"))).toEqual({ ...stationary, y: 36 }); expect(result.get("first")).toEqual({ ...stationary, y: 36 }); expect(result.get("second")).toEqual(stationary); }); it.each([ [sidebarMarkerId("pinned-divider"), 0, 0], ["a1", -83, 0], ["a2", -83, -83], ] as const)( "opens the active pointer slot over %s without adding an empty pinned row", (over, a1Offset, a2Offset) => { const items = [ pinnedHeader, thread("p", "pinned"), divider, thread("a1", "active"), thread("a2", "active"), settledHeader, thread("s", "settled"), ]; const result = preview({ items, settledOrder: [], settledExpanded: true }, "p", over); expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(-83); expect(result.get("a1")?.y).toBe(a1Offset); expect(result.get("a2")?.y).toBe(a2Offset); expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(0); }, ); it("opens label space below each pinned boundary while dragging", () => { const items = [ pinnedHeader, thread("p", "pinned"), divider, thread("a1", "active"), thread("a2", "active"), settledHeader, thread("s", "settled"), ]; // Reorder inside active: the header gap shifts every row, the divider // gap shifts the active rows and the shelf below by a second label. const result = preview( { items, settledOrder: [], settledExpanded: true, boundaryLabelHeight: 16 }, "a2", "a1", ); expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary); expect(result.get("p")?.y).toBe(16); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(16); expect(result.get("a2")).toEqual(stationary); expect(result.get("a1")?.y).toBe(32 + 83); expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(32); expect(result.get("s")?.y).toBe(32); }); it.each(["s1", "missing-target"])( "keeps label clearance when a settled drag is over %s", (over) => { const items = [ pinnedHeader, thread("p", "pinned"), divider, thread("a", "active"), settledHeader, thread("s1", "settled"), thread("s2", "settled"), ]; const result = preview( { items, settledOrder: ["s1", "s2"], settledExpanded: true, boundaryLabelHeight: 24, }, "s2", over, ); expect(result.get("p")?.y).toBe(24); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(24); expect(result.get("a")?.y).toBe(48); expect(result.get("s1")?.y).toBe(48); expect(result.get("s2")).toEqual(stationary); }, ); it("stacks the labels with their gaps when the pinned section is empty", () => { const items = [ pinnedHeader, divider, thread("a1", "active"), thread("a2", "active"), settledHeader, thread("s", "settled"), ]; const result = preview( { items, settledOrder: [], settledExpanded: true, boundaryLabelHeight: 16 }, "a2", "a1", ); expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(16); expect(result.get("a1")?.y).toBe(32 + 83); expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(32); }); it("scales the label space with the measured root scale", () => { const items = [pinnedHeader, thread("p", "pinned"), divider, thread("a1", "active")]; const result = preview( { items, settledOrder: [], settledExpanded: true, boundaryLabelHeight: 16 }, "a1", "p", 2, ); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(32 + 165); }); it("keeps the pinned header above the first arriving pin", () => { const items = [ pinnedHeader, divider, thread("a1", "active"), thread("a2", "active"), settledHeader, thread("s", "settled"), ]; const result = preview( { items, settledOrder: [], settledExpanded: true }, "a2", sidebarMarkerId("pinned-header"), ); expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(83); expect(result.get("a1")?.y).toBe(83); expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(0); }); it.each([ ["p", -83, -1], ["s", 0, 82], ] as const)( "replaces the empty Active target when %s enters", (active, dividerOffset, settledOffset) => { const items = [ pinnedHeader, thread("p", "pinned"), divider, marker("active-placeholder"), settledHeader, thread("s", "settled"), ]; const result = preview( { items, settledOrder: [], settledExpanded: true }, active, sidebarMarkerId("active-placeholder"), ); expect(result.get(sidebarMarkerId("active-placeholder"))?.scaleY).toBe(0); expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(dividerOffset); expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(settledOffset); }, ); it("uses the canonical settled rank and the destination's slim height", () => { const items = [ pinnedHeader, thread("p", "pinned"),