Skip to content

Commit

Permalink
feat: userEvent.press (#1386)
Browse files Browse the repository at this point in the history
* feat: create first version of userEvent.press

* feat: do not trigger press event when pointer events is disabled

* feat: make press events bubble up when trigerring a non touch responder

* tests: add test cases for userevent.press to check calls to onPressIn and onPressOut

* refactor: move userEvent.press tests in a dedicated test file

* feat: add pressDuration option for userEvent.press

* refactor: group test that check prop calls

* feat: add support for touchable opacity for userevent press

* feat: add support for Text

* feat: add support for TextInput for userEvent.press

* refactor: add some comments to explain pointer events api

* refactor: change the api of userEvent.press to make it async

* feat: add support for real timers for userEvent.press

* refactor: create a longPress api and remove duration option from press api

* feat: add warning for users when userEvent is used with real timers

* refactor: rewrite press using common user event code

* refactor: remove duplicate pointerEventEnabled method

* refactor: remove check on fake timers in user.press

* refactor: change order of functions in press file to have exports first

* feat: add delay before press

* feat: wait min press duration before calling onPressout for text and textInput

* chore: improve coverage

* feat: account for press duration when waiting for press out

* fix: wait for press duration also when pressing text or textinput

* docs: add documentation on press and longpress

* fix: check pointer events for Text and TextInput

* chore: fixes and tweaks on userEvent docs

* Update press doc based on review suggestion

Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>

* refactor: rename pressDuration option to duration

* Update longPress doc based on review suggestion

Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>

* refactor: use ts doc for isPointerEventsEnabled method

* refactor: rename file isPointerEventsEnabled to pointer-events

* refactor: split test in longPress in two

* refactor: use Date.now instead of new Date().getTime()

* refactor: also test payload of events for press and longpress

* refactor: fix typo in some longpress test names

* Update src/user-event/press/utils/warnAboutRealTimers.ts

Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>

* refactor: merge press and touch events

* fix: return after pressing text or textinput

* refactor: extract functions to trigger press on text/textInput

* refactor: use optional chaining

* feat: update warning when using userEvent with real timers

* refactor: check on press test that longPress is not called

* refactor: test directly warnings logged with real timers without mocking function

---------

Co-authored-by: pierrezimmermann <pierrez@nam.tech>
Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>
  • Loading branch information
3 people authored Jul 17, 2023
1 parent 1c0fc79 commit 1f38177
Show file tree
Hide file tree
Showing 17 changed files with 1,197 additions and 117 deletions.
24 changes: 2 additions & 22 deletions src/fireEvent.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ReactTestInstance } from 'react-test-renderer';
import act from './act';
import { getHostParent, isHostElement } from './helpers/component-tree';
import { isHostElement } from './helpers/component-tree';
import { getHostComponentNames } from './helpers/host-component-names';
import { isPointerEventEnabled } from './helpers/pointer-events';

type EventHandler = (...args: unknown[]) => unknown;

Expand All @@ -19,27 +20,6 @@ export function isTouchResponder(element: ReactTestInstance) {
);
}

export function isPointerEventEnabled(
element: ReactTestInstance,
isParent?: boolean
): boolean {
const pointerEvents = element.props.pointerEvents;
if (pointerEvents === 'none') {
return false;
}

if (isParent ? pointerEvents === 'box-only' : pointerEvents === 'box-none') {
return false;
}

const parent = getHostParent(element);
if (!parent) {
return true;
}

return isPointerEventEnabled(parent, true);
}

/**
* List of events affected by `pointerEvents` prop.
*
Expand Down
27 changes: 27 additions & 0 deletions src/helpers/pointer-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ReactTestInstance } from 'react-test-renderer';
import { getHostParent } from './component-tree';

/**
* pointerEvents controls whether the View can be the target of touch events.
* 'auto': The View and its children can be the target of touch events.
* 'none': The View is never the target of touch events.
* 'box-none': The View is never the target of touch events but its subviews can be
* 'box-only': The view can be the target of touch events but its subviews cannot be
* see the official react native doc https://reactnative.dev/docs/view#pointerevents */
export const isPointerEventEnabled = (
element: ReactTestInstance,
isParent?: boolean
): boolean => {
const parentCondition = isParent
? element?.props.pointerEvents === 'box-only'
: element?.props.pointerEvents === 'box-none';

if (element?.props.pointerEvents === 'none' || parentCondition) {
return false;
}

const hostParent = getHostParent(element);
if (!hostParent) return true;

return isPointerEventEnabled(hostParent, true);
};
4 changes: 4 additions & 0 deletions src/test-utils/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export function createEventLogger() {

return { events, logEvent };
}

