Skip to content

Commit

Permalink
Add option to pass contentSize and layoutMeasurement when calling…
Browse files Browse the repository at this point in the history
… `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 <mdjastrzebski@gmail.com>
  • Loading branch information
j-piasecki and mdjastrzebski committed Dec 15, 2023
1 parent 3ba97e3 commit edf52b6
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 31 deletions.
39 changes: 29 additions & 10 deletions src/user-event/event-builder/scroll-view.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
/**
* Experimental values:
* - 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}`
* - 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}}`
*/

/**
* Scroll position of a scrollable element.
*/
Expand All @@ -12,16 +6,41 @@ export interface ContentOffset {
x: number;
}

/**
* Other options for constructing a scroll event.
*/
export type ScrollEventOptions = {
contentSize?: {
height: number;
width: number;
};
layoutMeasurement?: {
height: number;
width: number;
};
};

/**
* Experimental values:
* - 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}`
* - 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}}`
*/
export const ScrollViewEventBuilder = {
scroll: (offset: ContentOffset = { y: 0, x: 0 }) => {
scroll: (
offset: ContentOffset = { y: 0, x: 0 },
options?: ScrollEventOptions
) => {
return {
nativeEvent: {
contentInset: { bottom: 0, left: 0, right: 0, top: 0 },
contentOffset: { y: offset.y, x: offset.x },
contentSize: { height: 0, width: 0 },
contentSize: {
height: options?.contentSize?.height ?? 0,
width: options?.contentSize?.width ?? 0,
},
layoutMeasurement: {
height: 0,
width: 0,
height: options?.layoutMeasurement?.height ?? 0,
width: options?.layoutMeasurement?.width ?? 0,
},
responderIgnoreScroll: true,
target: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as React from 'react';
import { FlatList, ScrollViewProps, Text } from 'react-native';
import { EventEntry, createEventLogger } from '../../../test-utils';
import { FlatList, ScrollViewProps, Text, View } from 'react-native';
import { render, screen } from '../../..';
import '../../../matchers/extend-expect';
import { EventEntry, createEventLogger } from '../../../test-utils';
import { userEvent } from '../..';

const data = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
Expand Down Expand Up @@ -68,3 +69,46 @@ describe('scrollTo() with FlatList', () => {
]);
});
});

const DATA = new Array(100).fill(0).map((_, i) => `Item ${i}`);

function Scrollable() {
return (
<View style={{ flex: 1 }}>
<FlatList
testID="flat-list"
data={DATA}
renderItem={(x) => <Item title={x.item} />}
initialNumToRender={10}
updateCellsBatchingPeriod={0}
/>
</View>
);
}

function Item({ title }: { title: string }) {
return (
<View>
<Text>{title}</Text>
</View>
);
}

test('scrollTo with contentSize and layoutMeasurement update FlatList content', async () => {
render(<Scrollable />);
const user = userEvent.setup();

expect(screen.getByText('Item 0')).toBeOnTheScreen();
expect(screen.getByText('Item 7')).toBeOnTheScreen();
expect(screen.queryByText('Item 15')).not.toBeOnTheScreen();

await user.scrollTo(screen.getByTestId('flat-list'), {
y: 300,
contentSize: { width: 240, height: 480 },
layoutMeasurement: { width: 240, height: 480 },
});

expect(screen.getByText('Item 0')).toBeOnTheScreen();
expect(screen.getByText('Item 7')).toBeOnTheScreen();
expect(screen.getByText('Item 15')).toBeOnTheScreen();
});
46 changes: 33 additions & 13 deletions src/user-event/scroll/scroll-to.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,27 @@ import { EventBuilder } from '../event-builder';
import { ErrorWithStack } from '../../helpers/errors';
import { isHostScrollView } from '../../helpers/host-component-names';
import { pick } from '../../helpers/object';
import { dispatchEvent, wait } from '../utils';
import { ContentOffset } from '../event-builder/scroll-view';
import { dispatchEvent, wait } from '../utils';
import {
createScrollSteps,
inertialInterpolator,
linearInterpolator,
} from './utils';
import { getElementScrollOffset, setElementScrollOffset } from './state';

export interface VerticalScrollToOptions {
interface CommonScrollToOptions {
contentSize?: {
height: number;
width: number;
};
layoutMeasurement?: {
height: number;
width: number;
};
}

export interface VerticalScrollToOptions extends CommonScrollToOptions {
y: number;
momentumY?: number;

Expand All @@ -23,7 +34,7 @@ export interface VerticalScrollToOptions {
momentumX?: never;
}

export interface HorizontalScrollToOptions {
export interface HorizontalScrollToOptions extends CommonScrollToOptions {
x: number;
momentumX?: number;

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

ensureScrollViewDirection(element, options);

dispatchEvent(
element,
'contentSizeChange',
options.contentSize?.width ?? 0,
options.contentSize?.height ?? 0
);

const initialPosition = getElementScrollOffset(element);
const dragSteps = createScrollSteps(
{ y: options.y, x: options.x },
initialPosition,
linearInterpolator
);
await emitDragScrollEvents(this.config, element, dragSteps);
await emitDragScrollEvents(this.config, element, dragSteps, options);

const momentumStart = dragSteps.at(-1) ?? initialPosition;
const momentumSteps = createScrollSteps(
{ y: options.momentumY, x: options.momentumX },
momentumStart,
inertialInterpolator
);
await emitMomentumScrollEvents(this.config, element, momentumSteps);
await emitMomentumScrollEvents(this.config, element, momentumSteps, options);

const finalPosition =
momentumSteps.at(-1) ?? dragSteps.at(-1) ?? initialPosition;
Expand All @@ -74,7 +92,8 @@ export async function scrollTo(
async function emitDragScrollEvents(
config: UserEventConfig,
element: ReactTestInstance,
scrollSteps: ContentOffset[]
scrollSteps: ContentOffset[],
scrollOptions: ScrollToOptions
) {
if (scrollSteps.length === 0) {
return;
Expand All @@ -84,7 +103,7 @@ async function emitDragScrollEvents(
dispatchEvent(
element,
'scrollBeginDrag',
EventBuilder.ScrollView.scroll(scrollSteps[0])
EventBuilder.ScrollView.scroll(scrollSteps[0], scrollOptions)
);

// Note: experimentally, in case of drag scroll the last scroll step
Expand All @@ -95,7 +114,7 @@ async function emitDragScrollEvents(
dispatchEvent(
element,
'scroll',
EventBuilder.ScrollView.scroll(scrollSteps[i])
EventBuilder.ScrollView.scroll(scrollSteps[i], scrollOptions)
);
}

Expand All @@ -104,14 +123,15 @@ async function emitDragScrollEvents(
dispatchEvent(
element,
'scrollEndDrag',
EventBuilder.ScrollView.scroll(lastStep)
EventBuilder.ScrollView.scroll(lastStep, scrollOptions)
);
}

async function emitMomentumScrollEvents(
config: UserEventConfig,
element: ReactTestInstance,
scrollSteps: ContentOffset[]
scrollSteps: ContentOffset[],
scrollOptions: ScrollToOptions
) {
if (scrollSteps.length === 0) {
return;
Expand All @@ -121,7 +141,7 @@ async function emitMomentumScrollEvents(
dispatchEvent(
element,
'momentumScrollBegin',
EventBuilder.ScrollView.scroll(scrollSteps[0])
EventBuilder.ScrollView.scroll(scrollSteps[0], scrollOptions)
);

// Note: experimentally, in case of momentum scroll the last scroll step
Expand All @@ -132,7 +152,7 @@ async function emitMomentumScrollEvents(
dispatchEvent(
element,
'scroll',
EventBuilder.ScrollView.scroll(scrollSteps[i])
EventBuilder.ScrollView.scroll(scrollSteps[i], scrollOptions)
);
}

Expand All @@ -141,7 +161,7 @@ async function emitMomentumScrollEvents(
dispatchEvent(
element,
'momentumScrollEnd',
EventBuilder.ScrollView.scroll(lastStep)
EventBuilder.ScrollView.scroll(lastStep, scrollOptions)
);
}

Expand Down
6 changes: 3 additions & 3 deletions src/user-event/utils/dispatch-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import act from '../../act';
*
* @param element element trigger event on
* @param eventName name of the event
* @param event event payload
* @param event event payload(s)
*/
export function dispatchEvent(
element: ReactTestInstance,
eventName: string,
event: unknown
...event: unknown[]
) {
const handler = getEventHandler(element, eventName);
if (!handler) {
Expand All @@ -20,7 +20,7 @@ export function dispatchEvent(

// This will be called synchronously.
void act(() => {
handler(event);
handler(...event);
});
}

Expand Down
14 changes: 11 additions & 3 deletions website/docs/UserEvent.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,13 @@ scrollTo(
options: {
y: number,
momentumY?: number,
contentSize?: { width: number, height: number },
layoutMeasurement?: { width: number, height: number },
} | {
x: number,
momentumX?: number,
contentSize?: { width: number, height: number },
layoutMeasurement?: { width: number, height: number },
}
```
Expand All @@ -219,22 +223,26 @@ await user.scrollTo(scrollView, { y: 100, momentumY: 200 });
This helper simulates user scrolling a host `ScrollView` element.
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.
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.
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.
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).
### Options {#type-options}
### Options {#scroll-to-options}
- `y` - target vertical drag scroll position
- `x` - target horizontal drag scroll position
- `momentumY` - target vertical momentum scroll position
- `momentumX` - target horizontal momentum scroll position
- `contentSize` - passed to `ScrollView` events and enabling `FlatList` updates
- `layoutMeasurement` - passed to `ScrollView` events and enabling `FlatList` updates
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.
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 }`.
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 }`.
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.
### Sequence of events
Expand Down

0 comments on commit edf52b6

Please sign in to comment.