Skip to content

feat: warn when firing events on disabled elements - #1934

Merged
mdjastrzebski merged 22 commits into
callstack:mainfrom
trinadhkoya:feat/disabled-event-warning
Oct 7, 2026
Merged

mdjastrzebski merged 22 commits into
callstack:mainfrom
trinadhkoya:feat/disabled-event-warning

Conversation

@trinadhkoya

@trinadhkoya trinadhkoya commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #1718.

When fireEvent or userEvent doesn't call any handler, the test silently does nothing, which is hard to debug. This PR adds an opt-in eventDiagnostics option that logs a warning in these cases:

  • The handler is on a disabled element (e.g. Pressable with disabled={true}). The warning lists every disabled element the event skipped while bubbling, nearest first, rather than the fired element.
  • The element is a non-editable TextInput (editable={false}), e.g. fireEvent.changeText or userEvent.type on a read-only input. It is reported as disabled, matching computeAriaDisabled. type(), clear() and paste() used to return silently in this case.
  • Neither the element nor any of its ancestors has a handler for the event. For direct events (layout), which don't bubble, only the element itself is checked. This replaces the old warning for direct events that had no handler, which was always on.

A userEvent interaction (press, longPress, type, clear, paste, scrollTo, pullToRefresh, accessibilityAction) dispatches several events. It warns only if none of them called a handler and it didn't update native state. The warning names the method and the events it dispatched, e.g. longPress() did not call any event handlers. The element has no handler for the "pressIn", "longPress" or "pressOut" events.

No warning is logged when the event is blocked on purpose (pointerEvents="none", a responder declining the touch), or when it updates native state (e.g. changeText or userEvent.type on an uncontrolled TextInput).

The option is off by default, so enable it with configure({ eventDiagnostics: true }). Behavior change: firing layout on an element without onLayout used to always warn, and now it only warns when the option is on.

