1import { describe, expect, it } from "vite-plus/test";
2import { closestCenter, type CollisionDetection } from "@dnd-kit/core";
3import type { SortingStrategy } from "@dnd-kit/sortable";
4import {
5 createSidebarCollisionDetection,
6 createSidebarSortingStrategy,
7 restrictBelowSidebarLabel,
8} from "./Sidebar.drag";
9import {
10 resolveSidebarDropTarget,
11 sidebarListItemId,
12 sidebarMarkerId,
13 type SidebarListItem,
14 type SidebarListMarker,
15 type SidebarSection,
16} from "./Sidebar.logic";
17
18const thread = (key: string, section: SidebarSection): SidebarListItem => ({
19 kind: "thread",
20 key,
21 section,
22});
23const marker = (marker: SidebarListMarker): SidebarListItem => ({ kind: "marker", marker });
24const pinnedHeader = marker("pinned-header");
25const divider = marker("pinned-divider");
26const settledHeader = marker("settled-header");
27const stationary = { x: 0, y: 0, scaleX: 1, scaleY: 1 };
28
29function layout(
30 items: readonly SidebarListItem[],
31 active: string,
32 over: string,
33 scale = 1,
34 cardHeight = 82,
35) {
36 let top = 100;
37 const rects = items.map((item) => {
38 const height =
39 item.kind === "thread"
40 ? (item.section === "pinned" || item.section === "active" ? cardHeight : 36) * scale
41 : item.marker === "pinned-header" || item.marker === "pinned-divider"
42 ? 0
43 : (item.marker.endsWith("placeholder") ? 0 : 32) * scale;
44 const rect = { top, height, bottom: top + height, left: 0, right: 260, width: 260 };
45 top += height + 1;
46 return rect;
47 });
48 const activeIndex = items.findIndex((item) => sidebarListItemId(item) === active);
49 return {
50 activeIndex,
51 overIndex: items.findIndex((item) => sidebarListItemId(item) === over),
52 activeNodeRect: rects[activeIndex]!,
53 rects,
54 index: 0,
55 } satisfies Parameters<SortingStrategy>[0];
56}
57
58function preview(
59 input: Parameters<typeof createSidebarSortingStrategy>[0],
60 active: string,
61 over: string,
62 scale = 1,
63) {
64 const strategy = createSidebarSortingStrategy(input);
65 const args = layout(input.items, active, over, scale);
66 return new Map(
67 input.items.map((item, index) => [sidebarListItemId(item), strategy({ ...args, index })]),
68 );
69}
70
71describe("sidebar collision detection", () => {
72 function collisionArgs(blockedAboveSource = false) {
73 const rows = [thread("source", "active"), thread("blocked", "active")];
74 const items = [
75 pinnedHeader,
76 divider,
77 ...(blockedAboveSource ? rows.toReversed() : rows),
78 settledHeader,
79 marker("settled-placeholder"),
80 ];
81 const { rects, activeIndex, overIndex } = layout(items, "source", "blocked");
82 const collisionRect = rects[overIndex]!;
83 return {
84 active: {
85 id: "source",
86 data: { current: {} },
87 rect: { current: { initial: rects[activeIndex]!, translated: collisionRect } },
88 },
89 collisionRect,
90 droppableRects: new Map(items.map((item, index) => [sidebarListItemId(item), rects[index]!])),
91 droppableContainers: items.map((item, index) => ({
92 id: sidebarListItemId(item),
93 key: sidebarListItemId(item),
94 disabled: false,
95 data: { current: {} },
96 node: { current: null },
97 rect: { current: rects[index]! },
98 })),
99 pointerCoordinates: null,
100 } satisfies Parameters<CollisionDetection>[0];
101 }
102
103 it.each([
104 [false, sidebarMarkerId("settled-header")],
105 [true, sidebarMarkerId("pinned-divider")],
106 ] as const)(
107 "rejects unsupported Active instead of selecting %s / %s",
108 (blockedAboveSource, nearbyTarget) => {
109 const args = collisionArgs(blockedAboveSource);
110 const detector = createSidebarCollisionDetection((id) => id !== "blocked");
111 const filtered = closestCenter({
112 ...args,
113 droppableContainers: args.droppableContainers.filter(
114 (container) => container.id !== "blocked",
115 ),
116 });
117 expect(filtered[0]?.id).toBe(nearbyTarget);
118 expect(detector(args).map((collision) => collision.id)).toEqual(["source"]);
119 },
120 );
121
122 it.each([
123 { sourceSection: "active", pins: 0 },
124 { sourceSection: "active", pins: 1 },
125 { sourceSection: "pinned", pins: 1 },
126 { sourceSection: "settled", pins: 1 },
127 ] as const)(
128 "switches on crossing the divider row from $sourceSection with $pins pins",
129 ({ sourceSection, pins }) => {
130 const items = [
131 pinnedHeader,
132 ...(pins ? [thread("p", "pinned")] : []),
133 ...(sourceSection === "pinned" ? [thread("source", "pinned")] : []),
134 divider,
135 thread("a", "active"),
136 ...(sourceSection === "active" ? [thread("source", "active")] : []),
137 settledHeader,
138 ...(sourceSection === "settled" ? [thread("source", "settled")] : []),
139 ];
140 const { rects, activeIndex } = layout(items, "source", "a");
141 const sourceRect = rects[activeIndex]!;
142 let boundaryTop = 300;
143 const boundaryNode = {
144 querySelector: () => ({
145 getBoundingClientRect: () => ({
146 top: boundaryTop,
147 bottom: boundaryTop + 16,
148 left: 0,
149 right: 260,
150 }),
151 }),
152 } as unknown as HTMLElement;
153 const detector = createSidebarCollisionDetection(() => true, {
154 items,
155 activationY: sourceSection === "pinned" ? 200 : 600,
156 });
157 const at = (center: number) => {
158 const collisionRect = {
159 ...sourceRect,
160 top: center - sourceRect.height / 2,
161 bottom: center + sourceRect.height / 2,
162 };
163 const args = {
164 ...collisionArgs(),
165 active: {
166 id: "source",
167 data: { current: {} },
168 rect: { current: { initial: sourceRect, translated: collisionRect } },
169 },
170 collisionRect,
171 pointerCoordinates: { x: 130, y: center },
172 droppableRects: new Map(
173 items.map((item, index) => [sidebarListItemId(item), rects[index]!]),
174 ),
175 droppableContainers: items.map((item, index) => ({
176 id: sidebarListItemId(item),
177 key: sidebarListItemId(item),
178 disabled: false,
179 data: { current: {} },
180 node: {
181 current:
182 item === divider
183 ? boundaryNode
184 : item === settledHeader
185 ? ({ getBoundingClientRect: () => ({ top: 600 }) } as unknown as HTMLElement)
186 : null,
187 },
188 rect: { current: rects[index]! },
189 })),
190 };
191 const over = detector(args)[0];
192 return over ? resolveSidebarDropTarget(items, "source", String(over.id))?.section : null;
193 };
194 expect(at(330)).toBe("active");
195 expect(at(317)).toBe("active");
196 expect(at(316)).toBe("pinned");
197 boundaryTop = 400;
198 expect(at(316)).toBe("pinned");
199 expect(at(399)).toBe("pinned");
200 expect(at(400)).toBe("active");
201 boundaryTop = 300;
202 expect(at(400)).toBe("active");
203 expect(at(317)).toBe("active");
204 expect(at(316)).toBe("pinned");
205 },
206 );
207
208 it("returns no collision if an unsupported target has no source fallback", () => {
209 const args = collisionArgs();
210 const detector = createSidebarCollisionDetection(() => false);
211 expect(
212 detector({
213 ...args,
214 droppableContainers: args.droppableContainers.filter(
215 (container) => container.id !== "source",
216 ),
217 }),
218 ).toEqual([]);
219 });
220});
221
222describe("sidebar drag projection", () => {
223 it.each([
224 ["a2", "a1"],
225 ["p", "a1"],
226 ["s", sidebarMarkerId("pinned-header")],
227 ["z", sidebarMarkerId("settled-header")],
228 ])("restores every row and marker when dragging %s out after hovering %s", (active, over) => {
229 const items = [
230 pinnedHeader,
231 thread("p", "pinned"),
232 divider,
233 marker("active-placeholder"),
234 thread("a1", "active"),
235 thread("a2", "active"),
236 marker("snoozed-header"),
237 thread("z", "snoozed"),
238 settledHeader,
239 marker("settled-placeholder"),
240 thread("s", "settled"),
241 ];
242 const input = {
243 items,
244 settledOrder: ["z", "s"],
245 settledExpanded: true,
246 boundaryLabelHeight: 24,
247 snoozedThreadCount: 1,
248 };
249 const reordered = preview(input, active, over);
250 expect([...reordered.values()].some((transform) => transform?.y !== 0)).toBe(true);
251
252 const restored = preview({ ...input, enabled: false }, active, over);
253 for (const transform of restored.values()) expect(transform).toEqual(stationary);
254
255 expect(preview({ ...input, enabled: true }, active, over)).toEqual(reordered);
256 });
257
258 it.each([
259 ["a1", "a2"],
260 ["a1", sidebarMarkerId("settled-header")],
261 ["z", "a2"],
262 ])("keeps sparse shelves at the bottom when dragging %s over %s", (active, over) => {
263 const items = [
264 pinnedHeader,
265 divider,
266 thread("a1", "active"),
267 thread("a2", "active"),
268 marker("snoozed-header"),
269 thread("z", "snoozed"),
270 settledHeader,
271 thread("s", "settled"),
272 ];
273 const strategy = createSidebarSortingStrategy({
274 items,
275 settledOrder: over === sidebarMarkerId("settled-header") ? ["a1", "s"] : ["s"],
276 settledExpanded: true,
277 boundaryLabelHeight: 24,
278 snoozedThreadCount: 1,
279 });
280 const args = layout(items, active, over);
281 for (const rect of args.rects.slice(4)) {
282 rect.top += 400;
283 rect.bottom += 400;
284 }
285 const lastIndex = items.length - 1;
286 expect(strategy({ ...args, index: lastIndex })).toEqual(stationary);
287 });
288
289 const pinned = [
290 pinnedHeader,
291 thread("p1", "pinned"),
292 thread("p2", "pinned"),
293 divider,
294 thread("a1", "active"),
295 settledHeader,
296 thread("s1", "settled"),
297 ];
298
299 it("keeps the pinned header above the gap when a lower pin moves to the top", () => {
300 const result = preview(
301 { items: pinned, settledOrder: [], settledExpanded: true },
302 "p2",
303 sidebarMarkerId("pinned-header"),
304 );
305 expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary);
306 expect(result.get("p1")).toEqual({ ...stationary, y: 83 });
307 expect(result.get(sidebarMarkerId("pinned-divider"))).toEqual(stationary);
308 expect(result.get("a1")).toEqual(stationary);
309 });
310
311 it("preserves settled order while opening the zero-height Active target", () => {
312 const items = [
313 pinnedHeader,
314 divider,
315 marker("active-placeholder"),
316 settledHeader,
317 thread("first", "settled"),
318 thread("second", "settled"),
319 ];
320 const result = preview(
321 { items, settledOrder: ["first", "second"], settledExpanded: true },
322 "second",
323 "first",
324 );
325 expect(result.get(sidebarMarkerId("active-placeholder"))).toEqual(stationary);
326 expect(result.get(sidebarMarkerId("settled-header"))).toEqual({ ...stationary, y: 36 });
327 expect(result.get("first")).toEqual({ ...stationary, y: 36 });
328 expect(result.get("second")).toEqual(stationary);
329 });
330
331 it.each([
332 [sidebarMarkerId("pinned-divider"), 0, 0],
333 ["a1", -83, 0],
334 ["a2", -83, -83],
335 ] as const)(
336 "opens the active pointer slot over %s without adding an empty pinned row",
337 (over, a1Offset, a2Offset) => {
338 const items = [
339 pinnedHeader,
340 thread("p", "pinned"),
341 divider,
342 thread("a1", "active"),
343 thread("a2", "active"),
344 settledHeader,
345 thread("s", "settled"),
346 ];
347 const result = preview({ items, settledOrder: [], settledExpanded: true }, "p", over);
348 expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary);
349 expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(-83);
350 expect(result.get("a1")?.y).toBe(a1Offset);
351 expect(result.get("a2")?.y).toBe(a2Offset);
352 expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(0);
353 },
354 );
355
356 it("opens label space below each pinned boundary while dragging", () => {
357 const items = [
358 pinnedHeader,
359 thread("p", "pinned"),
360 divider,
361 thread("a1", "active"),
362 thread("a2", "active"),
363 settledHeader,
364 thread("s", "settled"),
365 ];
366 const result = preview(
367 { items, settledOrder: [], settledExpanded: true, boundaryLabelHeight: 16 },
368 "a2",
369 "a1",
370 );
371 expect(result.get(sidebarMarkerId("pinned-header"))).toEqual(stationary);
372 expect(result.get("p")?.y).toBe(16);
373 expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(16);
374 expect(result.get("a2")).toEqual(stationary);
375 expect(result.get("a1")?.y).toBe(32 + 83);
376 expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(32);
377 expect(result.get("s")?.y).toBe(32);
378 });
379
380 it.each(["s1", "missing-target"])(
381 "keeps label clearance when a settled drag is over %s",
382 (over) => {
383 const items = [
384 pinnedHeader,
385 thread("p", "pinned"),
386 divider,
387 thread("a", "active"),
388 settledHeader,
389 thread("s1", "settled"),
390 thread("s2", "settled"),
391 ];
392 const result = preview(
393 {
394 items,
395 settledOrder: ["s1", "s2"],
396 settledExpanded: true,
397 boundaryLabelHeight: 24,
398 },
399 "s2",
400 over,
401 );
402 expect(result.get("p")?.y).toBe(24);
403 expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(24);
404 expect(result.get("a")?.y).toBe(48);
405 expect(result.get("s1")?.y).toBe(48);
406 expect(result.get("s2")).toEqual(stationary);
407 },
408 );
409
410 it.each([
411 ["p", -83, -1],
412 ["s", 0, 82],
413 ] as const)(
414 "replaces the empty Active target when %s enters",
415 (active, dividerOffset, settledOffset) => {
416 const items = [
417 pinnedHeader,
418 thread("p", "pinned"),
419 divider,
420 marker("active-placeholder"),
421 settledHeader,
422 thread("s", "settled"),
423 ];
424 const result = preview(
425 { items, settledOrder: [], settledExpanded: true },
426 active,
427 sidebarMarkerId("active-placeholder"),
428 );
429 expect(result.get(sidebarMarkerId("active-placeholder"))?.scaleY).toBe(0);
430 expect(result.get(sidebarMarkerId("pinned-divider"))?.y).toBe(dividerOffset);
431 expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(settledOffset);
432 },
433 );
434
435 it("uses the canonical settled rank and the destination's slim height", () => {
436 const items = [
437 pinnedHeader,
438 thread("p", "pinned"),
439 divider,
440 thread("a", "active"),
441 settledHeader,
442 thread("s1", "settled"),
443 thread("s2", "settled"),
444 ];
445 const result = preview(
446 { items, settledOrder: ["s1", "a", "s2"], settledExpanded: true },
447 "a",
448 "s2",
449 );
450 expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(-46);
451 expect(result.get("s1")?.y).toBe(-46);
452 expect(result.get("s2")?.y).toBe(-9);
453 });
454
455 it.each([
456 ["a1", 83],
457 ["a2", 0],
458 ] as const)(
459 "reserves a full card at the pointer slot over %s when a slim row enters Active",
460 (over, firstOffset) => {
461 const items = [
462 pinnedHeader,
463 thread("p", "pinned"),
464 divider,
465 thread("a1", "active"),
466 thread("a2", "active"),
467 settledHeader,
468 thread("s", "settled"),
469 ];
470 const result = preview({ items, settledOrder: [], settledExpanded: true }, "s", over);
471 expect(result.get("a1")?.y).toBe(firstOffset);
472 expect(result.get("a2")?.y).toBe(83);
473 expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(83);
474 },
475 );
476
477 it("removes the snoozed header when its last row leaves", () => {
478 const items = [
479 pinnedHeader,
480 thread("p", "pinned"),
481 divider,
482 thread("a", "active"),
483 marker("snoozed-header"),
484 thread("z", "snoozed"),
485 settledHeader,
486 thread("s", "settled"),
487 ];
488 const result = preview({ items, settledOrder: [], settledExpanded: true }, "z", "a");
489 expect(result.get(sidebarMarkerId("snoozed-header"))?.scaleY).toBe(0);
490 expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(13);
491 expect(result.get("s")?.y).toBe(13);
492 });
493
494 it("keeps a collapsed settled target without inserting a hidden row", () => {
495 const items = [
496 pinnedHeader,
497 thread("p", "pinned"),
498 divider,
499 thread("a1", "active"),
500 thread("a2", "active"),
501 settledHeader,
502 marker("settled-placeholder"),
503 ];
504 const result = preview(
505 { items, settledOrder: [], settledExpanded: false },
506 "a2",
507 sidebarMarkerId("settled-placeholder"),
508 );
509 expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(-83);
510 expect(result.get(sidebarMarkerId("settled-placeholder"))).toEqual({ ...stationary, y: -83 });
511 });
512
513 it("previews a time-ordered inbox drop at its time slot, above the Working shelf", () => {
514 const items = [
515 pinnedHeader,
516 thread("p", "pinned"),
517 divider,
518 thread("a1", "active"),
519 thread("a2", "active"),
520 marker("working-header"),
521 thread("w", "working"),
522 settledHeader,
523 marker("settled-placeholder"),
524 ];
525 const input = { items, settledOrder: [], settledExpanded: false };
526 expect(preview(input, "p", "a1").get("a2")?.y).toBe(0);
527 const byTime = preview({ ...input, activeOrder: ["a1", "a2", "p"] }, "p", "a1");
528 expect(byTime.get("a2")?.y).toBe(-83);
529 expect(byTime.get(sidebarMarkerId("working-header"))).toEqual(stationary);
530 expect(byTime.get("w")).toEqual(stationary);
531 });
532
533 it("updates the projection when the target or measured geometry changes", () => {
534 const strategy = createSidebarSortingStrategy({
535 items: pinned,
536 settledOrder: [],
537 settledExpanded: true,
538 });
539 const args = layout(pinned, "p1", "p1");
540 expect(strategy({ ...args, index: 2 })?.y).toBe(0);
541 expect(strategy({ ...args, index: 2, overIndex: 4 })?.y).toBe(-83);
542 const smaller = layout(pinned, "p1", "a1", 0.75);
543 expect(strategy({ ...smaller, index: 2 })?.y).toBe(-62.5);
544 });
545
546 it.each(["active", "settled"] as const)(
547 "reveals the mounted empty %s target when its last row leaves and hides it on return",
548 (section) => {
549 const items = [
550 pinnedHeader,
551 thread("p", "pinned"),
552 divider,
553 marker("active-placeholder"),
554 thread("a", "active"),
555 settledHeader,
556 marker("settled-placeholder"),
557 thread("s", "settled"),
558 ];
559 const active = section === "active" ? "a" : "s";
560 const input = { items, settledOrder: ["s"], settledExpanded: true };
561 const placeholderId = sidebarMarkerId(`${section}-placeholder`);
562 const resting = preview(input, active, active);
563 expect(resting.get(placeholderId)?.scaleY).toBe(0);
564 const leaving = preview(input, active, sidebarMarkerId("pinned-header"));
565 expect(leaving.get(placeholderId)?.scaleY).toBe(1);
566 const returning = preview(input, active, active);
567 expect(returning.get(placeholderId)?.scaleY).toBe(0);
568 },
569 );
570
571 it("uses shelf height for empty target sizing when card height differs from its default", () => {
572 const items = [
573 pinnedHeader,
574 thread("p", "pinned"),
575 divider,
576 thread("a", "active"),
577 settledHeader,
578 marker("settled-placeholder"),
579 ];
580 const strategy = createSidebarSortingStrategy({
581 items,
582 settledOrder: [],
583 settledExpanded: false,
584 });
585 const args = layout(items, "a", sidebarMarkerId("settled-placeholder"), 1, 78);
586 expect(strategy({ ...args, index: 4 })?.y).toBe(-42);
587 });
588
589 it("keeps the route row visible after a settled drop pushes it beyond the page", () => {
590 const items = [
591 pinnedHeader,
592 thread("p", "pinned"),
593 divider,
594 thread("a", "active"),
595 settledHeader,
596 thread("s", "settled"),
597 ];
598 const input = {
599 items,
600 settledOrder: ["a", "s", "hidden"],
601 settledExpanded: true,
602 settledVisibleCount: 1,
603 };
604 const withRoute = preview({ ...input, routeThreadKey: "s" }, "a", "s");
605 const withoutRoute = preview(input, "a", "s");
606 expect(withRoute.get("s")).toEqual({ ...stationary, y: -9 });
607 expect(withoutRoute.get("s")?.scaleY).toBe(0);
608 });
609
610 it("reserves the next page row when a visible settled thread leaves", () => {
611 const items = [
612 pinnedHeader,
613 thread("p", "pinned"),
614 divider,
615 thread("a", "active"),
616 settledHeader,
617 thread("s1", "settled"),
618 thread("route", "settled"),
619 ];
620 const result = preview(
621 {
622 items,
623 settledOrder: ["s1", "hidden", "route"],
624 settledExpanded: true,
625 settledVisibleCount: 1,
626 routeThreadKey: "route",
627 },
628 "s1",
629 "a",
630 );
631 expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(83);
632 expect(result.get("route")?.y).toBe(83);
633 });
634
635 it("keeps the dropped route thread visible in a collapsed settled shelf", () => {
636 const items = [
637 pinnedHeader,
638 thread("p", "pinned"),
639 divider,
640 thread("a", "active"),
641 settledHeader,
642 marker("settled-placeholder"),
643 ];
644 const result = preview(
645 {
646 items,
647 settledOrder: ["a", "hidden"],
648 settledExpanded: false,
649 settledVisibleCount: 1,
650 routeThreadKey: "a",
651 },
652 "a",
653 sidebarMarkerId("settled-placeholder"),
654 );
655 expect(result.get(sidebarMarkerId("settled-placeholder"))?.scaleY).toBe(0);
656 expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(-46);
657 });
658
659 it("preserves hidden snoozed membership when the only rendered route row leaves", () => {
660 const items = [
661 pinnedHeader,
662 thread("p", "pinned"),
663 divider,
664 thread("a", "active"),
665 marker("snoozed-header"),
666 thread("z", "snoozed"),
667 settledHeader,
668 thread("s", "settled"),
669 ];
670 const result = preview(
671 {
672 items,
673 settledOrder: ["s"],
674 settledExpanded: true,
675 snoozedThreadCount: 2,
676 },
677 "z",
678 "a",
679 );
680 expect(result.get(sidebarMarkerId("snoozed-header"))).toEqual({ ...stationary, y: 83 });
681 expect(result.get(sidebarMarkerId("settled-header"))?.y).toBe(46);
682 });
683});
684
685describe("lifted card clearance", () => {
686 const rect = (top: number, height: number) => ({
687 top,
688 bottom: top + height,
689 height,
690 left: 0,
691 right: 260,
692 width: 260,
693 });
694 const apply = (cardTop: number, cardHeight: number, y: number, listTop = 136, offset = 32) =>
695 restrictBelowSidebarLabel(
696 {
697 transform: { ...stationary, y },
698 containerNodeRect: rect(listTop, 500),
699 draggingNodeRect: rect(cardTop, cardHeight),
700 activatorEvent: null,
701 active: null,
702 activeNodeRect: null,
703 over: null,
704 overlayNodeRect: null,
705 scrollableAncestors: [],
706 scrollableAncestorRects: [],
707 windowRect: null,
708 },
709 offset,
710 );
711
712 it.each([36, 82])("keeps a %ipx row below empty Pins even past the top edge", (height) => {
713 for (const pointerY of [150, 136, 100, 0]) {
714 const transform = apply(511, height, pointerY - 529);
715 expect(511 + transform.y).toBe(168);
716 }
717 });
718
719 // regression: #10453 the label clamp must not swallow drags that are already below it
720 it("preserves pointer movement below the label", () => {
721 expect(apply(511, 36, -200).y).toBe(-200);
722 });
723
724 it("follows the list when it scrolls and includes content preceding Pins", () => {
725 expect(511 + apply(511, 36, -500, 96).y).toBe(128);
726 expect(511 + apply(511, 36, -500, 136, 114).y).toBe(250);
727 });
728});