1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
import { describe, expect, it } from "vitest";
import {
createSmartScrollState,
isNearBottom,
NEAR_BOTTOM_THRESHOLD,
onContentChange,
onReset,
onResume,
onScroll,
type ScrollGeometry,
} from "./smart-scroll";
// A viewport 100px tall over 1000px of content: scrollTop 900 == pinned to bottom.
const atBottom: ScrollGeometry = { scrollTop: 900, scrollHeight: 1000, clientHeight: 100 };
const nearBottom: ScrollGeometry = {
scrollTop: 900 - NEAR_BOTTOM_THRESHOLD,
scrollHeight: 1000,
clientHeight: 100,
};
const scrolledUp: ScrollGeometry = { scrollTop: 200, scrollHeight: 1000, clientHeight: 100 };
describe("isNearBottom", () => {
it("is true exactly at the bottom", () => {
expect(isNearBottom(atBottom)).toBe(true);
});
it("is true within the threshold of the bottom", () => {
expect(isNearBottom(nearBottom)).toBe(true);
});
it("is false just beyond the threshold", () => {
expect(
isNearBottom({
scrollTop: 900 - NEAR_BOTTOM_THRESHOLD - 1,
scrollHeight: 1000,
clientHeight: 100,
}),
).toBe(false);
});
it("is false when scrolled well up", () => {
expect(isNearBottom(scrolledUp)).toBe(false);
});
it("honours a custom threshold", () => {
const geom: ScrollGeometry = { scrollTop: 800, scrollHeight: 1000, clientHeight: 100 };
expect(isNearBottom(geom, 50)).toBe(false);
expect(isNearBottom(geom, 150)).toBe(true);
});
});
describe("smart-scroll reducer", () => {
it("starts stuck and hides the button", () => {
const s = createSmartScrollState();
expect(s.stuck).toBe(true);
});
it("onScroll up unsticks and shows the button, with no command", () => {
const r = onScroll(createSmartScrollState(), scrolledUp);
expect(r.state.stuck).toBe(false);
expect(r.showButton).toBe(true);
expect(r.command).toBeNull();
});
it("onScroll back to the bottom re-sticks and hides the button", () => {
const up = onScroll(createSmartScrollState(), scrolledUp).state;
const r = onScroll(up, atBottom);
expect(r.state.stuck).toBe(true);
expect(r.showButton).toBe(false);
expect(r.command).toBeNull();
});
it("onContentChange while stuck emits a NON-animated scroll (keep up with the stream)", () => {
const r = onContentChange(createSmartScrollState(), atBottom);
expect(r.command).toEqual({ kind: "scroll-to-bottom", animate: false });
expect(r.state.stuck).toBe(true);
});
it("onContentChange while unstuck emits NO command (leave the reader in place)", () => {
const up = onScroll(createSmartScrollState(), scrolledUp).state;
const r = onContentChange(up, scrolledUp);
expect(r.command).toBeNull();
expect(r.state.stuck).toBe(false);
expect(r.showButton).toBe(true);
});
it("onResume re-sticks and emits an ANIMATED scroll", () => {
const up = onScroll(createSmartScrollState(), scrolledUp).state;
const r = onResume(up);
expect(r.state.stuck).toBe(true);
expect(r.showButton).toBe(false);
expect(r.command).toEqual({ kind: "scroll-to-bottom", animate: true });
});
it("onReset returns to stuck and snaps (non-animated) to the bottom", () => {
const up = onScroll(createSmartScrollState(), scrolledUp).state;
const r = onReset();
void up;
expect(r.state.stuck).toBe(true);
expect(r.command).toEqual({ kind: "scroll-to-bottom", animate: false });
expect(r.showButton).toBe(false);
});
});
|