Skip to content

Commit

Permalink
feat: user event scroll (#1445)
Browse files Browse the repository at this point in the history
* feat: setup dummy scroll endpoint

* chore: use ScrollEventBuilder

* chore: enhance API endpoint with parameters

* chore: emit multiple scroll events

* fix: intermediate steps offset values

* feat: momentum scroll

* chore: add scroll to userEvent

* feat: scrollToTop

* feat: remember scroll position for element

* fix: setting and getting scroll state

* chore: validate host component type

* chore: validate and emit intermediate callbacks in scrollToTop

* chore: fix ts

* refactor: rename scroll to scrollTo

* refactor: y & x scrollTo params

* refactor: refactor y, x params to support explicit steps

* chore: fix typecheck

* refactor: improve typing

* refactor: momentumY, momentumX options

* docs: initial docs for `scrollTo`

* refactor: descope `scrollToTop` variant

* docs: tweaks

* refactor: improve TS typing by separating vertical/horizontal scroll

* refactor: wrap scroll event in nativeEvent wrapper

* refactor: clean up implementation

* chore: add FlatList tests

* refactor: remove explicit steps

* refactor: introduce inertial interpolator for momentum scroll

* docs: update docs

* chore: update snapshots

* refactor: code review changes

* feat: validate scroll direction

* docs: tweaks

* docs: tweak

* refactor: add more tests to improve code cov

* refactor: improve errors

* refactor: code review tweaks

* refactor: add wait calls

* refactor: tweaks, tweaks, tweaks

* refactor: final tweaks

---------

Co-authored-by: Maciej Jastrzębski <mdjastrzebski@gmail.com>
  • Loading branch information
siepra and mdjastrzebski committed Sep 13, 2023
1 parent 3b9c4da commit a7ea95d
Show file tree
Hide file tree
Showing 19 changed files with 1,017 additions and 3 deletions.
9 changes: 8 additions & 1 deletion src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,19 @@ test('resetToDefaults() resets config to defaults', () => {

test('resetToDefaults() resets internal config to defaults', () => {
configureInternal({
hostComponentNames: { text: 'A', textInput: 'A', switch: 'A', modal: 'A' },
hostComponentNames: {
text: 'A',
textInput: 'A',
switch: 'A',
scrollView: 'A',
modal: 'A',
},
});
expect(getConfig().hostComponentNames).toEqual({
text: 'A',
textInput: 'A',
switch: 'A',
scrollView: 'A',
modal: 'A',
});

Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/host-component-names.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe('getHostComponentNames', () => {
text: 'banana',
textInput: 'banana',
switch: 'banana',
scrollView: 'banana',
modal: 'banana',
},
});
Expand All @@ -29,6 +30,7 @@ describe('getHostComponentNames', () => {
text: 'banana',
textInput: 'banana',
switch: 'banana',
scrollView: 'banana',
modal: 'banana',
});
});
Expand All @@ -42,6 +44,7 @@ describe('getHostComponentNames', () => {
text: 'Text',
textInput: 'TextInput',
switch: 'RCTSwitch',
scrollView: 'RCTScrollView',
modal: 'Modal',
});
expect(getConfig().hostComponentNames).toBe(hostComponentNames);
Expand Down Expand Up @@ -71,6 +74,7 @@ describe('configureHostComponentNamesIfNeeded', () => {
text: 'Text',
textInput: 'TextInput',
switch: 'RCTSwitch',
scrollView: 'RCTScrollView',
modal: 'Modal',
});
});
Expand All @@ -81,6 +85,7 @@ describe('configureHostComponentNamesIfNeeded', () => {
text: 'banana',
textInput: 'banana',
switch: 'banana',
scrollView: 'banana',
modal: 'banana',
},
});
Expand All @@ -91,6 +96,7 @@ describe('configureHostComponentNamesIfNeeded', () => {
text: 'banana',
textInput: 'banana',
switch: 'banana',
scrollView: 'banana',
modal: 'banana',
});
});
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type HostComponentNames = {
text: string;
textInput: string;
switch: string;
scrollView: string;
modal: string;
};

