summaryrefslogtreecommitdiffhomepage
path: root/src/features/smart-scroll/logic/smart-scroll.test.ts
blob: fc3e3d1ea20f87ed878701d574ff6e9e4be35ffd (plain)
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);
	});
});