Skip to content
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
69 changes: 69 additions & 0 deletions src/__tests__/fire-event-debug.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as React from 'react';
import { Pressable, Text, View } from 'react-native';

import { configure, fireEvent, render, screen } from '..';

Check failure on line 4 in src/__tests__/fire-event-debug.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

'configure' is defined but never used
import { _console } from '../helpers/logger';

beforeEach(() => {
jest.spyOn(_console, 'debug').mockImplementation(() => {});
jest.spyOn(_console, 'info').mockImplementation(() => {});
jest.spyOn(_console, 'warn').mockImplementation(() => {});
jest.spyOn(_console, 'error').mockImplementation(() => {});
});

test('should log warning when firing event on element without handler', () => {
render(
<View>
<Text>No handler</Text>
</View>,
);

fireEvent.press(screen.getByText('No handler'));

expect(_console.warn).toHaveBeenCalledTimes(1);
expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(`
" ▲ Fire Event: no event handler for "press" event found on <Text>No handler</Text> or any of its ancestors.
"
`);
});

test('should log warning when firing event on single disabled element', () => {
render(
<View>
<Pressable onPress={() => {}} disabled>
<Text>Disabled button</Text>
</Pressable>
</View>,
);

fireEvent.press(screen.getByText('Disabled button'));

expect(_console.warn).toHaveBeenCalledTimes(1);
expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(`
" ▲ Fire Event: no enabled event handler for "press" event found. Found disabled event handler(s) on:
- <Pressable disabled={true} /> (composite element)
"
`);
});

test('should log warning about multiple disabled handlers', () => {
render(
<View>
<Pressable testID="outer" onPress={() => {}} disabled>
<Pressable testID="inner" onPress={() => {}} disabled>
<Text>Nested disabled</Text>
</Pressable>
</Pressable>
</View>,
);

fireEvent.press(screen.getByText('Nested disabled'));

expect(_console.warn).toHaveBeenCalledTimes(1);
expect(jest.mocked(_console.warn).mock.calls[0][0]).toMatchInlineSnapshot(`
" ▲ Fire Event: no enabled event handler for "press" event found. Found disabled event handler(s) on:
- <Pressable disabled={true} testID="inner" /> (composite element)
- <Pressable disabled={true} testID="outer" /> (composite element)
"
`);
});
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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

i would add what it helps log : user event and fire event issues (for now)

*/
debug: boolean;
};

