Repository navigation
feat: warn when firing events on disabled elements - #1934
Conversation
931ca0e to
0949859
Compare
There was a problem hiding this comment.
🔵 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.warnwhenfireEventfinds no handler and the nearest touch responder is disabled (opt-out viaconfigure({ disabledEventWarning: false })). - Add
disabledEventWarningto global config (defaulttrue) and update documentation. - Extend
fire-eventand 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.warnis 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.
0949859 to
42ca90d
Compare
There was a problem hiding this comment.
🟡 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
|
Heads-up from my side: after opening this I realized #1726 already resolves #1717/#1718 with a fuller design — a unified 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. |
There was a problem hiding this comment.
🟡 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
| return; | ||
| } | ||
|
|
||
| const target = getNearestTouchResponder(instance) ?? instance; |
f23a82b to
5a36023
Compare
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>
5a36023 to
67e5f54
Compare
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
Summary
Closes #1718.
When
fireEventoruserEventdoesn't call any handler, the test silently does nothing, which is hard to debug. This PR adds an opt-ineventDiagnosticsoption that logs a warning in these cases:Pressablewithdisabled={true}). The warning lists every disabled element the event skipped while bubbling, nearest first, rather than the fired element.TextInput(editable={false}), e.g.fireEvent.changeTextoruserEvent.typeon a read-only input. It is reported as disabled, matchingcomputeAriaDisabled.type(),clear()andpaste()used to return silently in this case.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
userEventinteraction (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.changeTextoruserEvent.typeon an uncontrolledTextInput).The option is off by default, so enable it with
configure({ eventDiagnostics: true }). Behavior change: firinglayouton an element withoutonLayoutused to always warn, and now it only warns when the option is on.Implementation:
findEventHandlernow returns{ handler, skippedTargets }, so callers can tell a blocked handler apart from no handler at all.skippedTargetslists every element whose handlerisEventEnabledrejected, nearest first.updateNativeStateFromEventreturns whether it updated native state, anddispatchEventreturns whether it called a handler.userEventaction tracks itself with anInteraction(src/user-event/utils/interaction.ts): events go throughinteraction.dispatchEvent()tointeraction.target(moved bypressto the element that handles the press), handlers called directly (pullToRefresh'sonRefresh) are recorded withinteraction.recordEvent(), native state writes sethasUpdatedNativeState,pressrecords the elements it skips while walking up the tree (disabled, blocked bypointerEvents, or with a responder that declines the touch), andtype,clearandpasterecord theTextInputwhen it is non-editable or blocked bypointerEvents.fireEventwarning lives insrc/events/warnings.ts(warnAboutUnhandledEvent), and theuserEventone insrc/user-event/utils/warnings.ts(warnAboutUnhandledInteraction). They share the formatting fromsrc/events/warnings.ts, and both report the skipped elements thatcomputeAriaDisabledtreats as disabled, which includes non-editableTextInput.eventDiagnosticsin the 14.x config docs (and regenerateddocs/api/configuration.mdwithyarn docs:generate), described theuserEventtracking incontributing/event-dispatch.md, and added a file layout section tocontributing/code-style.md.Known limitation:
press()on aPressablewithout any press props doesn't warn, because the responder events always reach Pressability's own handlers.Test plan
fireEventtests cover: a disabled element, nested disabled elements, no handler on the element or its ancestors, a direct event with no handler, a non-editableTextInput, no warning for intentional blocking or native state updates, and no warning wheneventDiagnosticsis off. The warning tests turn the option on in their setup.src/user-event/__tests__/event-diagnostics.test.tsxcovers: pressing a disabled element, a press nothing handles,longPress()on an element with onlyonPress,pullToRefresh()withoutonRefresh,accessibilityAction()without a handler,type(),clear()andpaste()on a non-editableTextInput, and no warning when one event is handled, when native state changes (type,clear,pasteon an uncontrolledTextInput), whenpointerEvents="none"blocks the press or typing, or when the option is off.eventDiagnosticsturned on for every test, to look for noisy interaction warnings. The only ones are in tests that check disabled elements or a missingonRefresh.