Expand Down
14 changes: 13 additions & 1 deletion src/helpers/host-component-names.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { ReactTestInstance } from 'react-test-renderer';
import { Modal, Switch, Text, TextInput, View } from 'react-native';
import { Modal, ScrollView, Switch, Text, TextInput, View } from 'react-native';
import { configureInternal, getConfig, HostComponentNames } from '../config';
import { renderWithAct } from '../render-act';
import { HostTestInstance } from './component-tree';
Expand Down Expand Up @@ -35,6 +35,7 @@ function detectHostComponentNames(): HostComponentNames {
<Text testID="text">Hello</Text>
<TextInput testID="textInput" />
<Switch testID="switch" />
<ScrollView testID="scrollView" />
<Modal testID="modal" />
</View>
);
Expand All @@ -43,6 +44,7 @@ function detectHostComponentNames(): HostComponentNames {
text: getByTestId(renderer.root, 'text').type as string,
textInput: getByTestId(renderer.root, 'textInput').type as string,
switch: getByTestId(renderer.root, 'switch').type as string,
scrollView: getByTestId(renderer.root, 'scrollView').type as string,
modal: getByTestId(renderer.root, 'modal').type as string,
};
} catch (error) {
Expand Down Expand Up @@ -89,6 +91,16 @@ export function isHostTextInput(
return element?.type === getHostComponentNames().textInput;
}

/**
* Checks if the given element is a host ScrollView.
* @param element The element to check.
*/
export function isHostScrollView(
element?: ReactTestInstance | null
): element is HostTestInstance {
return element?.type === getHostComponentNames().scrollView;
}

/**
* Checks if the given element is a host Modal.
* @param element The element to check.
Expand Down
10 changes: 10 additions & 0 deletions src/helpers/object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function pick<T extends {}>(object: T, keys: (keyof T)[]): Partial<T> {
const result: Partial<T> = {};
keys.forEach((key) => {
if (object[key] !== undefined) {
result[key] = object[key];
}
});

return result;
}
2 changes: 1 addition & 1 deletion src/test-utils/events.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
interface EventEntry {
export interface EventEntry {
name: string;
payload: any;
}
Expand Down
2 changes: 2 additions & 0 deletions src/user-event/event-builder/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { CommonEventBuilder } from './common';
import { ScrollViewEventBuilder } from './scroll-view';
import { TextInputEventBuilder } from './text-input';

export const EventBuilder = {
Common: CommonEventBuilder,
ScrollView: ScrollViewEventBuilder,
TextInput: TextInputEventBuilder,
};
32 changes: 32 additions & 0 deletions src/user-event/event-builder/scroll-view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* 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.
*/
export interface ContentOffset {
y: number;
x: number;
}

export const ScrollViewEventBuilder = {
scroll: (offset: ContentOffset = { y: 0, x: 0 }) => {
return {
nativeEvent: {
contentInset: { bottom: 0, left: 0, right: 0, top: 0 },
contentOffset: { y: offset.y, x: offset.x },
contentSize: { height: 0, width: 0 },
layoutMeasurement: {
height: 0,
width: 0,
},
responderIgnoreScroll: true,
target: 0,
velocity: { y: 0, x: 0 },
},
};
},
};
3 changes: 3 additions & 0 deletions src/user-event/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ReactTestInstance } from 'react-test-renderer';
import { setup } from './setup';
import { PressOptions } from './press';
import { TypeOptions } from './type';
import { ScrollToOptions } from './scroll';

export { UserEventConfig } from './setup';

Expand All @@ -15,4 +16,6 @@ export const userEvent = {
type: (element: ReactTestInstance, text: string, options?: TypeOptions) =>
setup().type(element, text, options),
clear: (element: ReactTestInstance) => setup().clear(element),
scrollTo: (element: ReactTestInstance, options: ScrollToOptions) =>
setup().scrollTo(element, options),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`scrollTo() with FlatList supports vertical drag scroll: scrollTo({ y: 100 }) 1`] = `
[
{
"name": "scrollBeginDrag",
"payload": {
"nativeEvent": {
"contentInset": {
"bottom": 0,
"left": 0,
"right": 0,
"top": 0,
},
"contentOffset": {
"x": 0,
"y": 0,
},
"contentSize": {
"height": 0,
"width": 0,
},
"layoutMeasurement": {
"height": 0,
"width": 0,
},
"responderIgnoreScroll": true,
"target": 0,
"velocity": {
"x": 0,
"y": 0,
},
},
},
},
{
"name": "scroll",
"payload": {
"nativeEvent": {
"contentInset": {
"bottom": 0,
"left": 0,
"right": 0,
"top": 0,
},
"contentOffset": {
"x": 0,
"y": 25,
},
"contentSize": {
"height": 0,
"width": 0,
},
"layoutMeasurement": {
"height": 0,
"width": 0,
},
"responderIgnoreScroll": true,
"target": 0,
"velocity": {
"x": 0,
"y": 0,
},
},
},
},
{
"name": "scroll",
"payload": {
"nativeEvent": {
"contentInset": {
"bottom": 0,
"left": 0,
"right": 0,
"top": 0,
},
"contentOffset": {
"x": 0,
"y": 50,
},
"contentSize": {
"height": 0,
"width": 0,
},
"layoutMeasurement": {
"height": 0,
"width": 0,
},
"responderIgnoreScroll": true,
"target": 0,
"velocity": {
"x": 0,
"y": 0,
},
},
},
},
{
"name": "scroll",
"payload": {
"nativeEvent": {
"contentInset": {
"bottom": 0,
"left": 0,
"right": 0,
"top": 0,
},
"contentOffset": {
"x": 0,
"y": 75,
},
"contentSize": {
"height": 0,
"width": 0,
},
"layoutMeasurement": {
"height": 0,
"width": 0,
},
"responderIgnoreScroll": true,
"target": 0,
"velocity": {
"x": 0,
"y": 0,
},
},
},
},
{
"name": "scrollEndDrag",
"payload": {
"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,
},
},
},
},
]
`;
Loading

0 comments on commit a7ea95d

Please sign in to comment.