diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts
index dc454bea..8150ef18 100644
--- a/src/__tests__/config.test.ts
+++ b/src/__tests__/config.test.ts
@@ -14,9 +14,10 @@ test('configure() overrides existing config values', () => {
configure({ defaultDebugOptions: { message: 'debug message' } });
expect(getConfig()).toEqual({
asyncUtilTimeout: 5000,
+ concurrentRoot: true,
+ debug: false,
defaultDebugOptions: { message: 'debug message' },
defaultIncludeHiddenElements: false,
- concurrentRoot: true,
});
});
diff --git a/src/__tests__/fire-event-debug.test.tsx b/src/__tests__/fire-event-debug.test.tsx
new file mode 100644
index 00000000..b8833e6b
--- /dev/null
+++ b/src/__tests__/fire-event-debug.test.tsx
@@ -0,0 +1,69 @@
+import * as React from 'react';
+import { Pressable, Text, View } from 'react-native';
+
+import { configure, fireEvent, render, screen } from '..';
+import { _console } from '../helpers/logger';
+
+beforeEach(() => {
+ jest.spyOn(_console, 'debug').mockImplementation(() => {});
+ jest.spyOn(_console, 'info').mockImplementation(() => {});
+ jest.spyOn(_console, 'warn').mockImplementation(() => {});
+ jest.spyOn(_console, 'error').mockImplementation(() => {});
+});
+
+test('should log warning when firing event on element without handler', () => {
+ render(
+
+ No handler
+ ,
+ );
+
+ fireEvent.press(screen.getByText('No handler'));
+
+ expect(_console.warn).toHaveBeenCalledTimes(1);
+ expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(`
+ " ▲ Fire Event: no event handler for "press" event found on No handler or any of its ancestors.
+ "
+ `);
+});
+
+test('should log warning when firing event on single disabled element', () => {
+ render(
+
+ {}} disabled>
+ Disabled button
+
+ ,
+ );
+
+ fireEvent.press(screen.getByText('Disabled button'));
+
+ expect(_console.warn).toHaveBeenCalledTimes(1);
+ expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(`
+ " ▲ Fire Event: no enabled event handler for "press" event found. Found disabled event handler(s) on:
+ - (composite element)
+ "
+ `);
+});
+
+test('should log warning about multiple disabled handlers', () => {
+ render(
+
+ {}} disabled>
+ {}} disabled>
+ Nested disabled
+
+
+ ,
+ );
+
+ fireEvent.press(screen.getByText('Nested disabled'));
+
+ expect(_console.warn).toHaveBeenCalledTimes(1);
+ expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(`
+ " ▲ Fire Event: no enabled event handler for "press" event found. Found disabled event handler(s) on:
+ - (composite element)
+ - (composite element)
+ "
+ `);
+});
diff --git a/src/config.ts b/src/config.ts
index e861d0eb..793e2ae9 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -19,6 +19,11 @@ export type Config = {
* Otherwise `render` will default to concurrent rendering.
*/
concurrentRoot: boolean;
+
+ /**
+ * Verbose logging for the library.
+ */
+ debug: boolean;
};
export type ConfigAliasOptions = {
@@ -30,6 +35,7 @@ const defaultConfig: Config = {
asyncUtilTimeout: 1000,
defaultIncludeHiddenElements: false,
concurrentRoot: true,
+ debug: false,
};
let config = { ...defaultConfig };
diff --git a/src/fire-event.ts b/src/fire-event.ts
index 981e6e64..abfc5068 100644
--- a/src/fire-event.ts
+++ b/src/fire-event.ts
@@ -10,7 +10,9 @@ import type { ReactTestInstance } from 'react-test-renderer';
import act from './act';
import { getEventHandler } from './event-handler';
import { isElementMounted, isHostElement } from './helpers/component-tree';
+import { formatElement } from './helpers/format-element';
import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
+import { logger } from './helpers/logger';
import { isPointerEventEnabled } from './helpers/pointer-events';
import { isEditableTextInput } from './helpers/text-input';
import { nativeState } from './native-state';
@@ -74,23 +76,41 @@ export function isEventEnabled(
return touchStart === undefined && touchMove === undefined;
}
+type FindEventHandlerState = {
+ nearestTouchResponder?: ReactTestInstance;
+ disabledElements: ReactTestInstance[];
+ targetElement: ReactTestInstance;
+};
+
function findEventHandler(
element: ReactTestInstance,
eventName: string,
- nearestTouchResponder?: ReactTestInstance,
+ state: FindEventHandlerState = {
+ disabledElements: [],
+ targetElement: element,
+ },
): EventHandler | null {
- const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder;
+ const touchResponder = isTouchResponder(element) ? element : state.nearestTouchResponder;
const handler = getEventHandler(element, eventName, { loose: true });
- if (handler && isEventEnabled(element, eventName, touchResponder)) {
- return handler;
+ if (handler) {
+ const isEnabled = isEventEnabled(element, eventName, touchResponder);
+ if (isEnabled) {
+ return handler;
+ } else {
+ state.disabledElements.push(element);
+ }
}
if (element.parent === null) {
+ logger.warn(formatEnabledEventHandlerNotFound(eventName, state));
return null;
}
- return findEventHandler(element.parent, eventName, touchResponder);
+ return findEventHandler(element.parent, eventName, {
+ ...state,
+ nearestTouchResponder: touchResponder,
+ });
}
// String union type of keys of T that start with on, stripped of 'on'
@@ -211,3 +231,23 @@ function tryGetContentOffset(event: unknown): Point | null {
return null;
}
+
+function formatEnabledEventHandlerNotFound(eventName: string, state: FindEventHandlerState) {
+ if (state.disabledElements.length === 0) {
+ return `Fire Event: no event handler for "${eventName}" event found on ${formatElement(
+ state.targetElement,
+ {
+ compact: true,
+ },
+ )} or any of its ancestors.`;
+ }
+
+ return `Fire Event: no enabled event handler for "${eventName}" event found. Found disabled event handler(s) on:\n${state.disabledElements
+ .map(
+ (e) =>
+ ` - ${formatElement(e, { compact: true })}${
+ typeof e.type === 'string' ? '' : ' (composite element)'
+ }`,
+ )
+ .join('\n')}`;
+}
diff --git a/src/helpers/format-element.ts b/src/helpers/format-element.ts
index 295636db..40d1d0eb 100644
--- a/src/helpers/format-element.ts
+++ b/src/helpers/format-element.ts
@@ -37,7 +37,7 @@ export function formatElement(
// This prop is needed persuade the prettyFormat that the element is
// a ReactTestRendererJSON instance, so it is formatted as JSX.
$$typeof: Symbol.for('react.test.json'),
- type: `${element.type}`,
+ type: formatElementName(element.type),
props: mapProps ? mapProps(props) : props,
children: childrenToDisplay,
},
@@ -52,6 +52,25 @@ export function formatElement(
);
}
+function formatElementName(type: ReactTestInstance['type']) {
+ if (typeof type === 'function') {
+ return type.displayName ?? type.name;
+ }
+
+ if (typeof type === 'object') {
+ if ('type' in type) {
+ // @ts-expect-error: despite typing this can happen for React.memo.
+ return formatElementName(type.type);
+ }
+ if ('render' in type) {
+ // @ts-expect-error: despite typing this can happen for React.forwardRefs.
+ return formatElementName(type.render);
+ }
+ }
+
+ return `${type}`;
+}
+
export function formatElementList(elements: ReactTestInstance[], options?: FormatElementOptions) {
if (elements.length === 0) {
return '(no elements)';
diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts
index d8222d3b..2de199c6 100644
--- a/src/helpers/logger.ts
+++ b/src/helpers/logger.ts
@@ -3,6 +3,8 @@ import pc from 'picocolors';
import redent from 'redent';
import * as nodeUtil from 'util';
+import { getConfig } from '../config';
+
export const _console = {
debug: nodeConsole.debug,
info: nodeConsole.info,
@@ -12,8 +14,10 @@ export const _console = {
export const logger = {
debug(message: unknown, ...args: unknown[]) {
- const output = formatMessage('●', message, ...args);
- _console.debug(pc.dim(output));
+ if (getConfig().debug) {
+ const output = formatMessage('●', message, ...args);
+ _console.debug(pc.dim(output));
+ }
},
info(message: unknown, ...args: unknown[]) {
diff --git a/src/helpers/map-props.ts b/src/helpers/map-props.ts
index 1c268928..03dc063d 100644
--- a/src/helpers/map-props.ts
+++ b/src/helpers/map-props.ts
@@ -28,6 +28,7 @@ const propsToDisplay = [
'aria-valuenow',
'aria-valuetext',
'defaultValue',
+ 'disabled',
'editable',
'importantForAccessibility',
'nativeID',
diff --git a/src/user-event/clear.ts b/src/user-event/clear.ts
index 4a070187..b46b8141 100644
--- a/src/user-event/clear.ts
+++ b/src/user-event/clear.ts
@@ -1,7 +1,9 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { ErrorWithStack } from '../helpers/errors';
+import { formatElement } from '../helpers/format-element';
import { isHostTextInput } from '../helpers/host-component-names';
+import { logger } from '../helpers/logger';
import { isPointerEventEnabled } from '../helpers/pointer-events';
import { getTextInputValue, isEditableTextInput } from '../helpers/text-input';
import { EventBuilder } from './event-builder';
@@ -17,7 +19,17 @@ export async function clear(this: UserEventInstance, element: ReactTestInstance)
);
}
- if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
+ if (!isEditableTextInput(element)) {
+ logger.warn(
+ `User Event (clear): element ${formatElement(element, { compact: true })} is not editable.`,
+ );
+ return;
+ }
+
+ if (!isPointerEventEnabled(element)) {
+ logger.warn(
+ `User Event (clear): element ${formatElement(element, { compact: true })} has pointer event handlers disabled.`,
+ );
return;
}
diff --git a/src/user-event/paste.ts b/src/user-event/paste.ts
index 98191d84..132f7c87 100644
--- a/src/user-event/paste.ts
+++ b/src/user-event/paste.ts
@@ -8,6 +8,8 @@ import { nativeState } from '../native-state';
import { EventBuilder } from './event-builder';
import type { UserEventInstance } from './setup';
import { dispatchEvent, getTextContentSize, wait } from './utils';
+import { formatElement } from '../helpers/format-element';
+import { logger } from '../helpers/logger';
export async function paste(
this: UserEventInstance,
@@ -21,7 +23,17 @@ export async function paste(
);
}
- if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
+ if (!isEditableTextInput(element)) {
+ logger.warn(
+ `User Event (paste): element ${formatElement(element, { compact: true })} is not editable.`,
+ );
+ return;
+ }
+
+ if (!isPointerEventEnabled(element)) {
+ logger.warn(
+ `User Event (paste): element ${formatElement(element, { compact: true })} has pointer event handlers disabled.`,
+ );
return;
}
diff --git a/src/user-event/type/type.ts b/src/user-event/type/type.ts
index 8607ef87..ccf26de0 100644
--- a/src/user-event/type/type.ts
+++ b/src/user-event/type/type.ts
@@ -9,6 +9,8 @@ import { EventBuilder } from '../event-builder';
import type { UserEventConfig, UserEventInstance } from '../setup';
import { dispatchEvent, getTextContentSize, wait } from '../utils';
import { parseKeys } from './parse-keys';
+import { logger } from '../../helpers/logger';
+import { formatElement } from '../../helpers/format-element';
export interface TypeOptions {
skipPress?: boolean;
@@ -29,11 +31,19 @@ export async function type(
);
}
- // Skip events if the element is disabled
- if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
+ if (!isEditableTextInput(element)) {
+ logger.warn(
+ `User Event (type): element ${formatElement(element, { compact: true })} is not editable.`,
+ );
return;
}
+ if (!isPointerEventEnabled(element)) {
+ logger.warn(
+ `User Event (type): element ${formatElement(element, { compact: true })} has pointer event handlers disabled.`,
+ );
+ return;
+ }
const keys = parseKeys(text);
if (!options?.skipPress) {
diff --git a/src/user-event/utils/dispatch-event.ts b/src/user-event/utils/dispatch-event.ts
index 161d4cfa..c425a3e3 100644
--- a/src/user-event/utils/dispatch-event.ts
+++ b/src/user-event/utils/dispatch-event.ts
@@ -3,6 +3,8 @@ import type { ReactTestInstance } from 'react-test-renderer';
import act from '../../act';
import { getEventHandler } from '../../event-handler';
import { isElementMounted } from '../../helpers/component-tree';
+import { formatElement } from '../../helpers/format-element';
+import { logger } from '../../helpers/logger';
/**
* Basic dispatch event function used by User Event module.
@@ -22,6 +24,11 @@ export async function dispatchEvent(
const handler = getEventHandler(element, eventName);
if (!handler) {
+ logger.warn(
+ `User Event: no event handler for "${eventName}" found on ${formatElement(element, {
+ compact: true,
+ })}`,
+ );
return;
}