Skip to content

Commit edf52b6

Browse files
Add option to pass contentSize and layoutMeasurement when calling scrollTo (#1543)
* Add option to pass `contentSize` and `layoutMeasurement` when calling `scrollTo` * Update docs * Add tests * refactor: code review changes * refactor: code review changes * refactor: code review changes * docs: fix typo --------- Co-authored-by: Maciej Jastrzebski <[email protected]>
1 parent 3ba97e3 commit edf52b6

File tree

8 files changed

+122
-31
lines changed

8 files changed

+122
-31
lines changed

src/user-event/event-builder/scroll-view.ts

+29-10
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
/**
2-
* Experimental values:
3-
* - iOS: `{"contentInset": {"bottom": 0, "left": 0, "right": 0, "top": 0}, "contentOffset": {"x": 0, "y": 5.333333333333333}, "contentSize": {"height": 1676.6666259765625, "width": 390}, "layoutMeasurement": {"height": 753, "width": 390}, "zoomScale": 1}`
4-
* - Android: `{"contentInset": {"bottom": 0, "left": 0, "right": 0, "top": 0}, "contentOffset": {"x": 0, "y": 31.619047164916992}, "contentSize": {"height": 1624.761962890625, "width": 411.4285583496094}, "layoutMeasurement": {"height": 785.5238037109375, "width": 411.4285583496094}, "responderIgnoreScroll": true, "target": 139, "velocity": {"x": -1.3633992671966553, "y": -1.3633992671966553}}`
5-
*/
6-
71
/**
82
* Scroll position of a scrollable element.
93
*/
@@ -12,16 +6,41 @@ export interface ContentOffset {
126
x: number;
137
}
148

9+
/**
10+
* Other options for constructing a scroll event.
11+
*/
12+
export type ScrollEventOptions = {
13+
contentSize?: {
14+
height: number;
15+
width: number;
16+
};
17+
layoutMeasurement?: {
18+
height: number;
19+
width: number;
20+
};
21+
};
22+
23+
/**
24+
* Experimental values:
25+
* - iOS: `{"contentInset": {"bottom": 0, "left": 0, "right": 0, "top": 0}, "contentOffset": {"x": 0, "y": 5.333333333333333}, "contentSize": {"height": 1676.6666259765625, "width": 390}, "layoutMeasurement": {"height": 753, "width": 390}, "zoomScale": 1}`
26+
* - Android: `{"contentInset": {"bottom": 0, "left": 0, "right": 0, "top": 0}, "contentOffset": {"x": 0, "y": 31.619047164916992}, "contentSize": {"height": 1624.761962890625, "width": 411.4285583496094}, "layoutMeasurement": {"height": 785.5238037109375, "width": 411.4285583496094}, "responderIgnoreScroll": true, "target": 139, "velocity": {"x": -1.3633992671966553, "y": -1.3633992671966553}}`
27+
*/
1528
export const ScrollViewEventBuilder = {
16-
scroll: (offset: ContentOffset = { y: 0, x: 0 }) => {
29+
scroll: (
30+
offset: ContentOffset = { y: 0, x: 0 },
31+
options?: ScrollEventOptions
32+
) => {
1733
return {
1834
nativeEvent: {
1935
contentInset: { bottom: 0, left: 0, right: 0, top: 0 },
2036
contentOffset: { y: offset.y, x: offset.x },
21-
contentSize: { height: 0, width: 0 },
37+
contentSize: {
38+
height: options?.contentSize?.height ?? 0,
39+
width: options?.contentSize?.width ?? 0,
40+
},
2241
layoutMeasurement: {
23-
height: 0,
24-
width: 0,
42+
height: options?.layoutMeasurement?.height ?? 0,
43+
width: options?.layoutMeasurement?.width ?? 0,
2544
},
2645
responderIgnoreScroll: true,
2746
target: 0,

src/user-event/scroll/__tests__/scrollTo-flatList.tsx renamed to src/user-event/scroll/__tests__/scroll-to-flat-list.test.tsx

+46-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as React from 'react';
2-
import { FlatList, ScrollViewProps, Text } from 'react-native';
3-
import { EventEntry, createEventLogger } from '../../../test-utils';
2+
import { FlatList, ScrollViewProps, Text, View } from 'react-native';
43
import { render, screen } from '../../..';
4+
import '../../../matchers/extend-expect';
5+
import { EventEntry, createEventLogger } from '../../../test-utils';
56
import { userEvent } from '../..';
67

78
const data = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
@@ -68,3 +69,46 @@ describe('scrollTo() with FlatList', () => {
6869
]);
6970
});
7071
});
72+
73+
const DATA = new Array(100).fill(0).map((_, i) => `Item ${i}`);
74+
75+
function Scrollable() {
76+
return (
77+
<View style={{ flex: 1 }}>
78+
<FlatList
79+
testID="flat-list"
80+
data={DATA}
81+
renderItem={(x) => <Item title={x.item} />}
82+
initialNumToRender={10}
83+
updateCellsBatchingPeriod={0}
84+
/>
85+
</View>
86+
);
87+
}
88+
89+
function Item({ title }: { title: string }) {
90+
return (
91+
<View>
92+
<Text>{title}</Text>
93+
</View>
94+
);
95+
}
96+
97+
test('scrollTo with contentSize and layoutMeasurement update FlatList content', async () => {
98+
render(<Scrollable />);
99+
const user = userEvent.setup();
100+
101+
expect(screen.getByText('Item 0')).toBeOnTheScreen();
102+
expect(screen.getByText('Item 7')).toBeOnTheScreen();
103+
expect(screen.queryByText('Item 15')).not.toBeOnTheScreen();
104+
105+
await user.scrollTo(screen.getByTestId('flat-list'), {
106+
y: 300,
107+
contentSize: { width: 240, height: 480 },
108+
layoutMeasurement: { width: 240, height: 480 },
109+
});
110+
111+
expect(screen.getByText('Item 0')).toBeOnTheScreen();
112+
expect(screen.getByText('Item 7')).toBeOnTheScreen();
113+
expect(screen.getByText('Item 15')).toBeOnTheScreen();
114+
});

src/user-event/scroll/scroll-to.ts

+33-13
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,27 @@ import { EventBuilder } from '../event-builder';
55
import { ErrorWithStack } from '../../helpers/errors';
66
import { isHostScrollView } from '../../helpers/host-component-names';
77
import { pick } from '../../helpers/object';
8-
import { dispatchEvent, wait } from '../utils';
98
import { ContentOffset } from '../event-builder/scroll-view';
9+
import { dispatchEvent, wait } from '../utils';
1010
import {
1111
createScrollSteps,
1212
inertialInterpolator,
1313
linearInterpolator,
1414
} from './utils';
1515
import { getElementScrollOffset, setElementScrollOffset } from './state';
1616

17-
export interface VerticalScrollToOptions {
17+
interface CommonScrollToOptions {
18+
contentSize?: {
19+
height: number;
20+
width: number;
21+
};
22+
layoutMeasurement?: {
23+
height: number;
24+
width: number;
25+
};
26+
}
27+
28+
export interface VerticalScrollToOptions extends CommonScrollToOptions {
1829
y: number;
1930
momentumY?: number;
2031

@@ -23,7 +34,7 @@ export interface VerticalScrollToOptions {
2334
momentumX?: never;
2435
}
2536

26-
export interface HorizontalScrollToOptions {
37+
export interface HorizontalScrollToOptions extends CommonScrollToOptions {
2738
x: number;
2839
momentumX?: number;
2940

@@ -50,21 +61,28 @@ export async function scrollTo(
5061

5162
ensureScrollViewDirection(element, options);
5263

64+
dispatchEvent(
65+
element,
66+
'contentSizeChange',
67+
options.contentSize?.width ?? 0,
68+
options.contentSize?.height ?? 0
69+
);
70+
5371
const initialPosition = getElementScrollOffset(element);
5472
const dragSteps = createScrollSteps(
5573
{ y: options.y, x: options.x },
5674
initialPosition,
5775
linearInterpolator
5876
);
59-
await emitDragScrollEvents(this.config, element, dragSteps);
77+
await emitDragScrollEvents(this.config, element, dragSteps, options);
6078

6179
const momentumStart = dragSteps.at(-1) ?? initialPosition;
6280
const momentumSteps = createScrollSteps(
6381
{ y: options.momentumY, x: options.momentumX },
6482
momentumStart,
6583
inertialInterpolator
6684
);
67-
await emitMomentumScrollEvents(this.config, element, momentumSteps);
85+
await emitMomentumScrollEvents(this.config, element, momentumSteps, options);
6886

6987
const finalPosition =
7088
momentumSteps.at(-1) ?? dragSteps.at(-1) ?? initialPosition;
@@ -74,7 +92,8 @@ export async function scrollTo(
7492
async function emitDragScrollEvents(
7593
config: UserEventConfig,
7694
element: ReactTestInstance,
77-
scrollSteps: ContentOffset[]
95+
scrollSteps: ContentOffset[],
96+
scrollOptions: ScrollToOptions
7897
) {
7998
if (scrollSteps.length === 0) {
8099
return;
@@ -84,7 +103,7 @@ async function emitDragScrollEvents(
84103
dispatchEvent(
85104
element,
86105
'scrollBeginDrag',
87-
EventBuilder.ScrollView.scroll(scrollSteps[0])
106+
EventBuilder.ScrollView.scroll(scrollSteps[0], scrollOptions)
88107
);
89108

90109
// Note: experimentally, in case of drag scroll the last scroll step
@@ -95,7 +114,7 @@ async function emitDragScrollEvents(
95114
dispatchEvent(
96115
element,
97116
'scroll',
98-
EventBuilder.ScrollView.scroll(scrollSteps[i])
117+
EventBuilder.ScrollView.scroll(scrollSteps[i], scrollOptions)
99118
);
100119
}
101120

@@ -104,14 +123,15 @@ async function emitDragScrollEvents(
104123
dispatchEvent(
105124
element,
106125
'scrollEndDrag',
107-
EventBuilder.ScrollView.scroll(lastStep)
126+
EventBuilder.ScrollView.scroll(lastStep, scrollOptions)
108127
);
109128
}
110129

111130
async function emitMomentumScrollEvents(
112131
config: UserEventConfig,
113132
element: ReactTestInstance,
114-
scrollSteps: ContentOffset[]
133+
scrollSteps: ContentOffset[],
134+
scrollOptions: ScrollToOptions
115135
) {
116136
if (scrollSteps.length === 0) {
117137
return;
@@ -121,7 +141,7 @@ async function emitMomentumScrollEvents(
121141
dispatchEvent(
122142
element,
123143
'momentumScrollBegin',
124-
EventBuilder.ScrollView.scroll(scrollSteps[0])
144+
EventBuilder.ScrollView.scroll(scrollSteps[0], scrollOptions)
125145
);
126146

127147
// Note: experimentally, in case of momentum scroll the last scroll step
@@ -132,7 +152,7 @@ async function emitMomentumScrollEvents(
132152
dispatchEvent(
133153
element,
134154
'scroll',
135-
EventBuilder.ScrollView.scroll(scrollSteps[i])
155+
EventBuilder.ScrollView.scroll(scrollSteps[i], scrollOptions)
136156
);
137157
}
138158

@@ -141,7 +161,7 @@ async function emitMomentumScrollEvents(
141161
dispatchEvent(
142162
element,
143163
'momentumScrollEnd',
144-
EventBuilder.ScrollView.scroll(lastStep)
164+
EventBuilder.ScrollView.scroll(lastStep, scrollOptions)
145165
);
146166
}
147167

src/user-event/utils/dispatch-event.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import act from '../../act';
66
*
77
* @param element element trigger event on
88
* @param eventName name of the event
9-
* @param event event payload
9+
* @param event event payload(s)
1010
*/
1111
export function dispatchEvent(
1212
element: ReactTestInstance,
1313
eventName: string,
14-
event: unknown
14+
...event: unknown[]
1515
) {
1616
const handler = getEventHandler(element, eventName);
1717
if (!handler) {
@@ -20,7 +20,7 @@ export function dispatchEvent(
2020

2121
// This will be called synchronously.
2222
void act(() => {
23-
handler(event);
23+
handler(...event);
2424
});
2525
}
2626

website/docs/UserEvent.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,13 @@ scrollTo(
204204
options: {
205205
y: number,
206206
momentumY?: number,
207+
contentSize?: { width: number, height: number },
208+
layoutMeasurement?: { width: number, height: number },
207209
} | {
208210
x: number,
209211
momentumX?: number,
212+
contentSize?: { width: number, height: number },
213+
layoutMeasurement?: { width: number, height: number },
210214
}
211215
```
212216
@@ -219,22 +223,26 @@ await user.scrollTo(scrollView, { y: 100, momentumY: 200 });
219223
220224
This helper simulates user scrolling a host `ScrollView` element.
221225
222-
This function supports only host `ScrollView` elements, passing other element types will result in error. Note that `FlatList` is accepted as it renders to a host `ScrolLView` element, however in the current iteration we focus only on base `ScrollView` only features.
226+
This function supports only host `ScrollView` elements, passing other element types will result in error. Note that `FlatList` is accepted as it renders to a host `ScrolLView` element.
223227
224228
Scroll interaction should match `ScrollView` element direction. For vertical scroll view (default or explicit `horizontal={false}`) you should pass only `y` (and optionally also `momentumY`) option, for horizontal scroll view (`horizontal={true}`) you should pass only `x` (and optionally `momentumX`) option.
225229
226230
Each scroll interaction consists of a mandatory drag scroll part which simulates user dragging the scroll view with his finger (`y` or `x` option). This may optionally be followed by a momentum scroll movement which simulates the inertial movement of scroll view content after the user lifts his finger up (`momentumY` or `momentumX` options).
227231
228-
### Options {#type-options}
232+
### Options {#scroll-to-options}
229233
230234
- `y` - target vertical drag scroll position
231235
- `x` - target horizontal drag scroll position
232236
- `momentumY` - target vertical momentum scroll position
233237
- `momentumX` - target horizontal momentum scroll position
238+
- `contentSize` - passed to `ScrollView` events and enabling `FlatList` updates
239+
- `layoutMeasurement` - passed to `ScrollView` events and enabling `FlatList` updates
234240
235241
User Event will generate a number of intermediate scroll steps to simulate user scroll interaction. You should not rely on exact number or values of these scrolls steps as they might be change in the future version.
236242
237-
This function will remember where the last scroll ended, so subsequent scroll interaction will starts from that positition. The initial scroll position will be assumed to be `{ y: 0, x: 0 }`.
243+
This function will remember where the last scroll ended, so subsequent scroll interaction will starts from that position. The initial scroll position will be assumed to be `{ y: 0, x: 0 }`.
244+
245+
In order to simulate a `FlatList` (and other controls based on `VirtualizedList`) scrolling, you should pass the `contentSize` and `layoutMeasurement` options, which enable the underlying logic to update the currently visible window.
238246
239247
### Sequence of events
240248

0 commit comments

Comments
 (0)