1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 createComposerScrollGestureState,
5 recordComposerScrollGestureEvent,
6 resetComposerScrollGesture,
7 shouldCollapseComposerForScrollKey,
8 suppressActiveComposerScrollGesture,
9} from "./composerScrollGesture";
10
11const RESET_MS = 120;
12const THRESHOLD_PX = 24;
13
14describe("composer keyboard scroll collapse", () => {
15 const middle = {
16 scrollTop: 500,
17 scrollHeight: 1500,
18 clientHeight: 500,
19 isAtLogicalEnd: false,
20 };
21
22 it.each(["PageUp", "PageDown", "Home", "End"])("collapses on %s in the timeline", (key) => {
23 expect(shouldCollapseComposerForScrollKey({ ...middle, key })).toBe(true);
24 });
25
26 it.each(["PageUp", "Home"])("does not collapse on %s at the top", (key) => {
27 expect(shouldCollapseComposerForScrollKey({ ...middle, key, scrollTop: 0 })).toBe(false);
28 });
29
30 it.each(["PageDown", "End"])("does not collapse on %s at the logical end", (key) => {
31 expect(shouldCollapseComposerForScrollKey({ ...middle, key, isAtLogicalEnd: true })).toBe(
32 false,
33 );
34 expect(shouldCollapseComposerForScrollKey({ ...middle, key, scrollTop: 1000 })).toBe(false);
35 });
36
37 it("ignores unrelated keys", () => {
38 expect(shouldCollapseComposerForScrollKey({ ...middle, key: "Enter" })).toBe(false);
39 });
40});
41
42function record(
43 state: ReturnType<typeof createComposerScrollGestureState>,
44 now: number,
45 options: Partial<{
46 deltaPx: number;
47 collapseEligible: boolean;
48 canScrollInGestureDirection: boolean;
49 scrollsTowardLogicalEnd: boolean;
50 }> = {},
51) {
52 return recordComposerScrollGestureEvent(state, {
53 now,
54 deltaPx: options.deltaPx ?? 30,
55 collapseThresholdPx: THRESHOLD_PX,
56 collapseEligible: options.collapseEligible ?? true,
57 canScrollInGestureDirection: options.canScrollInGestureDirection ?? true,
58 scrollsTowardLogicalEnd: options.scrollsTowardLogicalEnd ?? false,
59 });
60}
61
62describe("composer scroll gesture", () => {
63 it("lets an editor change win over the rest of the active gesture", () => {
64 const state = createComposerScrollGestureState();
65
66 expect(record(state, 0)).toBe(true);
67 suppressActiveComposerScrollGesture(state, 20, RESET_MS);
68
69 expect(record(state, 40)).toBe(false);
70 expect(record(state, 80)).toBe(false);
71 expect(record(state, 160)).toBe(false);
72 });
73
74 it("preserves suppression while collapse is temporarily ineligible", () => {
75 const state = createComposerScrollGestureState();
76
77 record(state, 0);
78 suppressActiveComposerScrollGesture(state, 20, RESET_MS);
79
80 expect(record(state, 40, { collapseEligible: false })).toBe(false);
81 expect(record(state, 80, { collapseEligible: true })).toBe(false);
82 });
83
84 it("keeps a boundary momentum tail in the same suppressed gesture", () => {
85 const state = createComposerScrollGestureState();
86
87 record(state, 0);
88 suppressActiveComposerScrollGesture(state, 20, RESET_MS);
89
90 expect(record(state, 80, { canScrollInGestureDirection: false })).toBe(false);
91 expect(state.lastEventAt).toBe(80);
92 expect(record(state, 160)).toBe(false);
93 });
94
95 it("does not carry gesture state into a new thread after reset", () => {
96 const state = createComposerScrollGestureState();
97
98 record(state, 0);
99 suppressActiveComposerScrollGesture(state, 20, RESET_MS);
100 record(state, 80);
101 resetComposerScrollGesture(state);
102
103 expect(record(state, 240)).toBe(true);
104 });
105
106 it("accumulates small deltas only while collapse remains eligible and scrollable", () => {
107 const state = createComposerScrollGestureState();
108
109 expect(record(state, 0, { deltaPx: 10 })).toBe(false);
110 expect(record(state, 20, { deltaPx: 10 })).toBe(false);
111 expect(record(state, 40, { deltaPx: 4 })).toBe(true);
112 expect(record(state, 60, { deltaPx: 10, collapseEligible: false })).toBe(false);
113 expect(record(state, 80, { deltaPx: 14 })).toBe(false);
114 });
115
116 it("does not collapse while scrolling down through composer footer space", () => {
117 const state = createComposerScrollGestureState();
118
119 expect(record(state, 0, { deltaPx: 20 })).toBe(false);
120 expect(record(state, 20, { scrollsTowardLogicalEnd: true })).toBe(false);
121 expect(record(state, 40, { deltaPx: 10 })).toBe(false);
122 });
123});