diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts
index 6949f3a6a..1fb2cc51d 100644
--- a/src/__tests__/config.test.ts
+++ b/src/__tests__/config.test.ts
@@ -22,6 +22,7 @@ test('configure() overrides existing config values', () => {
asyncUtilTimeout: 5000,
defaultDebugOptions: { message: 'debug message' },
defaultIncludeHiddenElements: false,
+ disabledEventWarning: true,
});
});
diff --git a/src/__tests__/fire-event.test.tsx b/src/__tests__/fire-event.test.tsx
index ea39223ac..becf3ec0f 100644
--- a/src/__tests__/fire-event.test.tsx
+++ b/src/__tests__/fire-event.test.tsx
@@ -14,6 +14,8 @@ import {
} from 'react-native';
import { fireEvent, render, screen } from '..';
+import { configure, resetToDefaults } from '../config';
+import { logger } from '../helpers/logger';
import { nativeState } from '../native-state';
const layoutEvent = { nativeEvent: { layout: { width: 100, height: 100 } } };
@@ -560,6 +562,17 @@ test('fireEvent handles handler that throws gracefully', async () => {
});
describe('disabled elements', () => {
+ let warnSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ warnSpy.mockRestore();
+ resetToDefaults();
+ });
+
test('does not fire on disabled Pressable', async () => {
const onPress = jest.fn();
await render(
@@ -623,6 +636,61 @@ describe('disabled elements', () => {
await fireEvent.press(screen.getByText('Trigger Test'));
expect(handlePress).toHaveBeenCalledTimes(1);
});
+
+ test('warns when firing an event on a disabled element', async () => {
+ await render(
+
+ Trigger
+ ,
+ );
+
+ await fireEvent.press(screen.getByText('Trigger'));
+
+ expect(warnSpy).toHaveBeenCalledTimes(1);
+ expect(warnSpy.mock.calls[0][0]).toMatchInlineSnapshot(`
+ "Tried to fire the "press" event on a disabled element, so no handler was called.
+ If this is intentional, you can disable this warning via \`configure({ disabledEventWarning: false })\`."
+ `);
+ });
+
+ test('does not warn when the event bubbles to an enabled parent', async () => {
+ await render(
+
+
+ Inner Trigger
+
+ ,
+ );
+
+ await fireEvent.press(screen.getByText('Inner Trigger'));
+
+ expect(warnSpy).not.toHaveBeenCalled();
+ });
+
+ test('does not warn when the element is not disabled (e.g. pointerEvents="none")', async () => {
+ await render(
+
+
+ ,
+ );
+
+ await fireEvent.press(screen.getByTestId('btn'));
+
+ expect(warnSpy).not.toHaveBeenCalled();
+ });
+
+ test('does not warn when disabledEventWarning is turned off', async () => {
+ configure({ disabledEventWarning: false });
+ await render(
+
+ Trigger
+ ,
+ );
+
+ await fireEvent.press(screen.getByText('Trigger'));
+
+ expect(warnSpy).not.toHaveBeenCalled();
+ });
});
describe('pointerEvents prop', () => {
@@ -831,6 +899,16 @@ describe('non-editable TextInput', () => {
});
describe('responder system', () => {
+ let warnSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ warnSpy.mockRestore();
+ });
+
test('respects disabled prop through composite wrappers', async () => {
function TestChildTouchableComponent({
onPress,
diff --git a/src/config.ts b/src/config.ts
index b910b40d8..cf3977784 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -12,6 +12,12 @@ export type Config = {
/** Default value for `includeHiddenElements` query option. */
defaultIncludeHiddenElements: boolean;
+ /**
+ * Warn when an event is fired on a disabled element and no handler is
+ * triggered as a result. Set to `false` to opt out.
+ */
+ disabledEventWarning: boolean;
+
/** Default options for `debug` helper. */
defaultDebugOptions?: Partial;
};
@@ -24,6 +30,7 @@ export type ConfigAliasOptions = {
const defaultConfig: Config = {
asyncUtilTimeout: 1000,
defaultIncludeHiddenElements: false,
+ disabledEventWarning: true,
};
let config = { ...defaultConfig };
@@ -37,6 +44,7 @@ export function configure(options: Partial) {
defaultDebugOptions,
defaultHidden,
defaultIncludeHiddenElements,
+ disabledEventWarning,
...rest
} = options;
@@ -50,6 +58,7 @@ export function configure(options: Partial) {
asyncUtilTimeout: asyncUtilTimeout ?? config.asyncUtilTimeout,
defaultDebugOptions,
defaultIncludeHiddenElements: resolvedDefaultIncludeHiddenElements,
+ disabledEventWarning: disabledEventWarning ?? config.disabledEventWarning,
};
}
diff --git a/src/fire-event.ts b/src/fire-event.ts
index 4b64b652c..b76e6f9b1 100644
--- a/src/fire-event.ts
+++ b/src/fire-event.ts
@@ -8,12 +8,15 @@ import type {
import type { Fiber, TestInstance } from 'test-renderer';
import { act } from './act';
+import { getConfig } from './config';
import type { LayoutRectangle } from './event-builder';
import { buildLayoutEvent, buildScrollEvent, buildTouchEvent } from './event-builder';
import type { EventHandler } from './event-handler';
import { getEventHandlerFromProps } from './event-handler';
+import { computeAriaDisabled } from './helpers/accessibility';
import { isInstanceMounted } from './helpers/component-tree';
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';
@@ -113,6 +116,51 @@ function findEventHandlerFromFiber(fiber: Fiber | null, eventName: string): Even
return findEventHandlerFromFiber(fiber.return, eventName);
}
+/**
+ * Walks up from the target to the nearest element that can respond to touches
+ * (a touch responder or a host `TextInput`), mirroring `findEventHandler`.
+ */
+function getNearestTouchResponder(instance: TestInstance): TestInstance | null {
+ let current: TestInstance | null = instance;
+ while (current != null) {
+ if (isTouchResponder(current)) {
+ return current;
+ }
+
+ current = current.parent;
+ }
+
+ return null;
+}
+
+/**
+ * Warns when an event did not trigger any handler because the responding
+ * element is disabled. Helps debug tests that silently do nothing.
+ * Can be opted out via `configure({ disabledEventWarning: false })`.
+ */
+function warnAboutDisabledEventTarget(instance: TestInstance, eventName: string) {
+ if (!getConfig().disabledEventWarning) {
+ return;
+ }
+
+ const target = getNearestTouchResponder(instance) ?? instance;
+
+ // `TextInput` editability (`editable={false}`) is a separate concern from
+ // disabled state, so we don't warn about it here to avoid false positives.
+ if (isHostTextInput(target)) {
+ return;
+ }
+
+ if (!computeAriaDisabled(target)) {
+ return;
+ }
+
+ logger.warn(
+ `Tried to fire the "${eventName}" event on a disabled element, so no handler was called.\n` +
+ 'If this is intentional, you can disable this warning via `configure({ disabledEventWarning: false })`.',
+ );
+}
+
// String union type of keys of T that start with on, stripped of 'on'
type EventNameExtractor = keyof {
[K in keyof T as K extends `on${infer Rest}` ? Uncapitalize : never]: T[K];
@@ -135,6 +183,7 @@ async function fireEvent(instance: TestInstance, eventName: EventName, ...data:
const handler = findEventHandler(instance, eventName);
if (!handler) {
+ warnAboutDisabledEventTarget(instance, eventName);
return;
}
diff --git a/website/docs/14.x/docs/api/misc/config.mdx b/website/docs/14.x/docs/api/misc/config.mdx
index a72fa8dea..f1b7eda3c 100644
--- a/website/docs/14.x/docs/api/misc/config.mdx
+++ b/website/docs/14.x/docs/api/misc/config.mdx
@@ -10,6 +10,9 @@ type Config = {
/** Default value for `includeHiddenElements` query option. */
defaultIncludeHiddenElements: boolean;
+ /** Warn when firing an event on a disabled element triggers no handler. */
+ disabledEventWarning: boolean;
+
/** Default options for `debug` helper. */
defaultDebugOptions?: Partial;
};
@@ -32,6 +35,14 @@ Default value for [includeHiddenElements](/docs/api/queries#includehiddenelement
This option is also available as `defaultHidden` alias for compatibility with [React Testing Library](https://testing-library.com/docs/dom-testing-library/api-configuration/#defaulthidden).
+### `disabledEventWarning` option
+
+When `fireEvent` is used on a disabled element (e.g. a `Pressable` with `disabled={true}`) the event is not dispatched to any handler, which can be surprising while debugging tests. When this option is enabled (the default), a warning is logged in that case. Set it to `false` to opt out:
+
+```ts
+configure({ disabledEventWarning: false });
+```
+
### `defaultDebugOptions` option
Default [debug options](#debug) to be used when calling `debug()`. These default options will be overridden by the ones you specify directly when calling `debug()`.