@rific/scroll-view 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.d.mts +16 -14
- package/dist/index.d.ts +16 -14
- package/dist/index.js +14 -1575
- package/dist/index.mjs +14 -1577
- package/package.json +10 -10
- package/src/redux/scrollViewSlice.ts +19 -11
- package/src/__mocks__/auto-paper.ts +0 -9
- package/src/__mocks__/flash-list.ts +0 -20
- package/src/__mocks__/react-native-gesture-handler.ts +0 -13
- package/src/__mocks__/react-native-keyboard-controller.ts +0 -11
- package/src/__mocks__/react-native-paper.ts +0 -22
- package/src/__mocks__/react-native-reanimated.ts +0 -61
- package/src/__mocks__/react-native-safe-area-context.ts +0 -2
- package/src/__mocks__/react-native.ts +0 -30
- package/src/__tests__/CustomList.test.tsx +0 -61
- package/src/__tests__/FlatList.test.tsx +0 -135
- package/src/__tests__/PullSearch.test.tsx +0 -38
- package/src/__tests__/ScrollView.test.tsx +0 -23
- package/src/__tests__/ScrollViewHeader.test.tsx +0 -28
- package/src/__tests__/ScrollViewProvider.test.tsx +0 -65
- package/src/__tests__/SectionList.test.tsx +0 -88
- package/src/__tests__/useScrollHandlerJS.test.ts +0 -119
- package/src/__tests__/useScrollInit.test.ts +0 -251
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { render } from '@testing-library/react'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
import { SectionList as RNSectionList } from 'react-native'
|
|
4
|
-
|
|
5
|
-
import { SectionList } from '../SectionList'
|
|
6
|
-
import { ScrollViewProvider } from '../ScrollViewProvider'
|
|
7
|
-
|
|
8
|
-
const wrapper = ({ children }: { children: React.ReactNode }) => <ScrollViewProvider>{children}</ScrollViewProvider>
|
|
9
|
-
|
|
10
|
-
type Section = { title: string; data: string[] }
|
|
11
|
-
const sections: Section[] = [
|
|
12
|
-
{ title: 'Fruits', data: ['Apple', 'Banana'] },
|
|
13
|
-
{ title: 'Veggies', data: ['Carrot'] }
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
describe('SectionList', () => {
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
;(RNSectionList as jest.Mock).mockClear()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('renders without crashing', () => {
|
|
22
|
-
render(
|
|
23
|
-
<SectionList sections={sections} renderItem={({ item }) => <>{item}</>} keyExtractor={(item) => item} />,
|
|
24
|
-
{ wrapper }
|
|
25
|
-
)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it('renders with renderSectionHeader', () => {
|
|
29
|
-
render(
|
|
30
|
-
<SectionList
|
|
31
|
-
sections={sections}
|
|
32
|
-
renderItem={({ item }) => <>{item}</>}
|
|
33
|
-
keyExtractor={(item) => item}
|
|
34
|
-
renderSectionHeader={({ section }) => <>{(section as Section).title}</>}
|
|
35
|
-
/>,
|
|
36
|
-
{ wrapper }
|
|
37
|
-
)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('disables native stickySectionHeadersEnabled when custom renderSectionHeader is provided', () => {
|
|
41
|
-
render(
|
|
42
|
-
<SectionList
|
|
43
|
-
sections={sections}
|
|
44
|
-
renderItem={({ item }) => <>{item}</>}
|
|
45
|
-
keyExtractor={(item) => item}
|
|
46
|
-
renderSectionHeader={({ section }) => <>{(section as Section).title}</>}
|
|
47
|
-
/>,
|
|
48
|
-
{ wrapper }
|
|
49
|
-
)
|
|
50
|
-
const calls = (RNSectionList as jest.Mock).mock.calls
|
|
51
|
-
const lastProps = calls[calls.length - 1]?.[0]
|
|
52
|
-
expect(lastProps?.stickySectionHeadersEnabled).toBe(false)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('keeps native sticky when no renderSectionHeader provided', () => {
|
|
56
|
-
render(
|
|
57
|
-
<SectionList sections={sections} renderItem={({ item }) => <>{item}</>} keyExtractor={(item) => item} />,
|
|
58
|
-
{ wrapper }
|
|
59
|
-
)
|
|
60
|
-
const calls = (RNSectionList as jest.Mock).mock.calls
|
|
61
|
-
const lastProps = calls[calls.length - 1]?.[0]
|
|
62
|
-
expect(lastProps?.stickySectionHeadersEnabled).toBe(false)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('renders with pullSearchHeight', () => {
|
|
66
|
-
render(
|
|
67
|
-
<SectionList
|
|
68
|
-
sections={sections}
|
|
69
|
-
renderItem={({ item }) => <>{item}</>}
|
|
70
|
-
keyExtractor={(item) => item}
|
|
71
|
-
pullSearchHeight={60}
|
|
72
|
-
/>,
|
|
73
|
-
{ wrapper }
|
|
74
|
-
)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('renders with renderFilters', () => {
|
|
78
|
-
render(
|
|
79
|
-
<SectionList
|
|
80
|
-
sections={sections}
|
|
81
|
-
renderItem={({ item }) => <>{item}</>}
|
|
82
|
-
keyExtractor={(item) => item}
|
|
83
|
-
renderFilters={<></>}
|
|
84
|
-
/>,
|
|
85
|
-
{ wrapper }
|
|
86
|
-
)
|
|
87
|
-
})
|
|
88
|
-
})
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { renderHook } from '@testing-library/react'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
import type { SharedValue } from 'react-native-reanimated'
|
|
4
|
-
|
|
5
|
-
import { useScrollHandlerJS } from '../internal/useScrollHandlerJS'
|
|
6
|
-
import { ScrollViewContext, type ScrollViewContextType } from '../ScrollViewContext'
|
|
7
|
-
|
|
8
|
-
const REMOUNT_RETRY_MAX_ATTEMPTS = 8
|
|
9
|
-
|
|
10
|
-
const buildContextValue = (): ScrollViewContextType =>
|
|
11
|
-
({
|
|
12
|
-
footerHeightShared: { value: 0 },
|
|
13
|
-
footerOffset: { value: 0 },
|
|
14
|
-
headerHeightShared: { value: 0 },
|
|
15
|
-
headerOffset: { value: 0 },
|
|
16
|
-
pullSearchHeightShared: { value: 0 },
|
|
17
|
-
scrollPosition: { value: 0 },
|
|
18
|
-
snapBackFooterShared: { value: false },
|
|
19
|
-
snapBackHeaderShared: { value: false }
|
|
20
|
-
}) as unknown as ScrollViewContextType
|
|
21
|
-
|
|
22
|
-
const scrollEvent = (y: number) =>
|
|
23
|
-
({
|
|
24
|
-
nativeEvent: {
|
|
25
|
-
contentOffset: { x: 0, y },
|
|
26
|
-
contentSize: { height: 1000, width: 0 },
|
|
27
|
-
layoutMeasurement: { height: 500, width: 0 }
|
|
28
|
-
}
|
|
29
|
-
}) as never
|
|
30
|
-
|
|
31
|
-
const renderScrollHandlerJS = (overrides: Partial<Parameters<typeof useScrollHandlerJS>[0]> = {}) => {
|
|
32
|
-
const contextValue = buildContextValue()
|
|
33
|
-
const chipHidden = { value: 0 } as unknown as SharedValue<number>
|
|
34
|
-
const capturedGeneration = { current: 0 }
|
|
35
|
-
const jsListGeneration = { current: 0 }
|
|
36
|
-
const scrollTo = jest.fn()
|
|
37
|
-
const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(ScrollViewContext.Provider, { value: contextValue }, children)
|
|
38
|
-
const rendered = renderHook(
|
|
39
|
-
() =>
|
|
40
|
-
useScrollHandlerJS({
|
|
41
|
-
capturedGeneration,
|
|
42
|
-
chipHidden,
|
|
43
|
-
footerFixed: false,
|
|
44
|
-
headerFixed: false,
|
|
45
|
-
jsListGeneration,
|
|
46
|
-
scrollTo,
|
|
47
|
-
...overrides
|
|
48
|
-
}),
|
|
49
|
-
{ wrapper }
|
|
50
|
-
)
|
|
51
|
-
return { ...rendered, capturedGeneration, chipHidden, contextValue, jsListGeneration, scrollTo }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe('useScrollHandlerJS generation guard', () => {
|
|
55
|
-
it('processes onScroll and updates scrollPosition/chipHidden when the generation matches', () => {
|
|
56
|
-
const { result, contextValue, chipHidden } = renderScrollHandlerJS({ chipThreshold: 100 })
|
|
57
|
-
result.current.onScroll(scrollEvent(50))
|
|
58
|
-
expect(contextValue.scrollPosition.value).toBe(50)
|
|
59
|
-
expect(chipHidden.value).toBe(1) // below threshold
|
|
60
|
-
|
|
61
|
-
result.current.onScroll(scrollEvent(150))
|
|
62
|
-
expect(contextValue.scrollPosition.value).toBe(150)
|
|
63
|
-
expect(chipHidden.value).toBe(0) // at/above threshold
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('drops onScroll from a zombie handler once jsListGeneration no longer matches its capturedGeneration', () => {
|
|
67
|
-
const { result, contextValue, capturedGeneration, jsListGeneration } = renderScrollHandlerJS()
|
|
68
|
-
result.current.onScroll(scrollEvent(10))
|
|
69
|
-
expect(contextValue.scrollPosition.value).toBe(10)
|
|
70
|
-
|
|
71
|
-
// Another list instance mounted/unmounted elsewhere and bumped the shared generation counter —
|
|
72
|
-
// this handler's own capturedGeneration (frozen at mount) is now stale.
|
|
73
|
-
jsListGeneration.current = 1
|
|
74
|
-
expect(capturedGeneration.current).toBe(0)
|
|
75
|
-
|
|
76
|
-
result.current.onScroll(scrollEvent(999))
|
|
77
|
-
expect(contextValue.scrollPosition.value).toBe(10) // unchanged — the stale event was ignored
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('lets a fresh instance keep working even while a stale sibling is guarded out', () => {
|
|
81
|
-
const jsListGeneration = { current: 1 }
|
|
82
|
-
const { result, contextValue } = renderScrollHandlerJS({ capturedGeneration: { current: 1 }, jsListGeneration })
|
|
83
|
-
result.current.onScroll(scrollEvent(42))
|
|
84
|
-
expect(contextValue.scrollPosition.value).toBe(42)
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
describe('useScrollHandlerJS remount retry', () => {
|
|
89
|
-
it('retries with a non-animated scrollTo until an onScroll event confirms the target position', () => {
|
|
90
|
-
const { result, scrollTo, contextValue } = renderScrollHandlerJS({ remountTarget: -40 })
|
|
91
|
-
result.current.onScroll(scrollEvent(0)) // native reports the wrong position
|
|
92
|
-
expect(scrollTo).toHaveBeenCalledWith(-40, false)
|
|
93
|
-
expect(contextValue.scrollPosition.value).toBe(0) // not yet treated as a real scroll position
|
|
94
|
-
|
|
95
|
-
scrollTo.mockClear()
|
|
96
|
-
result.current.onScroll(scrollEvent(-40)) // confirmed
|
|
97
|
-
expect(scrollTo).not.toHaveBeenCalled()
|
|
98
|
-
expect(contextValue.scrollPosition.value).toBe(-40) // now processed normally
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('gives up after REMOUNT_RETRY_MAX_ATTEMPTS and falls through to normal processing rather than blocking forever', () => {
|
|
102
|
-
const { result, scrollTo, contextValue } = renderScrollHandlerJS({ remountTarget: -40 })
|
|
103
|
-
for (let i = 0; i < REMOUNT_RETRY_MAX_ATTEMPTS; i += 1) result.current.onScroll(scrollEvent(0))
|
|
104
|
-
expect(scrollTo).toHaveBeenCalledTimes(REMOUNT_RETRY_MAX_ATTEMPTS)
|
|
105
|
-
|
|
106
|
-
scrollTo.mockClear()
|
|
107
|
-
result.current.onScroll(scrollEvent(0)) // one more, past the max — gives up
|
|
108
|
-
expect(scrollTo).not.toHaveBeenCalled()
|
|
109
|
-
expect(contextValue.scrollPosition.value).toBe(0) // accepted as-is instead of retrying forever
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('the generation guard takes precedence over an in-progress remount retry', () => {
|
|
113
|
-
const { result, scrollTo, contextValue, jsListGeneration } = renderScrollHandlerJS({ remountTarget: -40 })
|
|
114
|
-
jsListGeneration.current = 1 // stale before the remount retry ever gets a chance to run
|
|
115
|
-
result.current.onScroll(scrollEvent(0))
|
|
116
|
-
expect(scrollTo).not.toHaveBeenCalled()
|
|
117
|
-
expect(contextValue.scrollPosition.value).toBe(0) // untouched — onScroll bailed out immediately
|
|
118
|
-
})
|
|
119
|
-
})
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import { act, renderHook } from '@testing-library/react'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
|
|
4
|
-
import { ScrollViewContext, type ScrollViewContextType } from '../ScrollViewContext'
|
|
5
|
-
import { useScrollInit } from '../useScrollInit'
|
|
6
|
-
|
|
7
|
-
const HEADER_HEIGHT = 80
|
|
8
|
-
const PULL_SEARCH_HEIGHT = 40
|
|
9
|
-
const REMOUNT_SYNC_MAX_ATTEMPTS = 8
|
|
10
|
-
const SNAP_VERIFY_MAX_ATTEMPTS = 5
|
|
11
|
-
const SNAP_VERIFY_INITIAL_DELAY_MS = 400
|
|
12
|
-
const SNAP_VERIFY_RETRY_DELAY_MS = 150
|
|
13
|
-
|
|
14
|
-
const buildContextValue = (headerHeight: number | null): ScrollViewContextType =>
|
|
15
|
-
({
|
|
16
|
-
blur: true,
|
|
17
|
-
footerHeight: 0,
|
|
18
|
-
footerHeightShared: { value: 0 },
|
|
19
|
-
footerFixed: false,
|
|
20
|
-
footerOffset: { value: 0 },
|
|
21
|
-
headerHeight,
|
|
22
|
-
headerHeightShared: { value: headerHeight ?? 0 },
|
|
23
|
-
headerFixed: false,
|
|
24
|
-
headerOffset: { value: 0 },
|
|
25
|
-
listGeneration: { value: 0 },
|
|
26
|
-
onListUnmount: jest.fn(),
|
|
27
|
-
progress: null,
|
|
28
|
-
progressing: false,
|
|
29
|
-
pullSearchHeightShared: { value: 0 },
|
|
30
|
-
scrollHeight: 0,
|
|
31
|
-
scrollPosition: { value: 0 },
|
|
32
|
-
setFooterHeight: jest.fn(),
|
|
33
|
-
setHeaderHeight: jest.fn(),
|
|
34
|
-
setProgress: jest.fn(),
|
|
35
|
-
setProgressing: jest.fn(),
|
|
36
|
-
snapBackFooterShared: { value: false },
|
|
37
|
-
snapBackHeaderShared: { value: false },
|
|
38
|
-
tabBarHeight: 60
|
|
39
|
-
}) as unknown as ScrollViewContextType
|
|
40
|
-
|
|
41
|
-
const renderInit = (headerHeight: number | null, pullSearchHeight?: number) => {
|
|
42
|
-
const contextValue = buildContextValue(headerHeight)
|
|
43
|
-
const scrollTo = jest.fn()
|
|
44
|
-
const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(ScrollViewContext.Provider, { value: contextValue }, children)
|
|
45
|
-
const rendered = renderHook(() => useScrollInit({ pullSearchHeight, scrollTo }), { wrapper })
|
|
46
|
-
return { ...rendered, contextValue, scrollTo }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
describe('useScrollInit remount sync', () => {
|
|
50
|
-
it('true first mount (headerHeight null) skips remount-sync and starts ready', () => {
|
|
51
|
-
const { result, unmount } = renderInit(null)
|
|
52
|
-
expect(result.current.scrollReady).toBe(true)
|
|
53
|
-
expect(result.current.remountSyncTarget.value).toBeNull()
|
|
54
|
-
unmount()
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('remount (headerHeight already known at mount) hides the list and targets showY', () => {
|
|
58
|
-
const { result, scrollTo, unmount } = renderInit(HEADER_HEIGHT)
|
|
59
|
-
expect(result.current.scrollReady).toBe(false)
|
|
60
|
-
expect(result.current.remountSyncTarget.value).toBe(-HEADER_HEIGHT)
|
|
61
|
-
// Primes a corrective scrollTo immediately, rather than assuming native honored the initial contentOffset.
|
|
62
|
-
expect(scrollTo).toHaveBeenCalledWith(-HEADER_HEIGHT, false)
|
|
63
|
-
unmount()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('remount with pullSearchHeight targets hideY directly (skips the 2-phase first-mount dance)', () => {
|
|
67
|
-
const { result, unmount } = renderInit(HEADER_HEIGHT, 40)
|
|
68
|
-
expect(result.current.remountSyncTarget.value).toBe(-HEADER_HEIGHT + 40)
|
|
69
|
-
unmount()
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
it('retries the corrective scrollTo when an onScroll event reports the wrong position, and stays hidden', () => {
|
|
73
|
-
const { result, scrollTo, unmount } = renderInit(HEADER_HEIGHT)
|
|
74
|
-
scrollTo.mockClear()
|
|
75
|
-
act(() => {
|
|
76
|
-
result.current.onRemountSyncRetry(0) // a spurious post-remount event reporting y=0
|
|
77
|
-
})
|
|
78
|
-
expect(scrollTo).toHaveBeenCalledWith(-HEADER_HEIGHT, false)
|
|
79
|
-
expect(result.current.scrollReady).toBe(false)
|
|
80
|
-
unmount()
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('reveals once a confirmed on-target event arrives', () => {
|
|
84
|
-
const { result, contextValue, unmount } = renderInit(HEADER_HEIGHT)
|
|
85
|
-
act(() => {
|
|
86
|
-
result.current.onRemountSynced(-HEADER_HEIGHT)
|
|
87
|
-
})
|
|
88
|
-
expect(result.current.scrollReady).toBe(true)
|
|
89
|
-
expect(contextValue.scrollPosition.value).toBe(-HEADER_HEIGHT)
|
|
90
|
-
unmount()
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('gives up and reveals anyway after exceeding the max retry attempts, rather than hiding forever', () => {
|
|
94
|
-
const { result, unmount } = renderInit(HEADER_HEIGHT)
|
|
95
|
-
act(() => {
|
|
96
|
-
for (let i = 0; i < REMOUNT_SYNC_MAX_ATTEMPTS + 2; i += 1) result.current.onRemountSyncRetry(0)
|
|
97
|
-
})
|
|
98
|
-
expect(result.current.scrollReady).toBe(true)
|
|
99
|
-
expect(result.current.remountSyncTarget.value).toBeNull()
|
|
100
|
-
unmount()
|
|
101
|
-
})
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
const dragEvent = (y: number) => ({ nativeEvent: { contentOffset: { y } } }) as never
|
|
105
|
-
|
|
106
|
-
// A true first mount (not a remount): headerHeight starts null, then a rerender simulates the
|
|
107
|
-
// header's async measurement landing, mirroring the real production flow. Using a non-null
|
|
108
|
-
// headerHeight from the start (like the remount-sync tests above) would make useScrollInit treat
|
|
109
|
-
// this as a remount and pull in unrelated machinery — the priming scrollTo, the 40-frame RAF
|
|
110
|
-
// oscillation, and the REMOUNT_SYNC_FALLBACK_MS timer — none of which is relevant to the pull-search
|
|
111
|
-
// snap logic under test here.
|
|
112
|
-
const renderPullSearchInit = (pullSearchHeight: number) => {
|
|
113
|
-
const scrollTo = jest.fn()
|
|
114
|
-
let contextValue = buildContextValue(null)
|
|
115
|
-
const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(ScrollViewContext.Provider, { value: contextValue }, children)
|
|
116
|
-
const rendered = renderHook(() => useScrollInit({ pullSearchHeight, scrollTo }), { wrapper })
|
|
117
|
-
act(() => {
|
|
118
|
-
contextValue = buildContextValue(HEADER_HEIGHT)
|
|
119
|
-
rendered.rerender()
|
|
120
|
-
})
|
|
121
|
-
scrollTo.mockClear() // drop Effect A's insurance call and Effect B's showY->hideY push
|
|
122
|
-
return { ...rendered, contextValue, scrollTo }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
describe('useScrollInit pull-search snap', () => {
|
|
126
|
-
const showY = -HEADER_HEIGHT
|
|
127
|
-
const hideY = -HEADER_HEIGHT + PULL_SEARCH_HEIGHT
|
|
128
|
-
|
|
129
|
-
beforeEach(() => jest.useFakeTimers())
|
|
130
|
-
afterEach(() => jest.useRealTimers())
|
|
131
|
-
|
|
132
|
-
it('opens toward showY when dragged down past half the pull-search height', () => {
|
|
133
|
-
const { result, scrollTo, unmount } = renderPullSearchInit(PULL_SEARCH_HEIGHT)
|
|
134
|
-
act(() => {
|
|
135
|
-
result.current.handleScrollBeginDrag(dragEvent(hideY))
|
|
136
|
-
result.current.handleScrollEndDrag(dragEvent(showY))
|
|
137
|
-
})
|
|
138
|
-
expect(scrollTo).toHaveBeenCalledWith(showY, true)
|
|
139
|
-
unmount()
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('closes toward hideY when released without pulling past the half threshold', () => {
|
|
143
|
-
const { result, scrollTo, unmount } = renderPullSearchInit(PULL_SEARCH_HEIGHT)
|
|
144
|
-
act(() => {
|
|
145
|
-
result.current.handleScrollBeginDrag(dragEvent(hideY))
|
|
146
|
-
result.current.handleScrollEndDrag(dragEvent(hideY - 10))
|
|
147
|
-
})
|
|
148
|
-
expect(scrollTo).toHaveBeenCalledWith(hideY, true)
|
|
149
|
-
unmount()
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('does nothing when the release lands outside the pull-search zone entirely', () => {
|
|
153
|
-
const { result, scrollTo, unmount } = renderPullSearchInit(PULL_SEARCH_HEIGHT)
|
|
154
|
-
act(() => {
|
|
155
|
-
result.current.handleScrollBeginDrag(dragEvent(0))
|
|
156
|
-
result.current.handleScrollEndDrag(dragEvent(0))
|
|
157
|
-
})
|
|
158
|
-
expect(scrollTo).not.toHaveBeenCalled()
|
|
159
|
-
unmount()
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('keeps retrying (non-animated) until scrollPosition actually confirms the target, then stops', () => {
|
|
163
|
-
const { result, scrollTo, contextValue, unmount } = renderPullSearchInit(PULL_SEARCH_HEIGHT)
|
|
164
|
-
contextValue.scrollPosition.value = hideY
|
|
165
|
-
act(() => {
|
|
166
|
-
result.current.handleScrollBeginDrag(dragEvent(hideY))
|
|
167
|
-
result.current.handleScrollEndDrag(dragEvent(showY))
|
|
168
|
-
})
|
|
169
|
-
expect(scrollTo).toHaveBeenCalledTimes(1) // the initial animated call
|
|
170
|
-
|
|
171
|
-
act(() => {
|
|
172
|
-
jest.advanceTimersByTime(SNAP_VERIFY_INITIAL_DELAY_MS)
|
|
173
|
-
})
|
|
174
|
-
expect(scrollTo).toHaveBeenLastCalledWith(showY, false) // first non-animated retry, still short of target
|
|
175
|
-
|
|
176
|
-
// The retry "lands" — simulate onScroll confirming the real position.
|
|
177
|
-
contextValue.scrollPosition.value = showY
|
|
178
|
-
act(() => {
|
|
179
|
-
jest.advanceTimersByTime(SNAP_VERIFY_RETRY_DELAY_MS)
|
|
180
|
-
})
|
|
181
|
-
scrollTo.mockClear()
|
|
182
|
-
act(() => {
|
|
183
|
-
jest.advanceTimersByTime(SNAP_VERIFY_RETRY_DELAY_MS * 5)
|
|
184
|
-
})
|
|
185
|
-
expect(scrollTo).not.toHaveBeenCalled() // verification stopped once confirmed
|
|
186
|
-
unmount()
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
it('gives up after exceeding the max retry attempts rather than retrying forever', () => {
|
|
190
|
-
const { result, scrollTo, contextValue, unmount } = renderPullSearchInit(PULL_SEARCH_HEIGHT)
|
|
191
|
-
contextValue.scrollPosition.value = hideY // never reaches showY, no matter how many retries
|
|
192
|
-
act(() => {
|
|
193
|
-
result.current.handleScrollBeginDrag(dragEvent(hideY))
|
|
194
|
-
result.current.handleScrollEndDrag(dragEvent(showY))
|
|
195
|
-
})
|
|
196
|
-
act(() => {
|
|
197
|
-
jest.advanceTimersByTime(SNAP_VERIFY_INITIAL_DELAY_MS + SNAP_VERIFY_RETRY_DELAY_MS * (SNAP_VERIFY_MAX_ATTEMPTS + 1))
|
|
198
|
-
})
|
|
199
|
-
// 1 initial animated call + SNAP_VERIFY_MAX_ATTEMPTS non-animated retries, then it gives up.
|
|
200
|
-
expect(scrollTo).toHaveBeenCalledTimes(1 + SNAP_VERIFY_MAX_ATTEMPTS)
|
|
201
|
-
|
|
202
|
-
scrollTo.mockClear()
|
|
203
|
-
act(() => {
|
|
204
|
-
jest.advanceTimersByTime(SNAP_VERIFY_RETRY_DELAY_MS * 3)
|
|
205
|
-
})
|
|
206
|
-
expect(scrollTo).not.toHaveBeenCalled()
|
|
207
|
-
unmount()
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('cancels a pending verification when a new drag begins', () => {
|
|
211
|
-
const { result, scrollTo, contextValue, unmount } = renderPullSearchInit(PULL_SEARCH_HEIGHT)
|
|
212
|
-
contextValue.scrollPosition.value = hideY
|
|
213
|
-
act(() => {
|
|
214
|
-
result.current.handleScrollBeginDrag(dragEvent(hideY))
|
|
215
|
-
result.current.handleScrollEndDrag(dragEvent(showY))
|
|
216
|
-
})
|
|
217
|
-
scrollTo.mockClear()
|
|
218
|
-
act(() => {
|
|
219
|
-
result.current.handleScrollBeginDrag(dragEvent(showY))
|
|
220
|
-
})
|
|
221
|
-
act(() => {
|
|
222
|
-
jest.advanceTimersByTime(SNAP_VERIFY_INITIAL_DELAY_MS * 3)
|
|
223
|
-
})
|
|
224
|
-
expect(scrollTo).not.toHaveBeenCalled()
|
|
225
|
-
unmount()
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
it('handleMomentumScrollEnd auto-closes when momentum alone carried it into the zone with no drag-committed target', () => {
|
|
229
|
-
const { result, scrollTo, unmount } = renderPullSearchInit(PULL_SEARCH_HEIGHT)
|
|
230
|
-
act(() => {
|
|
231
|
-
result.current.handleMomentumScrollEnd(dragEvent(hideY - 10))
|
|
232
|
-
})
|
|
233
|
-
expect(scrollTo).toHaveBeenCalledWith(hideY, true)
|
|
234
|
-
unmount()
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
it('handleMomentumScrollEnd defers to an already-pending drag-committed target instead of double-correcting', () => {
|
|
238
|
-
const { result, scrollTo, contextValue, unmount } = renderPullSearchInit(PULL_SEARCH_HEIGHT)
|
|
239
|
-
contextValue.scrollPosition.value = hideY
|
|
240
|
-
act(() => {
|
|
241
|
-
result.current.handleScrollBeginDrag(dragEvent(hideY))
|
|
242
|
-
result.current.handleScrollEndDrag(dragEvent(showY))
|
|
243
|
-
})
|
|
244
|
-
scrollTo.mockClear()
|
|
245
|
-
act(() => {
|
|
246
|
-
result.current.handleMomentumScrollEnd(dragEvent(hideY - 10))
|
|
247
|
-
})
|
|
248
|
-
expect(scrollTo).not.toHaveBeenCalled()
|
|
249
|
-
unmount()
|
|
250
|
-
})
|
|
251
|
-
})
|