hookehuyr

feat(checkin): 添加滚动恢复功能以改善用户体验

新增 useScrollRestoration 组合式函数,用于在打卡列表页面实现滚动位置恢复
当用户从打卡详情页返回时,自动恢复到之前的滚动位置,支持锚点定位和日历高度补偿
添加对应的单元测试,覆盖等待条件、超时处理和条件恢复等场景
1 <!-- 1 <!--
2 * @Date: 2025-01-25 15:34:17 2 * @Date: 2025-01-25 15:34:17
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2026-01-23 09:33:40 4 + * @LastEditTime: 2026-01-23 10:38:40
5 * @FilePath: /mlaj/src/components/calendar/CollapsibleCalendar.vue 5 * @FilePath: /mlaj/src/components/calendar/CollapsibleCalendar.vue
6 * @Description: 可折叠日历组件 6 * @Description: 可折叠日历组件
7 --> 7 -->
......
1 +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
2 +import { useScrollRestoration } from '../useScrollRestoration'
3 +
4 +const create_session_storage = () => {
5 + const store = {}
6 + return {
7 + store,
8 + setItem: vi.fn((key, value) => {
9 + store[key] = String(value)
10 + }),
11 + getItem: vi.fn((key) => {
12 + if (Object.prototype.hasOwnProperty.call(store, key)) return store[key]
13 + return null
14 + }),
15 + removeItem: vi.fn((key) => {
16 + delete store[key]
17 + }),
18 + clear: vi.fn(() => {
19 + Object.keys(store).forEach((key) => delete store[key])
20 + }),
21 + }
22 +}
23 +
24 +describe('useScrollRestoration', () => {
25 + const original_window = globalThis.window
26 + const original_session_storage = globalThis.sessionStorage
27 +
28 + beforeEach(() => {
29 + const session_storage = create_session_storage()
30 + vi.stubGlobal('sessionStorage', session_storage)
31 +
32 + const window_stub = {
33 + scrollY: 0,
34 + scrollTo: vi.fn(({ top }) => {
35 + window_stub.scrollY = top
36 + }),
37 + }
38 + vi.stubGlobal('window', window_stub)
39 + })
40 +
41 + afterEach(() => {
42 + vi.unstubAllGlobals()
43 + if (original_window !== undefined) vi.stubGlobal('window', original_window)
44 + if (original_session_storage !== undefined) vi.stubGlobal('sessionStorage', original_session_storage)
45 + vi.clearAllMocks()
46 + })
47 +
48 + it('restore_state 会等待 wait_for 达成后再执行滚动', async () => {
49 + const { save_state, restore_state } = useScrollRestoration({
50 + get_key: () => 'scroll_key',
51 + get_scroll_el: () => window,
52 + })
53 +
54 + window.scrollY = 200
55 + save_state({ extra: 1 })
56 +
57 + window.scrollY = 0
58 + let calls = 0
59 +
60 + await restore_state({
61 + wait_for: () => {
62 + calls += 1
63 + return calls >= 3
64 + },
65 + wait_for_timeout_ms: 200,
66 + wait_for_interval_ms: 1,
67 + settle_frames: 0,
68 + get_scroll_top: () => 123,
69 + })
70 +
71 + expect(calls).toBeGreaterThanOrEqual(3)
72 + expect(window.scrollTo).toHaveBeenCalled()
73 + expect(window.scrollY).toBe(123)
74 + })
75 +
76 + it('wait_for 超时后仍会继续尝试恢复滚动', async () => {
77 + const { save_state, restore_state } = useScrollRestoration({
78 + get_key: () => 'scroll_key_timeout',
79 + get_scroll_el: () => window,
80 + })
81 +
82 + window.scrollY = 80
83 + save_state()
84 +
85 + window.scrollY = 0
86 +
87 + await restore_state({
88 + wait_for: () => false,
89 + wait_for_timeout_ms: 20,
90 + wait_for_interval_ms: 5,
91 + settle_frames: 0,
92 + get_scroll_top: () => 50,
93 + })
94 +
95 + expect(window.scrollTo).toHaveBeenCalled()
96 + expect(window.scrollY).toBe(50)
97 + })
98 +
99 + it('should_restore 返回 false 时不滚动且会清理状态', async () => {
100 + const { save_state, restore_state, read_state } = useScrollRestoration({
101 + get_key: () => 'scroll_key_should_restore',
102 + get_scroll_el: () => window,
103 + })
104 +
105 + window.scrollY = 10
106 + save_state()
107 +
108 + await restore_state({
109 + should_restore: () => false,
110 + })
111 +
112 + expect(window.scrollTo).not.toHaveBeenCalled()
113 + expect(read_state()).toBeNull()
114 + })
115 +})
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.