Skip to content

Commit fc93a55

Browse files
committed
basci impl
1 parent 95c9df3 commit fc93a55

File tree

11 files changed

+194
-13
lines changed

11 files changed

+194
-13
lines changed

src/__tests__/config.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ test('configure() overrides existing config values', () => {
1414
configure({ defaultDebugOptions: { message: 'debug message' } });
1515
expect(getConfig()).toEqual({
1616
asyncUtilTimeout: 5000,
17+
concurrentRoot: true,
18+
debug: false,
1719
defaultDebugOptions: { message: 'debug message' },
1820
defaultIncludeHiddenElements: false,
19-
concurrentRoot: true,
2021
});
2122
});
2223

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as React from 'react';
2+
import { Pressable, Text, View } from 'react-native';
3+
4+
import { configure, fireEvent, render, screen } from '..';
5+
import { _console } from '../helpers/logger';
6+
7+
beforeEach(() => {
8+
jest.spyOn(_console, 'debug').mockImplementation(() => {});
9+
jest.spyOn(_console, 'info').mockImplementation(() => {});
10+
jest.spyOn(_console, 'warn').mockImplementation(() => {});
11+
jest.spyOn(_console, 'error').mockImplementation(() => {});
12+
});
13+
14+
test('should log warning when firing event on element without handler', () => {
15+
render(
16+
<View>
17+
<Text>No handler</Text>
18+
</View>,
19+
);
20+
21+
fireEvent.press(screen.getByText('No handler'));
22+
23+
expect(_console.warn).toHaveBeenCalledTimes(1);
24+
expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(`
25+
" ▲ Fire Event: no event handler for "press" event found on <Text>No handler</Text> or any of its ancestors.
26+
"
27+
`);
28+
});
29+
30+
test('should log warning when firing event on single disabled element', () => {
31+
render(
32+
<View>
33+
<Pressable onPress={() => {}} disabled>
34+
<Text>Disabled button</Text>
35+
</Pressable>
36+
</View>,
37+
);
38+
39+
fireEvent.press(screen.getByText('Disabled button'));
40+
41+
expect(_console.warn).toHaveBeenCalledTimes(1);
42+
expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(`
43+
" ▲ Fire Event: no enabled event handler for "press" event found. Found disabled event handler(s) on:
44+
- <Pressable disabled={true} /> (composite element)
45+
"
46+
`);
47+
});
48+
49+
test('should log warning about multiple disabled handlers', () => {
50+
render(
51+
<View>
52+
<Pressable testID="outer" onPress={() => {}} disabled>
53+
<Pressable testID="inner" onPress={() => {}} disabled>
54+
<Text>Nested disabled</Text>
55+
</Pressable>
56+
</Pressable>
57+
</View>,
58+
);
59+
60+
fireEvent.press(screen.getByText('Nested disabled'));
61+
62+
expect(_console.warn).toHaveBeenCalledTimes(1);
63+
expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(`
64+
" ▲ Fire Event: no enabled event handler for "press" event found. Found disabled event handler(s) on:
65+
- <Pressable disabled={true} testID="inner" /> (composite element)
66+
- <Pressable disabled={true} testID="outer" /> (composite element)
67+
"
68+
`);
69+
});

src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ export type Config = {
1919
* Otherwise `render` will default to concurrent rendering.
2020
*/
2121
concurrentRoot: boolean;
22+
23+
/**
24+
* Verbose logging for the library.
25+
*/
26+
debug: boolean;
2227
};
2328