export function getEventsName(events: EventEntry[]) {
return events.map((event) => event.name);
}
4 changes: 3 additions & 1 deletion src/user-event/event-builder/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const CommonEventBuilder = {
*/
touch: () => {
return {
persist: jest.fn(),
currentTarget: { measure: jest.fn() },
nativeEvent: {
changedTouches: [],
identifier: 0,
Expand All @@ -14,7 +16,7 @@ export const CommonEventBuilder = {
pageX: 0,
pageY: 0,
target: 0,
timestamp: 0,
timestamp: Date.now(),
touches: [],
},
};
Expand Down
3 changes: 3 additions & 0 deletions src/user-event/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { ReactTestInstance } from 'react-test-renderer';
import { setup } from './setup';
import { PressOptions } from './press/press';

export const userEvent = {
setup,

// Direct access for User Event v13 compatibility
press: (element: ReactTestInstance) => setup().press(element),
longPress: (element: ReactTestInstance, options?: PressOptions) =>
setup().longPress(element, options),
type: (element: ReactTestInstance, text: string) =>
setup().type(element, text),
};
54 changes: 0 additions & 54 deletions src/user-event/press/__tests__/__snapshots__/press.test.tsx.snap

This file was deleted.

115 changes: 115 additions & 0 deletions src/user-event/press/__tests__/longPress.real-timers.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react';
import { Pressable, Text } from 'react-native';
import { render, screen } from '../../../pure';
import { userEvent } from '../..';
import * as WarnAboutRealTimers from '../utils/warnAboutRealTimers';

describe('userEvent.longPress with real timers', () => {
beforeEach(() => {
jest.useRealTimers();
jest.restoreAllMocks();
jest.spyOn(WarnAboutRealTimers, 'warnAboutRealTimers').mockImplementation();
});

test('calls onLongPress if the delayLongPress is the default one', async () => {
const mockOnLongPress = jest.fn();
const user = userEvent.setup();

render(
<Pressable onLongPress={mockOnLongPress}>
<Text>press me</Text>
</Pressable>
);
await user.longPress(screen.getByText('press me'));

expect(mockOnLongPress).toHaveBeenCalled();
});

test('calls onLongPress when duration is greater than specified delayLongPress', async () => {
const mockOnLongPress = jest.fn();
const mockOnPress = jest.fn();
const user = userEvent.setup();

render(
<Pressable
delayLongPress={800}
onLongPress={mockOnLongPress}
onPress={mockOnPress}
>
<Text>press me</Text>
</Pressable>
);

await user.longPress(screen.getByText('press me'), {
duration: 1000,
});

expect(mockOnLongPress).toHaveBeenCalled();
expect(mockOnPress).not.toHaveBeenCalled();
});

test('does not calls onLongPress when duration is lesser than specified delayLongPress', async () => {
const mockOnLongPress = jest.fn();
const mockOnPress = jest.fn();
const user = userEvent.setup();

render(
<Pressable
delayLongPress={1000}
onLongPress={mockOnLongPress}
onPress={mockOnPress}
>
<Text>press me</Text>
</Pressable>
);
await user.longPress(screen.getByText('press me'));

expect(mockOnLongPress).not.toHaveBeenCalled();
expect(mockOnPress).toHaveBeenCalledTimes(1);
});

test('does not calls onPress when onLongPress is called', async () => {
const mockOnLongPress = jest.fn();
const mockOnPress = jest.fn();
const user = userEvent.setup();

render(
<Pressable onLongPress={mockOnLongPress} onPress={mockOnPress}>
<Text>press me</Text>
</Pressable>
);
await user.longPress(screen.getByText('press me'));

expect(mockOnLongPress).toHaveBeenCalled();
expect(mockOnPress).not.toHaveBeenCalled();
});

test('longPress is accessible directly in userEvent', async () => {
const mockOnLongPress = jest.fn();

render(
<Pressable onLongPress={mockOnLongPress}>
<Text>press me</Text>
</Pressable>
);

await userEvent.longPress(screen.getByText('press me'));

expect(mockOnLongPress).toHaveBeenCalled();
});
});

test('warns about using real timers with userEvent', async () => {
jest.restoreAllMocks();
const mockConsoleWarn = jest.spyOn(console, 'warn').mockImplementation();

render(<Pressable testID="pressable" />);

await userEvent.longPress(screen.getByTestId('pressable'));

expect(mockConsoleWarn.mock.calls[0][0]).toMatchInlineSnapshot(`
"It is recommended to use userEvent with fake timers
Some events involve duration so your tests may take a long time to run.
For instance calling userEvent.longPress with real timers will take 500 ms."
`);
});
Loading

0 comments on commit 1f38177

Please sign in to comment.