Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to pass contentSize and layoutMeasurement when calling scrollTo #1543

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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