Skip to content

Commit f69d1d5

Browse files
refactor: simplify native state management (#1662)
* refactor: improve native state management * feat: add fireEvent.scroll support for scroll native events
1 parent 856332c commit f69d1d5

File tree

11 files changed

+75
-35
lines changed

11 files changed

+75
-35
lines changed

src/cleanup.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import { clearNativeState } from './native-state';
21
import { clearRenderResult } from './screen';
32

43
type CleanUpFunction = () => void;
54

65
const cleanupQueue = new Set<CleanUpFunction>();
76

87
export default function cleanup() {
9-
clearNativeState();
108
clearRenderResult();
119

1210
cleanupQueue.forEach((fn) => fn());

src/fire-event.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import {
88
} from 'react-native';
99
import act from './act';
1010
import { isHostElement } from './helpers/component-tree';
11-
import { isHostTextInput } from './helpers/host-component-names';
11+
import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
1212
import { isPointerEventEnabled } from './helpers/pointer-events';
1313
import { isTextInputEditable } from './helpers/text-input';
14-
import { StringWithAutocomplete } from './types';
14+
import { Point, StringWithAutocomplete } from './types';
1515
import { nativeState } from './native-state';
1616

1717
type EventHandler = (...args: unknown[]) => unknown;
@@ -147,13 +147,47 @@ fireEvent.scroll = (element: ReactTestInstance, ...data: unknown[]) =>
147147

148148
export default fireEvent;
149149

150+
const scrollEventNames = new Set([
151+
'scroll',
152+
'scrollBeginDrag',
153+
'scrollEndDrag',
154+
'momentumScrollBegin',
155+
'momentumScrollEnd',
156+
]);
157+
150158
function setNativeStateIfNeeded(element: ReactTestInstance, eventName: string, value: unknown) {
151159
if (
152160
eventName === 'changeText' &&
153161
typeof value === 'string' &&
154162
isHostTextInput(element) &&
155163
isTextInputEditable(element)
156164
) {
157-
nativeState?.valueForElement.set(element, value);
165+
nativeState.valueForElement.set(element, value);
166+
}
167+
168+
if (scrollEventNames.has(eventName) && isHostScrollView(element)) {
169+
const contentOffset = tryGetContentOffset(value);
170+
if (contentOffset) {
171+
nativeState.contentOffsetForElement.set(element, contentOffset);
172+
}
158173
}
159174
}
175+
176+
function tryGetContentOffset(value: unknown): Point | null {
177+
try {
178+
// @ts-expect-error: try to extract contentOffset from the event value
179+
const contentOffset = value?.nativeEvent?.contentOffset;
180+
const x = contentOffset?.x;
181+
const y = contentOffset?.y;
182+
if (typeof x === 'number' || typeof y === 'number') {
183+
return {
184+
x: Number.isFinite(x) ? x : 0,
185+
y: Number.isFinite(y) ? y : 0,
186+
};
187+
}
188+
} catch {
189+
// Do nothing
190+
}
191+
192+
return null;
193+
}

src/helpers/matchers/match-label-text.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ export function matchLabelText(
1818

1919
function matchAccessibilityLabel(
2020
element: ReactTestInstance,
21-
extpectedLabel: TextMatch,
21+
expectedLabel: TextMatch,
2222
options: TextMatchOptions,
2323
) {
24-
return matches(extpectedLabel, computeAriaLabel(element), options.normalizer, options.exact);
24+
return matches(expectedLabel, computeAriaLabel(element), options.normalizer, options.exact);
2525
}
2626

2727
function matchAccessibilityLabelledBy(

src/helpers/text-input.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function getTextInputValue(element: ReactTestInstance) {
1717

1818
return (
1919
element.props.value ??
20-
nativeState?.valueForElement.get(element) ??
20+
nativeState.valueForElement.get(element) ??
2121
element.props.defaultValue ??
2222
''
2323
);

src/native-state.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,7 @@ export type NativeState = {
1111
contentOffsetForElement: WeakMap<ReactTestInstance, Point>;
1212
};
1313

14-
export let nativeState: NativeState | null = null;
15-
16-
export function initNativeState(): void {
17-
nativeState = {
18-
valueForElement: new WeakMap(),
19-
contentOffsetForElement: new WeakMap(),
20-
};
21-
}
22-
23-
export function clearNativeState(): void {
24-
nativeState = null;
25-
}
14+
export let nativeState: NativeState = {
15+
valueForElement: new WeakMap(),
16+
contentOffsetForElement: new WeakMap(),
17+
};

src/render.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { validateStringsRenderedWithinText } from './helpers/string-validation';
1212
import { renderWithAct } from './render-act';
1313
import { setRenderResult } from './screen';
1414
import { getQueriesForElement } from './within';
15-
import { initNativeState } from './native-state';
1615

1716
export interface RenderOptions {
1817
wrapper?: React.ComponentType<any>;
@@ -128,7 +127,6 @@ function buildRenderResult(
128127
});
129128

130129
setRenderResult(result);
131-
initNativeState();
132130

133131
return result;
134132
}

src/user-event/paste.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export async function paste(
3333
dispatchEvent(element, 'selectionChange', EventBuilder.TextInput.selectionChange(rangeToClear));
3434

3535
// 3. Paste the text
36-
nativeState?.valueForElement.set(element, text);
36+
nativeState.valueForElement.set(element, text);
3737
dispatchEvent(element, 'change', EventBuilder.TextInput.change(text));
3838
dispatchEvent(element, 'changeText', text);
3939

src/user-event/scroll/__tests__/scroll-to.test.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react';
22
import { ScrollView, ScrollViewProps, View } from 'react-native';
33
import { EventEntry, createEventLogger } from '../../../test-utils';
4-
import { render, screen } from '../../..';
4+
import { fireEvent, render, screen } from '../../..';
55
import { userEvent } from '../..';
66

77
function mapEventsToShortForm(events: EventEntry[]) {
@@ -103,7 +103,7 @@ describe('scrollTo()', () => {
103103
]);
104104
});
105105

106-
test('remembers previous scroll position', async () => {
106+
test('remembers previous scroll offset', async () => {
107107
const { events } = renderScrollViewWithToolkit();
108108
const user = userEvent.setup();
109109

@@ -123,6 +123,24 @@ describe('scrollTo()', () => {
123123
]);
124124
});
125125

126+
test('remembers previous scroll offset from "fireEvent.scroll"', async () => {
127+
const { events } = renderScrollViewWithToolkit();
128+
const user = userEvent.setup();
129+
130+
fireEvent.scroll(screen.getByTestId('scrollView'), {
131+
nativeEvent: { contentOffset: { y: 100 } },
132+
});
133+
await user.scrollTo(screen.getByTestId('scrollView'), { y: 200 });
134+
expect(mapEventsToShortForm(events)).toEqual([
135+
['scroll', 100, undefined],
136+
['scrollBeginDrag', 100, 0],
137+
['scroll', 125, 0],
138+
['scroll', 150, 0],
139+
['scroll', 175, 0],
140+
['scrollEndDrag', 200, 0],
141+
]);
142+
});
143+
126144
it('validates vertical scroll direction', async () => {
127145
renderScrollViewWithToolkit();
128146
const user = userEvent.setup();

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,24 +56,24 @@ export async function scrollTo(
5656
options.contentSize?.height ?? 0,
5757
);
5858

59-
const initialPosition = nativeState?.contentOffsetForElement.get(element) ?? { x: 0, y: 0 };
59+
const initialOffset = nativeState.contentOffsetForElement.get(element) ?? { x: 0, y: 0 };
6060
const dragSteps = createScrollSteps(
6161
{ y: options.y, x: options.x },
62-
initialPosition,
62+
initialOffset,
6363
linearInterpolator,
6464
);
6565
await emitDragScrollEvents(this.config, element, dragSteps, options);
6666

67-
const momentumStart = dragSteps.at(-1) ?? initialPosition;
67+
const momentumStart = dragSteps.at(-1) ?? initialOffset;
6868
const momentumSteps = createScrollSteps(
6969
{ y: options.momentumY, x: options.momentumX },
7070
momentumStart,
7171
inertialInterpolator,
7272
);
7373
await emitMomentumScrollEvents(this.config, element, momentumSteps, options);
7474

75-
const finalPosition = momentumSteps.at(-1) ?? dragSteps.at(-1) ?? initialPosition;
76-
nativeState?.contentOffsetForElement.set(element, finalPosition);
75+
const finalOffset = momentumSteps.at(-1) ?? dragSteps.at(-1) ?? initialOffset;
76+
nativeState.contentOffsetForElement.set(element, finalOffset);
7777
}
7878

7979
async function emitDragScrollEvents(

src/user-event/type/type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export async function emitTypingEvents(
9595
return;
9696
}
9797

98-
nativeState?.valueForElement.set(element, text);
98+
nativeState.valueForElement.set(element, text);
9999
dispatchEvent(element, 'change', EventBuilder.TextInput.change(text));
100100
dispatchEvent(element, 'changeText', text);
101101

website/docs/12.x/docs/api/events/user-event.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,10 @@ Each scroll interaction consists of a mandatory drag scroll part, which simulate
269269
270270
### Options {#scroll-to-options}
271271
272-
- `y` - target vertical drag scroll position
273-
- `x` - target horizontal drag scroll position
274-
- `momentumY` - target vertical momentum scroll position
275-
- `momentumX` - target horizontal momentum scroll position
272+
- `y` - target vertical drag scroll offset
273+
- `x` - target horizontal drag scroll offset
274+
- `momentumY` - target vertical momentum scroll offset
275+
- `momentumX` - target horizontal momentum scroll offset
276276
- `contentSize` - passed to `ScrollView` events and enabling `FlatList` updates
277277
- `layoutMeasurement` - passed to `ScrollView` events and enabling `FlatList` updates
278278

0 commit comments

Comments
 (0)