Skip to content

Commit 965a880

Browse files
committed
cleanup
1 parent 21da189 commit 965a880

9 files changed

Lines changed: 166 additions & 205 deletions

File tree

‎src/events/dispatch-event.ts‎

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,15 @@ import { invokeEventHandler } from './shared/invoke-event-handler';
1414
* `configure({ eventSystem })`. Used by `userEvent`.
1515
*
1616
* - `legacy`: calls the element's own `on*` prop with `event` as it is.
17-
* - `modern`: native events (`focus`, `change`, `scroll`, ...) get a `SyntheticEvent` created from
18-
* `event.nativeEvent` and `event.timeStamp`, dispatched as React Native does, through the capture and bubble phases
19-
* for bubbling events. Callbacks that components call from JavaScript (`changeText`, `pressIn`,
20-
* `responderGrant`, ...) call the element's own `on*` prop with `event` as it is, as in
21-
* `legacy`. `press` is a native event type (`topPress`), so it reaches host `onPress` props
22-
* only, not `Pressable`'s `onPress`.
17+
* - `modern`: native events (`focus`, `change`, `scroll`, ...) get a `SyntheticEvent` from
18+
* `event.nativeEvent` and `event.timeStamp`, dispatched as React Native does (capture and bubble
19+
* phases for bubbling events). Callbacks that components call from JavaScript (`changeText`,
20+
* `pressIn`, `responderGrant`, ...) are called as in `legacy`. `press` is a native event type
21+
* (`topPress`), so it reaches host `onPress` props only, not `Pressable`'s `onPress`.
2322
*
2423
* @param eventType without the `on*` prefix, e.g. `focus`
25-
* @param event the event object, e.g. from `buildFocusEvent()`, or the callback's argument, e.g. the
26-
* text for `changeText`
24+
* @param event the event object, e.g. from `buildFocusEvent()`, or the callback's argument, e.g.
25+
* the text for `changeText`
2726
* @returns `true` if a handler was called.
2827
*/
2928
export async function dispatchEvent(
@@ -41,17 +40,12 @@ export async function dispatchEvent(
4140
? createEvent(eventType, { nativeEvent, timeStamp: extractTimeStamp(event) })
4241
: null;
4342
if (syntheticEvent == null) {
44-
// JavaScript callbacks, e.g. `changeText`, are called as in the legacy event system.
4543
return await invokeEventHandler(instance, eventType, event);
4644
}
4745

4846
return await dispatchModernEvent(instance, syntheticEvent);
4947
}
5048

51-
/**
52-
* Returns the `nativeEvent` payload if this is a native event: an event type React Native
53-
* dispatches natively, with an event object that has a `nativeEvent`.
54-
*/
5549
function extractNativeEvent(eventType: string, event: unknown): NativeEventPayload | null {
5650
if (getEventTypeConfig(eventType) == null) {
5751
return null;

‎src/events/modern/__tests__/event-types.test.ts‎

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { createEvent } from '../event';
12
import {
23
BUBBLING_EVENT_TYPES,
34
DIRECT_EVENT_TYPES,
@@ -13,7 +14,6 @@ type ViewConfig = {
1314
directEventTypes?: Record<string, { registrationName: string }>;
1415
};
1516

16-
// Base view configs plus the built-in components that declare events.
1717
const viewConfigModules = [
1818
'react-native/Libraries/NativeComponent/BaseViewConfig.ios',
1919
'react-native/Libraries/NativeComponent/BaseViewConfig.android',
@@ -39,23 +39,6 @@ const optionalViewConfigModules = [
3939
// Not exported by React Native, so listed by hand in `event-types.ts`.
4040
const manualDirectEventTypes = ['textLayout'];
4141

42-
function toEventType(topLevelType: string) {
43-
return topLevelType.charAt(3).toLowerCase() + topLevelType.slice(4);
44-
}
45-
46-
function loadViewConfigs(): ViewConfig[] {
47-
const modules = viewConfigModules.map((modulePath) => jest.requireActual(modulePath));
48-
for (const modulePath of optionalViewConfigModules) {
49-
try {
50-
modules.push(jest.requireActual(modulePath));
51-
} catch {
52-
// Not available in the installed React Native version.
53-
}
54-
}
55-
56-
return modules.map((module) => module.__INTERNAL_VIEW_CONFIG ?? module.default);
57-
}
58-
5942
function loadReactNativeEventTypes() {
6043
const bubbling = new Set<string>();
6144
const skipBubbling = new Set<string>();
@@ -64,8 +47,8 @@ function loadReactNativeEventTypes() {
6447
for (const viewConfig of loadViewConfigs()) {
6548
for (const [topLevelType, config] of Object.entries(viewConfig.bubblingEventTypes ?? {})) {
6649
const eventType = toEventType(topLevelType);
67-
// `getEventTypeConfig()` derives prop names from the event type.
68-
expect(getEventTypeConfig(eventType)?.dispatchConfig).toEqual({
50+
// Prop names derived from the event type match React Native's.
51+
expect(getDispatchConfig(eventType)).toEqual({
6952
phasedRegistrationNames: config.phasedRegistrationNames,
7053
});
7154
bubbling.add(eventType);
@@ -81,7 +64,7 @@ function loadReactNativeEventTypes() {
8164
}
8265

8366
const eventType = toEventType(topLevelType);
84-
expect(getEventTypeConfig(eventType)?.dispatchConfig).toEqual({
67+
expect(getDispatchConfig(eventType)).toEqual({
8568
registrationName: config.registrationName,
8669
});
8770
direct.add(eventType);
@@ -91,6 +74,27 @@ function loadReactNativeEventTypes() {
9174
return { bubbling, skipBubbling, direct };
9275
}
9376

77+
function loadViewConfigs(): ViewConfig[] {
78+
const modules = viewConfigModules.map((modulePath) => jest.requireActual(modulePath));
79+
for (const modulePath of optionalViewConfigModules) {
80+
try {
81+
modules.push(jest.requireActual(modulePath));
82+
} catch {
83+
// Not available in the installed React Native version.
84+
}
85+
}
86+
87+
return modules.map((module) => module.__INTERNAL_VIEW_CONFIG ?? module.default);
88+
}
89+
90+
function toEventType(topLevelType: string) {
91+
return topLevelType.charAt(3).toLowerCase() + topLevelType.slice(4);
92+
}
93+
94+
function getDispatchConfig(eventType: string) {
95+
return createEvent(eventType)?.dispatchConfig;
96+
}
97+
9498
// The lists are the union across supported React Native versions, so older versions declare only a
9599
// subset of them (e.g. `keyDown` and `keyUp` were added in React Native 0.84).
96100
test('event types cover React Native view configs', () => {
@@ -104,32 +108,23 @@ test('event types cover React Native view configs', () => {
104108
test('getEventTypeConfig() returns config of bubbling event', () => {
105109
expect(getEventTypeConfig('pointerUp')).toEqual({
106110
kind: 'bubbling',
111+
handlerName: 'onPointerUp',
112+
captureHandlerName: 'onPointerUpCapture',
107113
skipBubbling: false,
108-
dispatchConfig: {
109-
phasedRegistrationNames: { bubbled: 'onPointerUp', captured: 'onPointerUpCapture' },
110-
},
111114
});
112115
});
113116

114117
test('getEventTypeConfig() returns config of bubbling event with skipBubbling', () => {
115118
expect(getEventTypeConfig('pointerEnter')).toEqual({
116119
kind: 'bubbling',
120+
handlerName: 'onPointerEnter',
121+
captureHandlerName: 'onPointerEnterCapture',
117122
skipBubbling: true,
118-
dispatchConfig: {
119-
phasedRegistrationNames: {
120-
bubbled: 'onPointerEnter',
121-
captured: 'onPointerEnterCapture',
122-
skipBubbling: true,
123-
},
124-
},
125123
});
126124
});
127125

128126
test('getEventTypeConfig() returns config of direct event', () => {
129-
expect(getEventTypeConfig('layout')).toEqual({
130-
kind: 'direct',
131-
dispatchConfig: { registrationName: 'onLayout' },
132-
});
127+
expect(getEventTypeConfig('layout')).toEqual({ kind: 'direct', handlerName: 'onLayout' });
133128
});
134129

135130
test('getEventTypeConfig() returns null for events unknown to React Native', () => {

‎src/events/modern/__tests__/event.test.ts‎

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { createEvent, SyntheticEvent } from '../event';
1+
import { format } from 'pretty-format';
2+
import type { TestInstance } from 'test-renderer';
23

3-
const dispatchConfig = { registrationName: 'onFocus' };
4+
import { createEvent, getEventInternals, SyntheticEvent } from '../event';
5+
import type { EventTypeConfig } from '../event-types';
6+
7+
const typeConfig: EventTypeConfig = { kind: 'direct', handlerName: 'onFocus' };
48

59
afterEach(() => {
610
jest.restoreAllMocks();
@@ -10,9 +14,9 @@ test('exposes init values', () => {
1014
const nativeEvent = { target: 1 };
1115
const event = new SyntheticEvent(
1216
'layout',
13-
{ cancelable: true, rnIsDirect: true, timeStamp: 123 },
17+
{ cancelable: true, timeStamp: 123 },
1418
nativeEvent,
15-
dispatchConfig,
19+
typeConfig,
1620
);
1721

1822
expect(event.type).toBe('layout');
@@ -21,29 +25,39 @@ test('exposes init values', () => {
2125
expect(event.rnIsDirect).toBe(true);
2226
expect(event.timeStamp).toBe(123);
2327
expect(event.nativeEvent).toBe(nativeEvent);
24-
expect(event.dispatchConfig).toBe(dispatchConfig);
28+
expect(event.dispatchConfig).toEqual({ registrationName: 'onFocus' });
2529
expect(event.isTrusted).toBe(true);
2630
});
2731

2832
test('has no target and phase outside of dispatch', () => {
29-
const event = new SyntheticEvent('focus', { bubbles: true }, {}, dispatchConfig);
33+
const event = new SyntheticEvent('focus', {}, {}, typeConfig);
3034

3135
expect(event.target).toBeNull();
3236
expect(event.currentTarget).toBeNull();
3337
expect(event.eventPhase).toBe(SyntheticEvent.NONE);
3438
expect(event.composedPath()).toEqual([]);
3539
});
3640

41+
test('keeps dispatch fields out of printed and compared events', () => {
42+
const event = new SyntheticEvent('focus', { timeStamp: 1 }, {}, typeConfig);
43+
const target = { type: 'View', props: { testID: 'target' } } as unknown as TestInstance;
44+
getEventInternals(event).target = target;
45+
46+
expect(event.target).toBe(target);
47+
expect(event).toEqual(new SyntheticEvent('focus', { timeStamp: 1 }, {}, typeConfig));
48+
expect(format(event)).not.toContain('target');
49+
});
50+
3751
test('defaults timeStamp to performance.now()', () => {
3852
jest.spyOn(performance, 'now').mockReturnValue(456);
3953

40-
const event = new SyntheticEvent('focus', {}, {}, dispatchConfig);
54+
const event = new SyntheticEvent('focus', {}, {}, typeConfig);
4155

4256
expect(event.timeStamp).toBe(456);
4357
});
4458

4559
test('preventDefault() marks cancelable event as default prevented', () => {
46-
const event = new SyntheticEvent('focus', { cancelable: true }, {}, dispatchConfig);
60+
const event = new SyntheticEvent('focus', { cancelable: true }, {}, typeConfig);
4761

4862
event.preventDefault();
4963

@@ -52,7 +66,7 @@ test('preventDefault() marks cancelable event as default prevented', () => {
5266
});
5367

5468
test('preventDefault() does nothing on non-cancelable event', () => {
55-
const event = new SyntheticEvent('focus', {}, {}, dispatchConfig);
69+
const event = new SyntheticEvent('focus', {}, {}, typeConfig);
5670

5771
event.preventDefault();
5872

@@ -63,7 +77,7 @@ test('preventDefault() does nothing on non-cancelable event', () => {
6377
test.each(['stopPropagation', 'stopImmediatePropagation'] as const)(
6478
'%s() stops propagation',
6579
(method) => {
66-
const event = new SyntheticEvent('focus', {}, {}, dispatchConfig);
80+
const event = new SyntheticEvent('focus', {}, {}, typeConfig);
6781
expect(event.isPropagationStopped()).toBe(false);
6882

6983
event[method]();
@@ -74,7 +88,7 @@ test.each(['stopPropagation', 'stopImmediatePropagation'] as const)(
7488
);
7589

7690
test('cancelBubble can only be set to true', () => {
77-
const event = new SyntheticEvent('focus', {}, {}, dispatchConfig);
91+
const event = new SyntheticEvent('focus', {}, {}, typeConfig);
7892

7993
event.cancelBubble = true;
8094
event.cancelBubble = false;
@@ -83,13 +97,13 @@ test('cancelBubble can only be set to true', () => {
8397
});
8498

8599
test('persist() is a no-op', () => {
86-
const event = new SyntheticEvent('focus', {}, {}, dispatchConfig);
100+
const event = new SyntheticEvent('focus', {}, {}, typeConfig);
87101

88102
expect(() => event.persist()).not.toThrow();
89103
});
90104

91105
test('defines event phase constants on class and prototype', () => {
92-
const event = new SyntheticEvent('focus', {}, {}, dispatchConfig);
106+
const event = new SyntheticEvent('focus', {}, {}, typeConfig);
93107

94108
expect([
95109
SyntheticEvent.NONE,
@@ -126,6 +140,13 @@ describe('createEvent()', () => {
126140

127141
expect(event?.bubbles).toBe(false);
128142
expect(event?.rnIsDirect).toBe(false);
143+
expect(event?.dispatchConfig).toEqual({
144+
phasedRegistrationNames: {
145+
bubbled: 'onPointerEnter',
146+
captured: 'onPointerEnterCapture',
147+
skipBubbling: true,
148+
},
149+
});
129150
});
130151

131152
test('creates direct event', () => {

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

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import type { TestInstance } from 'test-renderer';
44

55
import { render, screen } from '../../..';
66
import type { SyntheticEvent } from '../event';
7-
import type { FireEventInit, FireEventType } from '../fire-event';
7+
import type { FireEventInit } from '../fire-event';
88
import { fireEvent } from '../fire-event';
99

1010
/** Calls `fireEvent` with arguments its types don't allow, as JavaScript callers can. */
1111
function fireEventUntyped(instance: TestInstance, eventType: string, ...args: unknown[]) {
12-
return fireEvent(instance, eventType as FireEventType, ...(args as [FireEventInit]));
12+
return fireEvent(instance, eventType, ...(args as [FireEventInit]));
1313
}
1414

1515
describe('event object', () => {
@@ -251,16 +251,6 @@ describe('event types unknown to React Native', () => {
251251
expect(handler).not.toHaveBeenCalled();
252252
});
253253

254-
test('are type errors', async () => {
255-
await render(<TextInput testID="input" />);
256-
const input = screen.getByTestId('input');
257-
258-
// @ts-expect-error `changeText` isn't dispatched natively.
259-
await expect(fireEvent(input, 'changeText', {})).rejects.toThrow();
260-
// @ts-expect-error `onChangeText` isn't dispatched natively.
261-
await expect(fireEvent(input, 'onChangeText', {})).rejects.toThrow();
262-
});
263-
264254
test('throws before checking the event object', async () => {
265255
await render(<TextInput testID="input" />);
266256

0 commit comments

Comments
 (0)