diff --git a/docs/api/user-event.md b/docs/api/user-event.md index bfee6e2c3..b3b9e8797 100644 --- a/docs/api/user-event.md +++ b/docs/api/user-event.md @@ -285,13 +285,13 @@ The sequence of events depends on whether the scroll includes an optional moment - `contentSizeChange` - `scrollBeginDrag` -- `scroll` (multiple events) +- `scroll` (zero or more events; none if the scroll offset does not change) - `scrollEndDrag` **Momentum scroll (optional)**: - `momentumScrollBegin` -- `scroll` (multiple events) +- `scroll` (zero or more events; none if the scroll offset does not change) - `momentumScrollEnd` ## `pullToRefresh()` \ diff --git a/src/user-event/scroll/__tests__/__snapshots__/scroll-to-flat-list.test.tsx.snap b/src/user-event/scroll/__tests__/__snapshots__/scroll-to-flat-list.test.tsx.snap index e71333cf6..ba7a21e4d 100644 --- a/src/user-event/scroll/__tests__/__snapshots__/scroll-to-flat-list.test.tsx.snap +++ b/src/user-event/scroll/__tests__/__snapshots__/scroll-to-flat-list.test.tsx.snap @@ -162,6 +162,46 @@ exports[`scrollTo() with FlatList supports vertical drag scroll: scrollTo({ y: 1 "timeStamp": 0, }, }, + { + "name": "scroll", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "contentInset": { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0, + }, + "contentOffset": { + "x": 0, + "y": 100, + }, + "contentSize": { + "height": 0, + "width": 0, + }, + "layoutMeasurement": { + "height": 0, + "width": 0, + }, + "responderIgnoreScroll": true, + "target": 0, + "velocity": { + "x": 0, + "y": 0, + }, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, { "name": "scrollEndDrag", "payload": { diff --git a/src/user-event/scroll/__tests__/__snapshots__/scroll-to.test.tsx.snap b/src/user-event/scroll/__tests__/__snapshots__/scroll-to.test.tsx.snap index ef252e6df..b12ed6bae 100644 --- a/src/user-event/scroll/__tests__/__snapshots__/scroll-to.test.tsx.snap +++ b/src/user-event/scroll/__tests__/__snapshots__/scroll-to.test.tsx.snap @@ -162,6 +162,46 @@ exports[`scrollTo() supports vertical drag scroll: scrollTo({ y: 100 }) 1`] = ` "timeStamp": 0, }, }, + { + "name": "scroll", + "payload": { + "currentTarget": {}, + "isDefaultPrevented": [Function], + "isPersistent": [Function], + "isPropagationStopped": [Function], + "nativeEvent": { + "contentInset": { + "bottom": 0, + "left": 0, + "right": 0, + "top": 0, + }, + "contentOffset": { + "x": 0, + "y": 100, + }, + "contentSize": { + "height": 0, + "width": 0, + }, + "layoutMeasurement": { + "height": 0, + "width": 0, + }, + "responderIgnoreScroll": true, + "target": 0, + "velocity": { + "x": 0, + "y": 0, + }, + }, + "persist": [Function], + "preventDefault": [Function], + "stopPropagation": [Function], + "target": {}, + "timeStamp": 0, + }, + }, { "name": "scrollEndDrag", "payload": { diff --git a/src/user-event/scroll/__tests__/scroll-to-flat-list.test.tsx b/src/user-event/scroll/__tests__/scroll-to-flat-list.test.tsx index 80141ee77..92080a3f4 100644 --- a/src/user-event/scroll/__tests__/scroll-to-flat-list.test.tsx +++ b/src/user-event/scroll/__tests__/scroll-to-flat-list.test.tsx @@ -23,7 +23,7 @@ async function renderFlatListWithToolkit(props: ScrollViewProps = {}) { const renderItem = (title: string) => {title}; await render( { const { events } = await renderFlatListWithToolkit(); const user = userEvent.setup(); - await user.scrollTo(screen.getByTestId('flatList'), { y: 100 }); + await user.scrollTo(screen.getByTestId('flat-list'), { y: 100 }); expect(mapEventsToShortForm(events)).toEqual([ ['scrollBeginDrag', 0, 0], ['scroll', 25, 0], ['scroll', 50, 0], ['scroll', 75, 0], + ['scroll', 100, 0], ['scrollEndDrag', 100, 0], ]); expect(events).toMatchSnapshot('scrollTo({ y: 100 })'); @@ -61,12 +62,13 @@ describe('scrollTo() with FlatList', () => { const { events } = await renderFlatListWithToolkit({ horizontal: true }); const user = userEvent.setup(); - await user.scrollTo(screen.getByTestId('flatList'), { x: 100 }); + await user.scrollTo(screen.getByTestId('flat-list'), { x: 100 }); expect(mapEventsToShortForm(events)).toEqual([ ['scrollBeginDrag', 0, 0], ['scroll', 0, 25], ['scroll', 0, 50], ['scroll', 0, 75], + ['scroll', 0, 100], ['scrollEndDrag', 0, 100], ]); }); @@ -117,3 +119,28 @@ test('scrollTo with contentSize and layoutMeasurement update FlatList content', // Prevent act warning by unmounting the component await screen.unmount(); }); + +test('drag scroll delivers final offset to FlatList', async () => { + const onEndReached = jest.fn(); + await render( + } + onEndReached={onEndReached} + onEndReachedThreshold={0} + />, + ); + const user = userEvent.setup(); + + await user.scrollTo(screen.getByTestId('flat-list'), { + y: 520, + contentSize: { width: 240, height: 1000 }, + layoutMeasurement: { width: 240, height: 480 }, + }); + + expect(onEndReached).toHaveBeenCalledTimes(1); + + // Prevent act warning by unmounting the component + await screen.unmount(); +}); diff --git a/src/user-event/scroll/__tests__/scroll-to.test.tsx b/src/user-event/scroll/__tests__/scroll-to.test.tsx index 3deb234fa..e2190ad3d 100644 --- a/src/user-event/scroll/__tests__/scroll-to.test.tsx +++ b/src/user-event/scroll/__tests__/scroll-to.test.tsx @@ -49,6 +49,7 @@ describe('scrollTo()', () => { ['scroll', 25, 0], ['scroll', 50, 0], ['scroll', 75, 0], + ['scroll', 100, 0], ['scrollEndDrag', 100, 0], ]); expect(events).toMatchSnapshot('scrollTo({ y: 100 })'); @@ -64,6 +65,7 @@ describe('scrollTo()', () => { ['scroll', 0, 25], ['scroll', 0, 50], ['scroll', 0, 75], + ['scroll', 0, 100], ['scrollEndDrag', 0, 100], ]); }); @@ -81,6 +83,7 @@ describe('scrollTo()', () => { ['scroll', 25, 0], ['scroll', 50, 0], ['scroll', 75, 0], + ['scroll', 100, 0], ['scrollEndDrag', 100, 0], ['momentumScrollBegin', 100, 0], ['scroll', 110, 0], @@ -102,6 +105,7 @@ describe('scrollTo()', () => { ['scroll', 25, 0], ['scroll', 50, 0], ['scroll', 75, 0], + ['scroll', 100, 0], ['scrollEndDrag', 100, 0], ]); }); @@ -117,11 +121,13 @@ describe('scrollTo()', () => { ['scroll', 25, 0], ['scroll', 50, 0], ['scroll', 75, 0], + ['scroll', 100, 0], ['scrollEndDrag', 100, 0], ['scrollBeginDrag', 100, 0], ['scroll', 125, 0], ['scroll', 150, 0], ['scroll', 175, 0], + ['scroll', 200, 0], ['scrollEndDrag', 200, 0], ]); }); @@ -140,6 +146,7 @@ describe('scrollTo()', () => { ['scroll', 125, 0], ['scroll', 150, 0], ['scroll', 175, 0], + ['scroll', 200, 0], ['scrollEndDrag', 200, 0], ]); }); @@ -190,9 +197,9 @@ describe('scrollTo()', () => { ['scroll', 25, 0], ['scroll', 50, 0], ['scroll', 75, 0], + ['scroll', 100, 0], ['scrollEndDrag', 100, 0], ['momentumScrollBegin', 100, 0], - ['scroll', 100, 0], ['momentumScrollEnd', 100, 0], ]); }); @@ -254,6 +261,7 @@ describe('scrollTo()', () => { ['scroll', 25, 0], ['scroll', 50, 0], ['scroll', 75, 0], + ['scroll', 100, 0], ['scrollEndDrag', 100, 0], ]); }); diff --git a/src/user-event/scroll/scroll-to.ts b/src/user-event/scroll/scroll-to.ts index 19f29d06b..6d581d6ef 100644 --- a/src/user-event/scroll/scroll-to.ts +++ b/src/user-event/scroll/scroll-to.ts @@ -103,10 +103,8 @@ async function emitDragScrollEvents( buildScrollEvent(scrollSteps[0], scrollOptions), ); - // Note: experimentally, in case of drag scroll the last scroll step - // will not trigger `scroll` event. - // See: https://github.com/callstack/react-native-testing-library/wiki/ScrollView-Events - for (let i = 1; i < scrollSteps.length - 1; i += 1) { + // Native emits `scroll` for every drag step, including the last; `scrollEndDrag` repeats its offset. + for (let i = 1; i < scrollSteps.length; i += 1) { await wait(config); await interaction.dispatchEvent('scroll', buildScrollEvent(scrollSteps[i], scrollOptions)); } @@ -132,9 +130,7 @@ async function emitMomentumScrollEvents( buildScrollEvent(scrollSteps[0], scrollOptions), ); - // Note: experimentally, in case of momentum scroll the last scroll step - // will trigger `scroll` event. - // See: https://github.com/callstack/react-native-testing-library/wiki/ScrollView-Events + // Native emits `scroll` for every momentum step, including the last; `momentumScrollEnd` repeats its offset. for (let i = 1; i < scrollSteps.length; i += 1) { await wait(config); await interaction.dispatchEvent('scroll', buildScrollEvent(scrollSteps[i], scrollOptions)); diff --git a/src/user-event/scroll/utils.ts b/src/user-event/scroll/utils.ts index 8fd043edd..4bb28483a 100644 --- a/src/user-event/scroll/utils.ts +++ b/src/user-event/scroll/utils.ts @@ -10,14 +10,14 @@ export function createScrollSteps( interpolator: InterpolatorFn, ): Point[] { if (target.y != null) { - return interpolator(target.y, initialOffset.y, DEFAULT_STEPS_COUNT).map((y) => ({ + return interpolate(target.y, initialOffset.y, interpolator).map((y) => ({ y, x: initialOffset.x, })); } if (target.x != null) { - return interpolator(target.x, initialOffset.x, DEFAULT_STEPS_COUNT).map((x) => ({ + return interpolate(target.x, initialOffset.x, interpolator).map((x) => ({ x, y: initialOffset.y, })); @@ -26,14 +26,19 @@ export function createScrollSteps( return []; } +function interpolate(end: number, start: number, interpolator: InterpolatorFn): number[] { + // No movement: a single step, so no `scroll` event is emitted for an unchanged offset. + if (end === start) { + return [start]; + } + + return interpolator(end, start, DEFAULT_STEPS_COUNT); +} + /** * Generate linear scroll values (with equal steps). */ export function linearInterpolator(end: number, start: number, steps: number): number[] { - if (end === start) { - return [end, start]; - } - const result = []; for (let i = 0; i < steps; i += 1) { result.push(lerp(start, end, i / (steps - 1))); @@ -46,10 +51,6 @@ export function linearInterpolator(end: number, start: number, steps: number): n * Generate inertial scroll values (exponentially slowing down). */ export function inertialInterpolator(end: number, start: number, steps: number): number[] { - if (end === start) { - return [end, start]; - } - const result = []; let factor = 1; for (let i = 0; i < steps - 1; i += 1) { diff --git a/website/docs/14.x/docs/api/events/user-event.mdx b/website/docs/14.x/docs/api/events/user-event.mdx index 2c7f5fcd6..984f74015 100644 --- a/website/docs/14.x/docs/api/events/user-event.mdx +++ b/website/docs/14.x/docs/api/events/user-event.mdx @@ -286,13 +286,13 @@ The sequence of events depends on whether the scroll includes an optional moment - `contentSizeChange` - `scrollBeginDrag` -- `scroll` (multiple events) +- `scroll` (zero or more events; none if the scroll offset does not change) - `scrollEndDrag` **Momentum scroll (optional)**: - `momentumScrollBegin` -- `scroll` (multiple events) +- `scroll` (zero or more events; none if the scroll offset does not change) - `momentumScrollEnd` ## `pullToRefresh()` \{#pull-to-refresh}