2429
export type ConfigAliasOptions = {
@@ -30,6 +35,7 @@ const defaultConfig: Config = {
3035
asyncUtilTimeout: 1000,
3136
defaultIncludeHiddenElements: false,
3237
concurrentRoot: true,
38+
debug: false,
3339
};
3440

3541
let config = { ...defaultConfig };

src/fire-event.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import type { ReactTestInstance } from 'react-test-renderer';
1010
import act from './act';
1111
import { getEventHandler } from './event-handler';
1212
import { isElementMounted, isHostElement } from './helpers/component-tree';
13+
import { formatElement } from './helpers/format-element';
1314
import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
15+
import { logger } from './helpers/logger';
1416
import { isPointerEventEnabled } from './helpers/pointer-events';
1517
import { isEditableTextInput } from './helpers/text-input';
1618
import { nativeState } from './native-state';
@@ -74,23 +76,41 @@ export function isEventEnabled(
7476
return touchStart === undefined && touchMove === undefined;
7577
}
7678

79+
type FindEventHandlerState = {
80+
nearestTouchResponder?: ReactTestInstance;
81+
disabledElements: ReactTestInstance[];
82+
targetElement: ReactTestInstance;
83+
};
84+
7785
function findEventHandler(
7886
element: ReactTestInstance,
7987
eventName: string,
80-
nearestTouchResponder?: ReactTestInstance,
88+
state: FindEventHandlerState = {
89+
disabledElements: [],
90+
targetElement: element,
91+
},
8192
): EventHandler | null {
82-
const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder;
93+
const touchResponder = isTouchResponder(element) ? element : state.nearestTouchResponder;
8394

8495
const handler = getEventHandler(element, eventName, { loose: true });
85-
if (handler && isEventEnabled(element, eventName, touchResponder)) {
86-
return handler;
96+
if (handler) {
97+
const isEnabled = isEventEnabled(element, eventName, touchResponder);
98+
if (isEnabled) {
99+
return handler;
100+
} else {
101+
state.disabledElements.push(element);
102+
}
87103
}
88104

89105
if (element.parent === null) {
106+
logger.warn(formatEnabledEventHandlerNotFound(eventName, state));
90107
return null;
91108
}
92109

93-
return findEventHandler(element.parent, eventName, touchResponder);
110+
return findEventHandler(element.parent, eventName, {
111+
...state,
112+
nearestTouchResponder: touchResponder,
113+
});
94114
}
95115

96116
// String union type of keys of T that start with on, stripped of 'on'
@@ -211,3 +231,23 @@ function tryGetContentOffset(event: unknown): Point | null {
211231

212232
return null;
213233
}
234+
235+
function formatEnabledEventHandlerNotFound(eventName: string, state: FindEventHandlerState) {
236+
if (state.disabledElements.length === 0) {
237+
return `Fire Event: no event handler for "${eventName}" event found on ${formatElement(
238+
state.targetElement,
239+
{
240+
compact: true,
241+
},
242+
)} or any of its ancestors.`;
243+
}
244+
245+
return `Fire Event: no enabled event handler for "${eventName}" event found. Found disabled event handler(s) on:\n${state.disabledElements
246+
.map(
247+
(e) =>
248+
` - ${formatElement(e, { compact: true })}${
249+
typeof e.type === 'string' ? '' : ' (composite element)'
250+
}`,
251+
)
252+
.join('\n')}`;
253+
}

src/helpers/format-element.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function formatElement(
3737
// This prop is needed persuade the prettyFormat that the element is
3838
// a ReactTestRendererJSON instance, so it is formatted as JSX.
3939
$$typeof: Symbol.for('react.test.json'),
40-
type: `${element.type}`,
40+
type: formatElementName(element.type),
4141
props: mapProps ? mapProps(props) : props,
4242
children: childrenToDisplay,
4343
},
@@ -52,6 +52,25 @@ export function formatElement(
5252
);
5353
}
5454

55+
function formatElementName(type: ReactTestInstance['type']) {
56+
if (typeof type === 'function') {
57+
return type.displayName ?? type.name;
58+
}
59+
60+
if (typeof type === 'object') {
61+
if ('type' in type) {
62+
// @ts-expect-error: despite typing this can happen for React.memo.
63+
return formatElementName(type.type);
64+
}
65+
if ('render' in type) {
66+
// @ts-expect-error: despite typing this can happen for React.forwardRefs.
67+
return formatElementName(type.render);
68+
}
69+
}
70+
71+
return `${type}`;
72+
}
73+
5574
export function formatElementList(elements: ReactTestInstance[], options?: FormatElementOptions) {
5675
if (elements.length === 0) {
5776
return '(no elements)';

src/helpers/logger.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import pc from 'picocolors';
33
import redent from 'redent';
44
import * as nodeUtil from 'util';
55

6+
import { getConfig } from '../config';
7+
68
export const _console = {
79
debug: nodeConsole.debug,
810
info: nodeConsole.info,
@@ -12,8 +14,10 @@ export const _console = {
1214

1315
export const logger = {
1416
debug(message: unknown, ...args: unknown[]) {
15-
const output = formatMessage('●', message, ...args);
16-
_console.debug(pc.dim(output));
17+
if (getConfig().debug) {
18+
const output = formatMessage('●', message, ...args);
19+
_console.debug(pc.dim(output));
20+
}
1721
},
1822

1923
info(message: unknown, ...args: unknown[]) {

src/helpers/map-props.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const propsToDisplay = [
2828
'aria-valuenow',
2929
'aria-valuetext',
3030
'defaultValue',
31+
'disabled',
3132
'editable',
3233
'importantForAccessibility',
3334
'nativeID',

src/user-event/clear.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22

33
import { ErrorWithStack } from '../helpers/errors';
4+
import { formatElement } from '../helpers/format-element';
45
import { isHostTextInput } from '../helpers/host-component-names';
6+
import { logger } from '../helpers/logger';
57
import { isPointerEventEnabled } from '../helpers/pointer-events';
68
import { getTextInputValue, isEditableTextInput } from '../helpers/text-input';
79
import { EventBuilder } from './event-builder';
@@ -17,7 +19,17 @@ export async function clear(this: UserEventInstance, element: ReactTestInstance)
1719
);
1820
}
1921

20-
if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
22+
if (!isEditableTextInput(element)) {
23+
logger.warn(
24+
`User Event (clear): element ${formatElement(element, { compact: true })} is not editable.`,
25+
);
26+
return;
27+
}
28+
29+
if (!isPointerEventEnabled(element)) {
30+
logger.warn(
31+
`User Event (clear): element ${formatElement(element, { compact: true })} has pointer event handlers disabled.`,
32+
);
2133
return;
2234
}
2335

src/user-event/paste.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { nativeState } from '../native-state';
88
import { EventBuilder } from './event-builder';
99
import type { UserEventInstance } from './setup';
1010
import { dispatchEvent, getTextContentSize, wait } from './utils';
11+
import { formatElement } from '../helpers/format-element';
12+
import { logger } from '../helpers/logger';
1113

1214
export async function paste(
1315
this: UserEventInstance,
@@ -21,7 +23,17 @@ export async function paste(
2123
);
2224
}
2325

24-
if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
26+
if (!isEditableTextInput(element)) {
27+
logger.warn(
28+
`User Event (paste): element ${formatElement(element, { compact: true })} is not editable.`,
29+
);
30+
return;
31+
}
32+
33+
if (!isPointerEventEnabled(element)) {
34+
logger.warn(
35+
`User Event (paste): element ${formatElement(element, { compact: true })} has pointer event handlers disabled.`,
36+
);
2537
return;
2638
}
2739

src/user-event/type/type.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { EventBuilder } from '../event-builder';
99
import type { UserEventConfig, UserEventInstance } from '../setup';
1010
import { dispatchEvent, getTextContentSize, wait } from '../utils';
1111
import { parseKeys } from './parse-keys';
12+
import { logger } from '../../helpers/logger';
13+
import { formatElement } from '../../helpers/format-element';
1214

1315
export interface TypeOptions {
1416
skipPress?: boolean;
@@ -29,11 +31,19 @@ export async function type(
2931
);
3032
}
3133

32-
// Skip events if the element is disabled
33-
if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
34+
if (!isEditableTextInput(element)) {
35+
logger.warn(
36+
`User Event (type): element ${formatElement(element, { compact: true })} is not editable.`,
37+
);
3438
return;
3539
}
3640

41+
if (!isPointerEventEnabled(element)) {
42+
logger.warn(
43+
`User Event (type): element ${formatElement(element, { compact: true })} has pointer event handlers disabled.`,
44+
);
45+
return;
46+
}
3747
const keys = parseKeys(text);
3848

3949
if (!options?.skipPress) {

0 commit comments

Comments
 (0)