Skip to content

Commit d627e14

Browse files
committed
generalize to all pressability events
1 parent d731d83 commit d627e14

5 files changed

Lines changed: 315 additions & 222 deletions

File tree

‎src/events/modern/__tests__/fire-event.test.tsx‎

Lines changed: 10 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -913,53 +913,23 @@ describe('fireEvent.press', () => {
913913
expect(onPanResponderGrant).not.toHaveBeenCalled();
914914
});
915915

916-
test('lets an ancestor claim the touch in the capture phase', async () => {
916+
test('asks responders from the target to the root, without arguments', async () => {
917917
const calls: string[] = [];
918-
await render(
919-
<View
920-
testID="parent"
921-
onStartShouldSetResponderCapture={(event) => {
922-
calls.push(`capture: ${event.currentTarget === screen.getByTestId('parent')}`);
923-
return true;
924-
}}
925-
>
926-
<Pressable onPress={() => calls.push('onPress')}>
927-
<Text>Press me</Text>
928-
</Pressable>
929-
</View>,
930-
);
931-
932-
await fireEvent.press(screen.getByText('Press me'));
933-
934-
expect(calls).toEqual(['capture: true']);
935-
});
936-
937-
test('asks capture responders from the root, then responders from the target', async () => {
938-
const calls: string[] = [];
939-
const responderProps = (name: string) => ({
940-
onStartShouldSetResponderCapture: (event: unknown) => {
941-
calls.push(`${name} capture, phase ${(event as SyntheticEvent).eventPhase}`);
942-
return false;
943-
},
944-
onStartShouldSetResponder: (event: unknown) => {
945-
calls.push(`${name} bubble, phase ${(event as SyntheticEvent).eventPhase}`);
918+
const onStartShouldSetResponder =
919+
(name: string) =>
920+
(...args: unknown[]) => {
921+
calls.push(`${name} (${args.length} args)`);
946922
return false;
947-
},
948-
});
923+
};
949924
await render(
950-
<View {...responderProps('parent')}>
951-
<View testID="child" {...responderProps('child')} />
925+
<View onStartShouldSetResponder={onStartShouldSetResponder('parent')}>
926+
<View testID="child" onStartShouldSetResponder={onStartShouldSetResponder('child')} />
952927
</View>,
953928
);
954929

955930
await fireEvent.press(screen.getByTestId('child'));
956931

957-
expect(calls).toEqual([
958-
'parent capture, phase 1',
959-
'child capture, phase 2',
960-
'child bubble, phase 2',
961-
'parent bubble, phase 3',
962-
]);
932+
expect(calls).toEqual(['child (0 args)', 'parent (0 args)']);
963933
});
964934

965935
describe('pointerEvents', () => {
@@ -1077,7 +1047,7 @@ describe('fireEvent.press', () => {
10771047
const onPress = jest.fn();
10781048
await render(<Pressable testID="pressable" onPress={onPress} />);
10791049
const pressable = screen.getByTestId('pressable');
1080-
await screen.unmount();
1050+
await screen.rerender(<View />);
10811051

10821052
expect(await fireEvent.press(pressable)).toBe(true);
10831053
expect(onPress).not.toHaveBeenCalled();
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import * as React from 'react';
2+
import { Pressable, Text, TouchableOpacity, View } from 'react-native';
3+
4+
import { render, screen } from '../../..';
5+
import type { SyntheticEvent } from '../event';
6+
import { createEvent } from '../event';
7+
import type { PressabilityCallbackName } from '../pressability';
8+
import { dispatchPressabilityEvent } from '../pressability';
9+
10+
const callbackNames: PressabilityCallbackName[] = [
11+
'onPress',
12+
'onPressIn',
13+
'onPressOut',
14+
'onLongPress',
15+
];
16+
17+
function createTouchEvent() {
18+
return createEvent('touchEnd', { nativeEvent: { pageX: 10 } }) as SyntheticEvent;
19+
}
20+
21+
function logCallbacks(calls: string[], id: string) {
22+
const log = (name: PressabilityCallbackName) => () => {
23+
calls.push(`${id}.${name}`);
24+
};
25+
return {
26+
onPress: log('onPress'),
27+
onPressIn: log('onPressIn'),
28+
onPressOut: log('onPressOut'),
29+
onLongPress: log('onLongPress'),
30+
};
31+
}
32+
33+
test.each(callbackNames)('calls only %s of Pressable and TouchableOpacity', async (name) => {
34+
const calls: string[] = [];
35+
await render(
36+
<>
37+
<Pressable {...logCallbacks(calls, 'pressable')}>
38+
<Text>Pressable</Text>
39+
</Pressable>
40+
<TouchableOpacity {...logCallbacks(calls, 'touchable')}>
41+
<Text>Touchable</Text>
42+
</TouchableOpacity>
43+
</>,
44+
);
45+
46+
await dispatchPressabilityEvent(screen.getByText('Pressable'), name, createTouchEvent());
47+
await dispatchPressabilityEvent(screen.getByText('Touchable'), name, createTouchEvent());
48+
49+
expect(calls).toEqual([`pressable.${name}`, `touchable.${name}`]);
50+
});
51+
52+
test.each(callbackNames)('calls only %s of host elements, incl. testOnly_ props', async (name) => {
53+
const calls: string[] = [];
54+
await render(
55+
<>
56+
<Text {...logCallbacks(calls, 'text')}>Text</Text>
57+
<View
58+
testID="view"
59+
{...{ [`testOnly_${name}`]: () => calls.push(`view.testOnly_${name}`) }}
60+
/>
61+
</>,
62+
);
63+
64+
await dispatchPressabilityEvent(screen.getByText('Text'), name, createTouchEvent());
65+
await dispatchPressabilityEvent(screen.getByTestId('view'), name, createTouchEvent());
66+
67+
expect(calls).toEqual([`text.${name}`, `view.testOnly_${name}`]);
68+
});
69+
70+
test('passes the event, with the hit target and the responder', async () => {
71+
const onPressIn = jest.fn((event) => ({
72+
currentTarget: event.currentTarget,
73+
target: event.target,
74+
}));
75+
await render(
76+
<Pressable testID="pressable" onPressIn={onPressIn}>
77+
<Text>Press me</Text>
78+
</Pressable>,
79+
);
80+
const event = createTouchEvent();
81+
82+
expect(await dispatchPressabilityEvent(screen.getByText('Press me'), 'onPressIn', event)).toBe(
83+
true,
84+
);
85+
86+
expect(onPressIn).toHaveBeenCalledWith(event);
87+
expect(onPressIn.mock.results[0].value).toEqual({
88+
currentTarget: screen.getByTestId('pressable'),
89+
target: screen.getByText('Press me'),
90+
});
91+
});
92+
93+
test('host element with another press callback claims the touch, but gets nothing', async () => {
94+
const onPressIn = jest.fn();
95+
await render(
96+
<Pressable onPressIn={onPressIn}>
97+
<Text onPress={jest.fn()}>Press me</Text>
98+
</Pressable>,
99+
);
100+
101+
expect(
102+
await dispatchPressabilityEvent(screen.getByText('Press me'), 'onPressIn', createTouchEvent()),
103+
).toBe(false);
104+
expect(onPressIn).not.toHaveBeenCalled();
105+
});
106+
107+
test('returns false when the responder has no such callback', async () => {
108+
await render(
109+
<Pressable onPress={jest.fn()}>
110+
<Text>Press me</Text>
111+
</Pressable>,
112+
);
113+
114+
expect(
115+
await dispatchPressabilityEvent(
116+
screen.getByText('Press me'),
117+
'onLongPress',
118+
createTouchEvent(),
119+
),
120+
).toBe(false);
121+
});
122+
123+
test('does not call callbacks of disabled Pressable', async () => {
124+
const calls: string[] = [];
125+
await render(
126+
<Pressable {...logCallbacks(calls, 'pressable')} disabled>
127+
<Text>Press me</Text>
128+
</Pressable>,
129+
);
130+
131+
for (const name of callbackNames) {
132+
await dispatchPressabilityEvent(screen.getByText('Press me'), name, createTouchEvent());
133+
}
134+
135+
expect(calls).toEqual([]);
136+
});

‎src/events/modern/fire-event.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ import type { DispatchOptions } from './dispatch';
2020
import { dispatchEvent } from './dispatch';
2121
import type { CreateEventInit, NativeEventPayload } from './event';
2222
import { createEvent } from './event';
23-
import { dispatchPress } from './press';
2423
import {
2524
ensureEventType,
2625
ensureSingleEventArg,
2726
validateChangeTextArgs,
2827
validateEventInit,
2928
} from './fire-event-utils';
29+
import { dispatchPressabilityEvent } from './pressability';
3030

3131
/**
3232
* Event object passed to modern `fireEvent`. Handlers receive a `SyntheticEvent` from
@@ -89,7 +89,7 @@ fireEvent.changeText = async (instance: TestInstance, text: string): Promise<boo
8989
* on a device does. `Pressable` and `Touchable*` get it from their Pressability config, hosts with
9090
* their own `onPress` (mocked `Text`, `testOnly_onPress`) directly. Disabled elements and
9191
* responders that decline the touch pass it on to their ancestors. `pointerEvents` moves the touch
92-
* to the nearest ancestor it doesn't block. See `dispatchPress()`.
92+
* to the nearest ancestor it doesn't block. See `dispatchPressabilityEvent()`.
9393
*
9494
* Only `onPress` is called. Use `userEvent.press()` for `onPressIn`, `onPressOut` and timing.
9595
*
@@ -106,7 +106,7 @@ fireEvent.press = async (instance: TestInstance, event: FireEventInit = {}): Pro
106106
return true;
107107
}
108108

109-
await dispatchPress(instance, pressEvent);
109+
await dispatchPressabilityEvent(instance, 'onPress', pressEvent);
110110
return !pressEvent.defaultPrevented;
111111
};
112112

0 commit comments

Comments
 (0)