Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: event debugging (WIP) #1726

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
});

Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -30,6 +35,7 @@ const defaultConfig: Config = {
asyncUtilTimeout: 1000,
defaultIncludeHiddenElements: false,
concurrentRoot: true,
debug: false,
};

let config = { ...defaultConfig };
Expand Down
54 changes: 44 additions & 10 deletions src/fire-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
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 { debugLogger } from './helpers/logger';
import { isPointerEventEnabled } from './helpers/pointer-events';
import { isEditableTextInput } from './helpers/text-input';
import { nativeState } from './native-state';
Expand Down Expand Up @@ -47,29 +49,43 @@
'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' };

Check warning on line 71 in src/fire-event.ts

View check run for this annotation

Codecov / codecov/patch

src/fire-event.ts#L66-L71

Added lines #L66 - L71 were not covered by tests
}

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 };
}

if (touchStart === undefined && touchMove === undefined) {
return { enabled: true };
}

return touchStart === undefined && touchMove === undefined;
return { enabled: false, reason: 'not being a touch responder' };
}

function findEventHandler(
Expand All @@ -80,7 +96,19 @@
const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder;

const handler = getEventHandler(element, eventName);
if (handler && isEventEnabled(element, eventName, touchResponder)) return handler;
if (handler) {
const handlerState = getEventHandlerState(element, eventName, touchResponder);

if (handlerState.enabled) {
return handler;
} else {
debugLogger.warn(
`FireEvent: "${eventName}" event handler is disabled on ${formatElement(element, {
compact: true,
})} due to ${handlerState.reason}.`,
);
}
}

// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (element.parent === null || element.parent.parent === null) {
Expand Down Expand Up @@ -129,6 +157,12 @@

const handler = findEventHandler(element, eventName);
if (!handler) {
debugLogger.warn(
`FireEvent: no enabled event handler for "${eventName}" found on ${formatElement(element, {
compact: true,
})} or its ancestors.`,
);

return;
}

Expand Down
56 changes: 39 additions & 17 deletions src/helpers/format-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)';
Expand Down
27 changes: 27 additions & 0 deletions src/helpers/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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[]) {
Expand All @@ -25,6 +26,32 @@
},
};

export const debugLogger = {
debug(message: unknown, ...args: unknown[]) {
if (getConfig().debug) {
logger.debug(message, ...args);
}

Check warning on line 33 in src/helpers/logger.ts

View check run for this annotation

Codecov / codecov/patch

src/helpers/logger.ts#L32-L33

Added lines #L32 - L33 were not covered by tests
},

info(message: unknown, ...args: unknown[]) {
if (getConfig().debug) {
logger.info(message, ...args);
}

Check warning on line 39 in src/helpers/logger.ts

View check run for this annotation

Codecov / codecov/patch

src/helpers/logger.ts#L37-L39

Added lines #L37 - L39 were not covered by tests
},

warn(message: unknown, ...args: unknown[]) {
if (getConfig().debug) {
logger.warn(message, ...args);
}

Check warning on line 45 in src/helpers/logger.ts

View check run for this annotation

Codecov / codecov/patch

src/helpers/logger.ts#L44-L45

Added lines #L44 - L45 were not covered by tests
},

error(message: unknown, ...args: unknown[]) {
if (getConfig().debug) {
logger.error(message, ...args);
}
},

Check warning on line 52 in src/helpers/logger.ts

View check run for this annotation

Codecov / codecov/patch

src/helpers/logger.ts#L49-L52

Added lines #L49 - L52 were not covered by tests
};

function formatMessage(symbol: string, message: unknown, ...args: unknown[]) {
const formatted = nodeUtil.format(message, ...args);
const indented = redent(formatted, 4);
Expand Down
7 changes: 7 additions & 0 deletions src/user-event/utils/dispatch-event.ts
Original file line number Diff line number Diff line change
@@ -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 { debugLogger } from '../../helpers/logger';

/**
* Basic dispatch event function used by User Event module.
Expand All @@ -16,6 +18,11 @@ export function dispatchEvent(element: ReactTestInstance, eventName: string, ...

const handler = getEventHandler(element, eventName);
if (!handler) {
debugLogger.debug(
`User Event: no event handler for "${eventName}" found on ${formatElement(element, {
compact: true,
})}`,
);
return;
}

Expand Down