Skip to content

Commit

Permalink
refactor: simplify native state management (#1662)
Browse files Browse the repository at this point in the history
* refactor: improve native state management

* feat: add fireEvent.scroll support for scroll native events
  • Loading branch information
mdjastrzebski committed Sep 9, 2024
1 parent 856332c commit f69d1d5
Show file tree
Hide file tree
Showing 11 changed files with 75 additions and 35 deletions.
2 changes: 0 additions & 2 deletions src/cleanup.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { clearNativeState } from './native-state';
import { clearRenderResult } from './screen';

type CleanUpFunction = () => void;

const cleanupQueue = new Set<CleanUpFunction>();

export default function cleanup() {
clearNativeState();
clearRenderResult();

cleanupQueue.forEach((fn) => fn());
Expand Down
40 changes: 37 additions & 3 deletions src/fire-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
} from 'react-native';
import act from './act';
import { isHostElement } from './helpers/component-tree';
import { isHostTextInput } from './helpers/host-component-names';
import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
import { isPointerEventEnabled } from './helpers/pointer-events';
import { isTextInputEditable } from './helpers/text-input';
import { StringWithAutocomplete } from './types';
import { Point, StringWithAutocomplete } from './types';
import { nativeState } from './native-state';

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

export default fireEvent;

const scrollEventNames = new Set([
'scroll',
'scrollBeginDrag',
'scrollEndDrag',
'momentumScrollBegin',
'momentumScrollEnd',
]);

function setNativeStateIfNeeded(element: ReactTestInstance, eventName: string, value: unknown) {
if (
eventName === 'changeText' &&
typeof value === 'string' &&
isHostTextInput(element) &&
isTextInputEditable(element)
) {
nativeState?.valueForElement.set(element, value);
nativeState.valueForElement.set(element, value);
}

if (scrollEventNames.has(eventName) && isHostScrollView(element)) {
const contentOffset = tryGetContentOffset(value);
if (contentOffset) {
nativeState.contentOffsetForElement.set(element, contentOffset);
}
}
}

function tryGetContentOffset(value: unknown): Point | null {
try {
// @ts-expect-error: try to extract contentOffset from the event value
const contentOffset = value?.nativeEvent?.contentOffset;
const x = contentOffset?.x;
const y = contentOffset?.y;
if (typeof x === 'number' || typeof y === 'number') {
return {
x: Number.isFinite(x) ? x : 0,
y: Number.isFinite(y) ? y : 0,
};
}
} catch {
// Do nothing
}

return null;
}
4 changes: 2 additions & 2 deletions src/helpers/matchers/match-label-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ export function matchLabelText(

function matchAccessibilityLabel(
element: ReactTestInstance,
extpectedLabel: TextMatch,
expectedLabel: TextMatch,
options: TextMatchOptions,
) {
return matches(extpectedLabel, computeAriaLabel(element), options.normalizer, options.exact);
return matches(expectedLabel, computeAriaLabel(element), options.normalizer, options.exact);
}

function matchAccessibilityLabelledBy(
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/text-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function getTextInputValue(element: ReactTestInstance) {

return (
element.props.value ??
nativeState?.valueForElement.get(element) ??
nativeState.valueForElement.get(element) ??
element.props.defaultValue ??
''
);
Expand Down
16 changes: 4 additions & 12 deletions src/native-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,7 @@ export type NativeState = {
contentOffsetForElement: WeakMap<ReactTestInstance, Point>;
};

export let nativeState: NativeState | null = null;

export function initNativeState(): void {
nativeState = {
valueForElement: new WeakMap(),
contentOffsetForElement: new WeakMap(),
};
}

export function clearNativeState(): void {
nativeState = null;
}
export let nativeState: NativeState = {
valueForElement: new WeakMap(),
contentOffsetForElement: new WeakMap(),
};
2 changes: 0 additions & 2 deletions src/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { validateStringsRenderedWithinText } from './helpers/string-validation';
import { renderWithAct } from './render-act';
import { setRenderResult } from './screen';
import { getQueriesForElement } from './within';
import { initNativeState } from './native-state';

export interface RenderOptions {
wrapper?: React.ComponentType<any>;
Expand Down Expand Up @@ -128,7 +127,6 @@ function buildRenderResult(
});

setRenderResult(result);
initNativeState();

return result;
}
Expand Down
2 changes: 1 addition & 1 deletion src/user-event/paste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function paste(
dispatchEvent(element, 'selectionChange', EventBuilder.TextInput.selectionChange(rangeToClear));

// 3. Paste the text
nativeState?.valueForElement.set(element, text);
nativeState.valueForElement.set(element, text);
dispatchEvent(element, 'change', EventBuilder.TextInput.change(text));
dispatchEvent(element, 'changeText', text);

Expand Down
22 changes: 20 additions & 2 deletions src/user-event/scroll/__tests__/scroll-to.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { ScrollView, ScrollViewProps, View } from 'react-native';
import { EventEntry, createEventLogger } from '../../../test-utils';
import { render, screen } from '../../..';
import { fireEvent, render, screen } from '../../..';
import { userEvent } from '../..';

function mapEventsToShortForm(events: EventEntry[]) {
Expand Down Expand Up @@ -103,7 +103,7 @@ describe('scrollTo()', () => {
]);
});

test('remembers previous scroll position', async () => {
test('remembers previous scroll offset', async () => {
const { events } = renderScrollViewWithToolkit();
const user = userEvent.setup();

Expand All @@ -123,6 +123,24 @@ describe('scrollTo()', () => {
]);
});

test('remembers previous scroll offset from "fireEvent.scroll"', async () => {
const { events } = renderScrollViewWithToolkit();
const user = userEvent.setup();

fireEvent.scroll(screen.getByTestId('scrollView'), {
nativeEvent: { contentOffset: { y: 100 } },
});
await user.scrollTo(screen.getByTestId('scrollView'), { y: 200 });
expect(mapEventsToShortForm(events)).toEqual([
['scroll', 100, undefined],
['scrollBeginDrag', 100, 0],
['scroll', 125, 0],
['scroll', 150, 0],
['scroll', 175, 0],
['scrollEndDrag', 200, 0],
]);
});

it('validates vertical scroll direction', async () => {
renderScrollViewWithToolkit();
const user = userEvent.setup();
Expand Down
10 changes: 5 additions & 5 deletions src/user-event/scroll/scroll-to.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,24 @@ export async function scrollTo(
options.contentSize?.height ?? 0,
);

const initialPosition = nativeState?.contentOffsetForElement.get(element) ?? { x: 0, y: 0 };
const initialOffset = nativeState.contentOffsetForElement.get(element) ?? { x: 0, y: 0 };
const dragSteps = createScrollSteps(
{ y: options.y, x: options.x },
initialPosition,
initialOffset,
linearInterpolator,
);
await emitDragScrollEvents(this.config, element, dragSteps, options);

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

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

async function emitDragScrollEvents(
Expand Down
2 changes: 1 addition & 1 deletion src/user-event/type/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export async function emitTypingEvents(
return;
}

nativeState?.valueForElement.set(element, text);
nativeState.valueForElement.set(element, text);
dispatchEvent(element, 'change', EventBuilder.TextInput.change(text));
dispatchEvent(element, 'changeText', text);

Expand Down
8 changes: 4 additions & 4 deletions website/docs/12.x/docs/api/events/user-event.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -269,10 +269,10 @@ Each scroll interaction consists of a mandatory drag scroll part, which simulate
### 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
- `y` - target vertical drag scroll offset
- `x` - target horizontal drag scroll offset
- `momentumY` - target vertical momentum scroll offset
- `momentumX` - target horizontal momentum scroll offset
- `contentSize` - passed to `ScrollView` events and enabling `FlatList` updates
- `layoutMeasurement` - passed to `ScrollView` events and enabling `FlatList` updates
Expand Down

0 comments on commit f69d1d5

Please sign in to comment.