Implementation:

  • findEventHandler now returns { handler, skippedTargets }, so callers can tell a blocked handler apart from no handler at all. skippedTargets lists every element whose handler isEventEnabled rejected, nearest first.
  • updateNativeStateFromEvent returns whether it updated native state, and dispatchEvent returns whether it called a handler.
  • Each userEvent action tracks itself with an Interaction (src/user-event/utils/interaction.ts): events go through interaction.dispatchEvent() to interaction.target (moved by press to the element that handles the press), handlers called directly (pullToRefresh's onRefresh) are recorded with interaction.recordEvent(), native state writes set hasUpdatedNativeState, press records the elements it skips while walking up the tree (disabled, blocked by pointerEvents, or with a responder that declines the touch), and type, clear and paste record the TextInput when it is non-editable or blocked by pointerEvents.
  • The fireEvent warning lives in src/events/warnings.ts (warnAboutUnhandledEvent), and the userEvent one in src/user-event/utils/warnings.ts (warnAboutUnhandledInteraction). They share the formatting from src/events/warnings.ts, and both report the skipped elements that computeAriaDisabled treats as disabled, which includes non-editable TextInput.
  • Removed the warning spies from tests that only used them to silence warnings.
  • Documented eventDiagnostics in the 14.x config docs (and regenerated docs/api/configuration.md with yarn docs:generate), described the userEvent tracking in contributing/event-dispatch.md, and added a file layout section to contributing/code-style.md.

Known limitation: press() on a Pressable without any press props doesn't warn, because the responder events always reach Pressability's own handlers.

Test plan

  • New fireEvent tests cover: a disabled element, nested disabled elements, no handler on the element or its ancestors, a direct event with no handler, a non-editable TextInput, no warning for intentional blocking or native state updates, and no warning when eventDiagnostics is off. The warning tests turn the option on in their setup.
  • New src/user-event/__tests__/event-diagnostics.test.tsx covers: pressing a disabled element, a press nothing handles, longPress() on an element with only onPress, pullToRefresh() without onRefresh, accessibilityAction() without a handler, type(), clear() and paste() on a non-editable TextInput, and no warning when one event is handled, when native state changes (type, clear, paste on an uncontrolled TextInput), when pointerEvents="none" blocks the press or typing, or when the option is off.
  • Ran the whole suite once more with eventDiagnostics turned on for every test, to look for noisy interaction warnings. The only ones are in tests that check disabled elements or a missing onRefresh.

Copilot AI lite review requested due to automatic review settings September 5, 2026 18:03
@trinadhkoya
trinadhkoya force-pushed the feat/disabled-event-warning branch from 931ca0e to 0949859 Compare September 5, 2026 18:05

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

A test-local logger.warn spy is restored in a way that can leak mocks if the test fails before cleanup.

Pull request overview

Adds a configurable warning to help diagnose fireEvent calls that silently do nothing when the target element is disabled, leveraging existing disabled-state computation and the shared logger.

Changes:

  • Emit a logger.warn when fireEvent finds no handler and the nearest touch responder is disabled (opt-out via configure({ disabledEventWarning: false })).
  • Add disabledEventWarning to global config (default true) and update documentation.
  • Extend fire-event and config tests to cover the warning behavior and default config assertions.
File summaries
File Description
website/docs/14.x/docs/api/misc/config.mdx Documents the new disabledEventWarning configuration option and opt-out usage.
src/fire-event.ts Adds disabled-target detection and a warning when fireEvent results in no handler due to disabled state.
src/config.ts Introduces disabledEventWarning in Config, default config, and configure() plumbing.
src/tests/fire-event.test.tsx Adds tests validating warning behavior and config opt-out; suppresses warning output in relevant suites.
src/tests/config.test.ts Updates default-config equality assertion to include disabledEventWarning: true.
Review details

Suppressed comments (1)

src/tests/fire-event.test.tsx:907

  • logger.warn is spied and restored manually at the end of the test. If an assertion throws before the restore line, the spy will leak into later tests and can hide/alter warning expectations.

Wrap the test body in a try/finally (or move the spy to a beforeEach/afterEach) so the restore always runs.

    const warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => {});
    function TestChildTouchableComponent({
      onPress,
      someProp,
    }: {
  • Files reviewed: 5/5 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new TextInput exclusion in the warning logic is broader than the PR description’s intent and may incorrectly suppress warnings for truly disabled TextInput elements.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/fire-event.ts Outdated
@trinadhkoya

Copy link
Copy Markdown
Contributor Author

Heads-up from my side: after opening this I realized #1726 already resolves #1717/#1718 with a fuller design — a unified configure({ debug: true }) debugging mode covering both fireEvent and userEvent. That's clearly the better direction than the dedicated, on-by-default disabledEventWarning flag I proposed here, and I don't want to add a competing API.

I'd rather help land your approach. Happy to do whichever is most useful:

Just let me know what you'd prefer. If you'd welcome the help on #1726, I'm glad to put the work in.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Warning logic can misidentify why handler resolution failed, producing both false positives and false negatives.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/fire-event.ts Outdated
return;
}

const target = getNearestTouchResponder(instance) ?? instance;
@mdjastrzebski
mdjastrzebski force-pushed the feat/disabled-event-warning branch from f23a82b to 5a36023 Compare October 5, 2026 15:35
trinadhkoya and others added 11 commits October 7, 2026 10:00
Firing an event on a disabled element (e.g. a `Pressable` with
`disabled={true}`) silently triggers no handler, which is confusing when
debugging tests. Emit a warning in that case, reusing `computeAriaDisabled`
for detection and the existing `logger`.

- Gated on no handler being found, so events that bubble to an enabled
  parent do not warn.
- Scoped to disabled state only; `pointerEvents="none"` and `TextInput`
  editability are intentionally excluded to avoid false positives.
- Opt-out via `configure({ disabledEventWarning: false })`; on by default.

Closes callstack#1718 (fireEvent scope; userEvent is a follow-up).

^ Conflicts:
^	src/events/__tests__/fire-event.test.tsx
^	src/fire-event.ts
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@mdjastrzebski
mdjastrzebski force-pushed the feat/disabled-event-warning branch from 5a36023 to 67e5f54 Compare October 7, 2026 08:58
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.12207% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.36%. Comparing base (d9366a4) to head (3cbe314).

Files with missing lines Patch % Lines
src/events/warnings.ts 95.91% 1 Missing and 1 partial ⚠️
src/user-event/utils/warnings.ts 93.33% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1934      +/-   ##
==========================================
- Coverage   98.48%   98.36%   -0.12%     
==========================================
  Files          84       87       +3     
  Lines        1581     1714     +133     
  Branches      433      470      +37     
==========================================
+ Hits         1557     1686     +129     
- Misses         24       26       +2     
- Partials        0        2       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mdjastrzebski
mdjastrzebski merged commit b80a762 into callstack:main Oct 7, 2026
37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UX: feedback when event could not be triggered due to disabled state, etc

3 participants