From e131283c2c434ac3d94f882c5110970134e9f86f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Tue, 7 Jan 2025 18:06:53 +0100 Subject: [PATCH 1/6] basci impl --- src/fire-event.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/fire-event.ts b/src/fire-event.ts index c659fcfe..2650fbc8 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -9,8 +9,10 @@ import type { ReactTestInstance } from 'react-test-renderer'; import act from './act'; import { isElementMounted, isHostElement } 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 { formatElement } from './matchers/utils'; import { nativeState } from './native-state'; import type { Point, StringWithAutocomplete } from './types'; @@ -80,7 +82,15 @@ function findEventHandler( const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder; const handler = getEventHandler(element, eventName); - if (handler && isEventEnabled(element, eventName, touchResponder)) return handler; + if (handler) { + if (isEventEnabled(element, eventName, touchResponder)) { + return handler; + } else { + logger.warn( + `${formatElement(element, { minimal: true })}: "${eventName}" event is not enabled.`, + ); + } + } // eslint-disable-next-line @typescript-eslint/prefer-optional-chain if (element.parent === null || element.parent.parent === null) { @@ -129,6 +139,11 @@ function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: un const handler = findEventHandler(element, eventName); if (!handler) { + logger.warn( + `${formatElement(element, { + minimal: true, + })}: no "${eventName}" event handler found on element or any of it's ancestors`, + ); return; } From 6161c8894513fb637a2b649275f18986b556553f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 8 Jan 2025 10:23:29 +0100 Subject: [PATCH 2/6] extract formatElement --- src/fire-event.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/fire-event.ts b/src/fire-event.ts index 2650fbc8..b404b799 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -8,11 +8,11 @@ import type { import type { ReactTestInstance } from 'react-test-renderer'; import act from './act'; 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 { formatElement } from './matchers/utils'; import { nativeState } from './native-state'; import type { Point, StringWithAutocomplete } from './types'; @@ -87,7 +87,9 @@ function findEventHandler( return handler; } else { logger.warn( - `${formatElement(element, { minimal: true })}: "${eventName}" event is not enabled.`, + `FireEvent(${eventName}): event handler is disabled on ${formatElement(element, { + minimal: true, + })}`, ); } } @@ -140,9 +142,9 @@ function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: un const handler = findEventHandler(element, eventName); if (!handler) { logger.warn( - `${formatElement(element, { + `FireEvent(${eventName}): no event handler found on ${formatElement(element, { minimal: true, - })}: no "${eventName}" event handler found on element or any of it's ancestors`, + })}`, ); return; } From 49626a75e50bbdb9f4a5d5571f1d9208e239d8f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 8 Jan 2025 10:26:53 +0100 Subject: [PATCH 3/6] . --- src/fire-event.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fire-event.ts b/src/fire-event.ts index b404b799..f3bfccff 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -87,7 +87,7 @@ function findEventHandler( return handler; } else { logger.warn( - `FireEvent(${eventName}): event handler is disabled on ${formatElement(element, { + `FireEvent "${eventName}": event handler is disabled on ${formatElement(element, { minimal: true, })}`, ); @@ -142,9 +142,9 @@ function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: un const handler = findEventHandler(element, eventName); if (!handler) { logger.warn( - `FireEvent(${eventName}): no event handler found on ${formatElement(element, { + `FireEvent "${eventName}": no event handler found on ${formatElement(element, { minimal: true, - })}`, + })} or its ancestors`, ); return; } From 9e7e744f367e651144d5fc0b81ee331925ca489a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 8 Jan 2025 15:07:20 +0100 Subject: [PATCH 4/6] improve debug details --- src/fire-event.ts | 44 ++++++++++++++++++-------- src/user-event/utils/dispatch-event.ts | 7 ++++ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/fire-event.ts b/src/fire-event.ts index f3bfccff..d12dcaff 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -49,29 +49,43 @@ const textInputEventsIgnoringEditableProp = new Set([ 'onScroll', ]); -export function isEventEnabled( +type EventHandlerState = { + enabled: boolean; + reason?: string; +}; + +function getEventHandlerState( element: ReactTestInstance, eventName: string, nearestTouchResponder?: ReactTestInstance, -) { +): EventHandlerState { if (nearestTouchResponder != null && isHostTextInput(nearestTouchResponder)) { - return ( - isEditableTextInput(nearestTouchResponder) || - textInputEventsIgnoringEditableProp.has(eventName) - ); + if (isEditableTextInput(nearestTouchResponder)) { + return { enabled: true }; + } + + if (textInputEventsIgnoringEditableProp.has(eventName)) { + return { enabled: true }; + } + + return { enabled: false, reason: '"editable" prop' }; } if (eventsAffectedByPointerEventsProp.has(eventName) && !isPointerEventEnabled(element)) { - return false; + return { enabled: false, reason: '"pointerEvents" prop' }; } const touchStart = nearestTouchResponder?.props.onStartShouldSetResponder?.(); const touchMove = nearestTouchResponder?.props.onMoveShouldSetResponder?.(); if (touchStart || touchMove) { - return true; + return { enabled: true }; } - return touchStart === undefined && touchMove === undefined; + if (touchStart === undefined && touchMove === undefined) { + return { enabled: true }; + } + + return { enabled: false, reason: 'not a touch responder' }; } function findEventHandler( @@ -83,13 +97,15 @@ function findEventHandler( const handler = getEventHandler(element, eventName); if (handler) { - if (isEventEnabled(element, eventName, touchResponder)) { + const handlerState = getEventHandlerState(element, eventName, touchResponder); + + if (handlerState.enabled) { return handler; } else { logger.warn( - `FireEvent "${eventName}": event handler is disabled on ${formatElement(element, { + `FireEvent: "${eventName}" event handler is disabled on ${formatElement(element, { minimal: true, - })}`, + })} (${handlerState.reason}).`, ); } } @@ -142,9 +158,9 @@ function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: un const handler = findEventHandler(element, eventName); if (!handler) { logger.warn( - `FireEvent "${eventName}": no event handler found on ${formatElement(element, { + `FireEvent: no enabled event handler for "${eventName}" found on ${formatElement(element, { minimal: true, - })} or its ancestors`, + })} or its ancestors.`, ); return; } diff --git a/src/user-event/utils/dispatch-event.ts b/src/user-event/utils/dispatch-event.ts index 3ae2551d..6e71b5d5 100644 --- a/src/user-event/utils/dispatch-event.ts +++ b/src/user-event/utils/dispatch-event.ts @@ -1,6 +1,8 @@ import type { ReactTestInstance } from 'react-test-renderer'; import act from '../../act'; 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. @@ -16,6 +18,11 @@ export function dispatchEvent(element: ReactTestInstance, eventName: string, ... const handler = getEventHandler(element, eventName); if (!handler) { + logger.debug( + `User Event: no event handler for "${eventName}" found on ${formatElement(element, { + minimal: true, + })}`, + ); return; } From ec9b0764008441f7e43af951c42ffe62faf873bf Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Mon, 13 Jan 2025 12:05:49 +0100 Subject: [PATCH 5/6] . --- src/fire-event.ts | 8 ++--- src/helpers/format-element.ts | 56 ++++++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/fire-event.ts b/src/fire-event.ts index d12dcaff..a0315b2e 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -85,7 +85,7 @@ function getEventHandlerState( return { enabled: true }; } - return { enabled: false, reason: 'not a touch responder' }; + return { enabled: false, reason: 'not being a touch responder' }; } function findEventHandler( @@ -104,8 +104,8 @@ function findEventHandler( } else { logger.warn( `FireEvent: "${eventName}" event handler is disabled on ${formatElement(element, { - minimal: true, - })} (${handlerState.reason}).`, + compact: true, + })} due to ${handlerState.reason}.`, ); } } @@ -159,7 +159,7 @@ function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: un if (!handler) { logger.warn( `FireEvent: no enabled event handler for "${eventName}" found on ${formatElement(element, { - minimal: true, + compact: true, })} or its ancestors.`, ); return; diff --git a/src/helpers/format-element.ts b/src/helpers/format-element.ts index 2170f83b..b5e2a358 100644 --- a/src/helpers/format-element.ts +++ b/src/helpers/format-element.ts @@ -31,26 +31,48 @@ export function formatElement( const { children, ...props } = element.props; const childrenToDisplay = typeof children === 'string' ? [children] : undefined; - return prettyFormat( - { - // 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}`, - props: mapProps ? mapProps(props) : props, - children: childrenToDisplay, - }, - // See: https://www.npmjs.com/package/pretty-format#usage-with-options - { - plugins: [plugins.ReactTestComponent, plugins.ReactElement], - printFunctionName: false, - printBasicPrototype: false, - highlight: highlight, - min: compact, - }, + return ( + (typeof element.type === 'string' ? '' : 'composite ') + + prettyFormat( + { + // 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: formatElementName(element.type), + props: mapProps ? mapProps(props) : props, + children: childrenToDisplay, + }, + // See: https://www.npmjs.com/package/pretty-format#usage-with-options + { + plugins: [plugins.ReactTestComponent, plugins.ReactElement], + printFunctionName: false, + printBasicPrototype: false, + highlight: highlight, + min: compact, + }, + ) ); } +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)'; From 95845da1d82abba97b3cccd2e569c92b74c24127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Wed, 29 Jan 2025 12:36:09 +0100 Subject: [PATCH 6/6] debug config option --- src/__tests__/config.test.ts | 3 ++- src/config.ts | 6 ++++++ src/fire-event.ts | 7 ++++--- src/helpers/logger.ts | 27 ++++++++++++++++++++++++++ src/user-event/utils/dispatch-event.ts | 6 +++--- 5 files changed, 42 insertions(+), 7 deletions(-) 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/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 a0315b2e..1a6fca35 100644 --- a/src/fire-event.ts +++ b/src/fire-event.ts @@ -10,7 +10,7 @@ import act from './act'; 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 { debugLogger } from './helpers/logger'; import { isPointerEventEnabled } from './helpers/pointer-events'; import { isEditableTextInput } from './helpers/text-input'; import { nativeState } from './native-state'; @@ -102,7 +102,7 @@ function findEventHandler( if (handlerState.enabled) { return handler; } else { - logger.warn( + debugLogger.warn( `FireEvent: "${eventName}" event handler is disabled on ${formatElement(element, { compact: true, })} due to ${handlerState.reason}.`, @@ -157,11 +157,12 @@ function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: un const handler = findEventHandler(element, eventName); if (!handler) { - logger.warn( + debugLogger.warn( `FireEvent: no enabled event handler for "${eventName}" found on ${formatElement(element, { compact: true, })} or its ancestors.`, ); + return; } diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts index eccb4dc3..1710b634 100644 --- a/src/helpers/logger.ts +++ b/src/helpers/logger.ts @@ -2,6 +2,7 @@ import chalk from 'chalk'; import * as nodeConsole from 'console'; import redent from 'redent'; import * as nodeUtil from 'util'; +import { getConfig } from '../config'; export const logger = { debug(message: unknown, ...args: unknown[]) { @@ -25,6 +26,32 @@ export const logger = { }, }; +export const debugLogger = { + debug(message: unknown, ...args: unknown[]) { + if (getConfig().debug) { + logger.debug(message, ...args); + } + }, + + info(message: unknown, ...args: unknown[]) { + if (getConfig().debug) { + logger.info(message, ...args); + } + }, + + warn(message: unknown, ...args: unknown[]) { + if (getConfig().debug) { + logger.warn(message, ...args); + } + }, + + error(message: unknown, ...args: unknown[]) { + if (getConfig().debug) { + logger.error(message, ...args); + } + }, +}; + function formatMessage(symbol: string, message: unknown, ...args: unknown[]) { const formatted = nodeUtil.format(message, ...args); const indented = redent(formatted, 4); diff --git a/src/user-event/utils/dispatch-event.ts b/src/user-event/utils/dispatch-event.ts index 6e71b5d5..566652ad 100644 --- a/src/user-event/utils/dispatch-event.ts +++ b/src/user-event/utils/dispatch-event.ts @@ -2,7 +2,7 @@ import type { ReactTestInstance } from 'react-test-renderer'; import act from '../../act'; import { isElementMounted } from '../../helpers/component-tree'; import { formatElement } from '../../helpers/format-element'; -import { logger } from '../../helpers/logger'; +import { debugLogger } from '../../helpers/logger'; /** * Basic dispatch event function used by User Event module. @@ -18,9 +18,9 @@ export function dispatchEvent(element: ReactTestInstance, eventName: string, ... const handler = getEventHandler(element, eventName); if (!handler) { - logger.debug( + debugLogger.debug( `User Event: no event handler for "${eventName}" found on ${formatElement(element, { - minimal: true, + compact: true, })}`, ); return;