export type ConfigAliasOptions = {
Expand All @@ -30,6 +35,7 @@ const defaultConfig: Config = {
asyncUtilTimeout: 1000,
defaultIncludeHiddenElements: false,
concurrentRoot: true,
debug: false,
Copy link
Collaborator

Choose a reason for hiding this comment

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

imo true is a good default because otherwise i fear not many people will hear about it and remember to turn it on
do you see more advantages with false as default ?

};

let config = { ...defaultConfig };
Expand Down
50 changes: 45 additions & 5 deletions src/fire-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import type { ReactTestInstance } from 'react-test-renderer';
import act from './act';
import { getEventHandler } from './event-handler';
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 { nativeState } from './native-state';
Expand Down Expand Up @@ -74,23 +76,41 @@ export function isEventEnabled(
return touchStart === undefined && touchMove === undefined;
}

type FindEventHandlerState = {
nearestTouchResponder?: ReactTestInstance;
disabledElements: ReactTestInstance[];
targetElement: ReactTestInstance;
};

function findEventHandler(
element: ReactTestInstance,
eventName: string,
nearestTouchResponder?: ReactTestInstance,
state: FindEventHandlerState = {
disabledElements: [],
targetElement: element,
},
): EventHandler | null {
const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder;
const touchResponder = isTouchResponder(element) ? element : state.nearestTouchResponder;

const handler = getEventHandler(element, eventName, { loose: true });
if (handler && isEventEnabled(element, eventName, touchResponder)) {
return handler;
if (handler) {
const isEnabled = isEventEnabled(element, eventName, touchResponder);
if (isEnabled) {
return handler;
} else {
state.disabledElements.push(element);
}
}

if (element.parent === null) {
logger.warn(formatEnabledEventHandlerNotFound(eventName, state));
return null;
}

return findEventHandler(element.parent, eventName, touchResponder);
return findEventHandler(element.parent, eventName, {
...state,
nearestTouchResponder: touchResponder,
});
}

// String union type of keys of T that start with on, stripped of 'on'
Expand Down Expand Up @@ -211,3 +231,23 @@ function tryGetContentOffset(event: unknown): Point | null {

return null;
}

function formatEnabledEventHandlerNotFound(eventName: string, state: FindEventHandlerState) {
if (state.disabledElements.length === 0) {
return `Fire Event: no event handler for "${eventName}" event found on ${formatElement(
state.targetElement,
{
compact: true,
},
)} or any of its ancestors.`;
}

return `Fire Event: no enabled event handler for "${eventName}" event found. Found disabled event handler(s) on:\n${state.disabledElements
.map(
(e) =>
` - ${formatElement(e, { compact: true })}${
typeof e.type === 'string' ? '' : ' (composite element)'
}`,
)
.join('\n')}`;
}
21 changes: 20 additions & 1 deletion src/helpers/format-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function formatElement(
// 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}`,
type: formatElementName(element.type),
props: mapProps ? mapProps(props) : props,
children: childrenToDisplay,
},
Expand All @@ -52,6 +52,25 @@ export function formatElement(
);
}

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
8 changes: 6 additions & 2 deletions src/helpers/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import pc from 'picocolors';
import redent from 'redent';
import * as nodeUtil from 'util';

import { getConfig } from '../config';

export const _console = {
debug: nodeConsole.debug,
info: nodeConsole.info,
Expand All @@ -12,8 +14,10 @@ export const _console = {

export const logger = {
debug(message: unknown, ...args: unknown[]) {
const output = formatMessage('●', message, ...args);
_console.debug(pc.dim(output));
if (getConfig().debug) {
const output = formatMessage('●', message, ...args);
_console.debug(pc.dim(output));
}
},

info(message: unknown, ...args: unknown[]) {
Expand Down
1 change: 1 addition & 0 deletions src/helpers/map-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const propsToDisplay = [
'aria-valuenow',
'aria-valuetext',
'defaultValue',
'disabled',
'editable',
'importantForAccessibility',
'nativeID',
Expand Down
14 changes: 13 additions & 1 deletion src/user-event/clear.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { ReactTestInstance } from 'react-test-renderer';

import { ErrorWithStack } from '../helpers/errors';
import { formatElement } from '../helpers/format-element';
import { isHostTextInput } from '../helpers/host-component-names';
import { logger } from '../helpers/logger';
import { isPointerEventEnabled } from '../helpers/pointer-events';
import { getTextInputValue, isEditableTextInput } from '../helpers/text-input';
import { EventBuilder } from './event-builder';
Expand All @@ -17,7 +19,17 @@ export async function clear(this: UserEventInstance, element: ReactTestInstance)
);
}

if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
if (!isEditableTextInput(element)) {
logger.warn(
`User Event (clear): element ${formatElement(element, { compact: true })} is not editable.`,
);
return;
}

if (!isPointerEventEnabled(element)) {
logger.warn(
`User Event (clear): element ${formatElement(element, { compact: true })} has pointer event handlers disabled.`,
);
return;
}

Expand Down
14 changes: 13 additions & 1 deletion src/user-event/paste.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactTestInstance } from 'react-test-renderer';

Check failure on line 1 in src/user-event/paste.ts

View workflow job for this annotation

GitHub Actions / Lint

Run autofix to sort these imports!

import { ErrorWithStack } from '../helpers/errors';
import { isHostTextInput } from '../helpers/host-component-names';
Expand All @@ -8,6 +8,8 @@
import { EventBuilder } from './event-builder';
import type { UserEventInstance } from './setup';
import { dispatchEvent, getTextContentSize, wait } from './utils';
import { formatElement } from '../helpers/format-element';
import { logger } from '../helpers/logger';

export async function paste(
this: UserEventInstance,
Expand All @@ -21,7 +23,17 @@
);
}

if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
if (!isEditableTextInput(element)) {
logger.warn(
`User Event (paste): element ${formatElement(element, { compact: true })} is not editable.`,
);
return;
}

if (!isPointerEventEnabled(element)) {
logger.warn(
`User Event (paste): element ${formatElement(element, { compact: true })} has pointer event handlers disabled.`,
);
return;
}

Expand Down
14 changes: 12 additions & 2 deletions src/user-event/type/type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactTestInstance } from 'react-test-renderer';

Check failure on line 1 in src/user-event/type/type.ts

View workflow job for this annotation

GitHub Actions / Lint

Run autofix to sort these imports!

import { ErrorWithStack } from '../../helpers/errors';
import { isHostTextInput } from '../../helpers/host-component-names';
Expand All @@ -9,6 +9,8 @@
import type { UserEventConfig, UserEventInstance } from '../setup';
import { dispatchEvent, getTextContentSize, wait } from '../utils';
import { parseKeys } from './parse-keys';
import { logger } from '../../helpers/logger';
import { formatElement } from '../../helpers/format-element';

export interface TypeOptions {
skipPress?: boolean;
Expand All @@ -29,11 +31,19 @@
);
}

// Skip events if the element is disabled
if (!isEditableTextInput(element) || !isPointerEventEnabled(element)) {
if (!isEditableTextInput(element)) {
logger.warn(
`User Event (type): element ${formatElement(element, { compact: true })} is not editable.`,
);
return;
}

if (!isPointerEventEnabled(element)) {
logger.warn(
`User Event (type): element ${formatElement(element, { compact: true })} has pointer event handlers disabled.`,
);
return;
}
const keys = parseKeys(text);

if (!options?.skipPress) {
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
Expand Up @@ -3,6 +3,8 @@ import type { ReactTestInstance } from 'react-test-renderer';
import act from '../../act';
import { getEventHandler } from '../../event-handler';
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.
Expand All @@ -22,6 +24,11 @@ export async function dispatchEvent(

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

Expand Down
Loading