Skip to content

refactor: simplify native state management #1662

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

Merged
merged 2 commits into from
Sep 9, 2024
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
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
Loading