Skip to content

Commit c82b906

Browse files
committed
refactor component tree & fire event
1 parent 519000c commit c82b906

File tree

15 files changed

+62
-128
lines changed

15 files changed

+62
-128
lines changed

src/__tests__/event-handler.test.tsx

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22
import { Text, View } from 'react-native';
33

44
import { render, screen } from '..';
5-
import { getEventHandler } from '../event-handler';
5+
import { getEventHandlerFromProps } from '../event-handler';
66

77
test('getEventHandler strict mode', () => {
88
const onPress = jest.fn();
@@ -22,13 +22,13 @@ test('getEventHandler strict mode', () => {
2222
const testOnly = screen.getByTestId('testOnly');
2323
const both = screen.getByTestId('both');
2424

25-
expect(getEventHandler(regular, 'press')).toBe(onPress);
26-
expect(getEventHandler(testOnly, 'press')).toBe(testOnlyOnPress);
27-
expect(getEventHandler(both, 'press')).toBe(onPress);
25+
expect(getEventHandlerFromProps(regular.props, 'press')).toBe(onPress);
26+
expect(getEventHandlerFromProps(testOnly.props, 'press')).toBe(testOnlyOnPress);
27+
expect(getEventHandlerFromProps(both.props, 'press')).toBe(onPress);
2828

29-
expect(getEventHandler(regular, 'onPress')).toBe(undefined);
30-
expect(getEventHandler(testOnly, 'onPress')).toBe(undefined);
31-
expect(getEventHandler(both, 'onPress')).toBe(undefined);
29+
expect(getEventHandlerFromProps(regular.props, 'onPress')).toBe(undefined);
30+
expect(getEventHandlerFromProps(testOnly.props, 'onPress')).toBe(undefined);
31+
expect(getEventHandlerFromProps(both.props, 'onPress')).toBe(undefined);
3232
});
3333

3434
test('getEventHandler loose mode', () => {
@@ -49,11 +49,13 @@ test('getEventHandler loose mode', () => {
4949
const testOnly = screen.getByTestId('testOnly');
5050
const both = screen.getByTestId('both');
5151

52-
expect(getEventHandler(regular, 'press', { loose: true })).toBe(onPress);
53-
expect(getEventHandler(testOnly, 'press', { loose: true })).toBe(testOnlyOnPress);
54-
expect(getEventHandler(both, 'press', { loose: true })).toBe(onPress);
52+
expect(getEventHandlerFromProps(regular.props, 'press', { loose: true })).toBe(onPress);
53+
expect(getEventHandlerFromProps(testOnly.props, 'press', { loose: true })).toBe(testOnlyOnPress);
54+
expect(getEventHandlerFromProps(both.props, 'press', { loose: true })).toBe(onPress);
5555

56-
expect(getEventHandler(regular, 'onPress', { loose: true })).toBe(onPress);
57-
expect(getEventHandler(testOnly, 'onPress', { loose: true })).toBe(testOnlyOnPress);
58-
expect(getEventHandler(both, 'onPress', { loose: true })).toBe(onPress);
56+
expect(getEventHandlerFromProps(regular.props, 'onPress', { loose: true })).toBe(onPress);
57+
expect(getEventHandlerFromProps(testOnly.props, 'onPress', { loose: true })).toBe(
58+
testOnlyOnPress,
59+
);
60+
expect(getEventHandlerFromProps(both.props, 'onPress', { loose: true })).toBe(onPress);
5961
});

src/event-handler.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
1-
import type { HostElement } from 'universal-test-renderer';
1+
export type EventHandler = (...args: unknown[]) => unknown;
22

33
export type EventHandlerOptions = {
44
/** Include check for event handler named without adding `on*` prefix. */
55
loose?: boolean;
66
};
77

8-
export function getEventHandler(
9-
element: HostElement,
8+
export function getEventHandlerFromProps(
9+
props: Record<string, unknown>,
1010
eventName: string,
1111
options?: EventHandlerOptions,
12-
) {
12+
): EventHandler | undefined {
1313
const handlerName = getEventHandlerName(eventName);
14-
if (typeof element.props[handlerName] === 'function') {
15-
return element.props[handlerName];
14+
if (typeof props[handlerName] === 'function') {
15+
return props[handlerName] as EventHandler;
1616
}
1717

18-
if (options?.loose && typeof element.props[eventName] === 'function') {
19-
return element.props[eventName];
18+
if (options?.loose && typeof props[eventName] === 'function') {
19+
return props[eventName] as EventHandler;
2020
}
2121

22-
if (typeof element.props[`testOnly_${handlerName}`] === 'function') {
23-
return element.props[`testOnly_${handlerName}`];
22+
if (typeof props[`testOnly_${handlerName}`] === 'function') {
23+
return props[`testOnly_${handlerName}`] as EventHandler;
2424
}
2525

26-
if (options?.loose && typeof element.props[`testOnly_${eventName}`] === 'function') {
27-
return element.props[`testOnly_${eventName}`];
26+
if (options?.loose && typeof props[`testOnly_${eventName}`] === 'function') {
27+
return props[`testOnly_${eventName}`] as EventHandler;
2828
}
2929

3030
return undefined;

src/fire-event.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,14 @@ import type {
88
import type { HostElement } from 'universal-test-renderer';
99

1010
import act from './act';
11-
import { getEventHandler } from './event-handler';
11+
import { EventHandler, getEventHandlerFromProps } from './event-handler';
1212
import { isElementMounted, isHostElement } from './helpers/component-tree';
1313
import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
1414
import { isPointerEventEnabled } from './helpers/pointer-events';
1515
import { isEditableTextInput } from './helpers/text-input';
1616
import { nativeState } from './native-state';
1717
import type { Point, StringWithAutocomplete } from './types';
1818

19-
type EventHandler = (...args: unknown[]) => unknown;
20-
2119
export function isTouchResponder(element: HostElement) {
2220
if (!isHostElement(element)) {
2321
return false;
@@ -81,7 +79,7 @@ function findEventHandler(
8179
): EventHandler | null {
8280
const touchResponder = isTouchResponder(element) ? element : nearestTouchResponder;
8381

84-
const handler = getEventHandler(element, eventName, { loose: true });
82+
const handler = getEventHandlerFromProps(element.props, eventName, { loose: true });
8583
if (handler && isEventEnabled(element, eventName, touchResponder)) {
8684
return handler;
8785
}

src/helpers/__tests__/component-tree.test.tsx

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import React from 'react';
2-
import { Text, TextInput, View } from 'react-native';
2+
import { View } from 'react-native';
33

44
import { render, screen } from '../..';
5-
import { getHostSelves, getHostSiblings, getContainerElement } from '../component-tree';
6-
7-
function ZeroHostChildren() {
8-
return <></>;
9-
}
5+
import { getContainerElement, getHostSiblings } from '../component-tree';
106

117
function MultipleHostChildren() {
128
return (
@@ -18,55 +14,6 @@ function MultipleHostChildren() {
1814
);
1915
}
2016

21-
describe('getHostSelves()', () => {
22-
it('returns passed element for host components', () => {
23-
render(
24-
<View testID="grandparent">
25-
<View testID="parent">
26-
<View testID="subject" />
27-
<View testID="sibling" />
28-
</View>
29-
</View>,
30-
);
31-
32-
const hostSubject = screen.getByTestId('subject');
33-
expect(getHostSelves(hostSubject)).toEqual([hostSubject]);
34-
35-
const hostSibling = screen.getByTestId('sibling');
36-
expect(getHostSelves(hostSibling)).toEqual([hostSibling]);
37-
38-
const hostParent = screen.getByTestId('parent');
39-
expect(getHostSelves(hostParent)).toEqual([hostParent]);
40-
41-
const hostGrandparent = screen.getByTestId('grandparent');
42-
expect(getHostSelves(hostGrandparent)).toEqual([hostGrandparent]);
43-
});
44-
45-
test('returns single host element for React Native composite components', () => {
46-
render(
47-
<View testID="parent">
48-
<Text testID="text">Text</Text>
49-
<TextInput
50-
testID="textInput"
51-
defaultValue="TextInputValue"
52-
placeholder="TextInputPlaceholder"
53-
/>
54-
</View>,
55-
);
56-
57-
const compositeText = screen.getByText('Text');
58-
const hostText = screen.getByTestId('text');
59-
expect(getHostSelves(compositeText)).toEqual([hostText]);
60-
61-
const compositeTextInputByValue = screen.getByDisplayValue('TextInputValue');
62-
const compositeTextInputByPlaceholder = screen.getByPlaceholderText('TextInputPlaceholder');
63-
64-
const hostTextInput = screen.getByTestId('textInput');
65-
expect(getHostSelves(compositeTextInputByValue)).toEqual([hostTextInput]);
66-
expect(getHostSelves(compositeTextInputByPlaceholder)).toEqual([hostTextInput]);
67-
});
68-
});
69-
7017
describe('getHostSiblings()', () => {
7118
it('returns host siblings for host component', () => {
7219
render(

src/helpers/accessibility.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { AccessibilityRole, AccessibilityState, AccessibilityValue, Role }
22
import { StyleSheet } from 'react-native';
33
import type { HostElement } from 'universal-test-renderer';
44

5-
import { getHostSiblings, getContainerElement, isHostElement } from './component-tree';
5+
import { getContainerElement, getHostSiblings, isHostElement } from './component-tree';
66
import { findAll } from './find-all';
77
import { isHostImage, isHostSwitch, isHostText, isHostTextInput } from './host-component-names';
88
import { getTextContent } from './text-content';
@@ -157,9 +157,9 @@ export function computeAriaModal(element: HostElement): boolean | undefined {
157157
export function computeAriaLabel(element: HostElement): string | undefined {
158158
const labelElementId = element.props['aria-labelledby'] ?? element.props.accessibilityLabelledBy;
159159
if (labelElementId) {
160-
const rootElement = getContainerElement(element);
160+
const container = getContainerElement(element);
161161
const labelElement = findAll(
162-
rootElement,
162+
container,
163163
(node) => isHostElement(node) && node.props.nativeID === labelElementId,
164164
{ includeHiddenElements: true },
165165
);

src/helpers/component-tree.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,20 @@ export function isElementMounted(element: HostElement) {
1414
return getContainerElement(element) === screen.container;
1515
}
1616

17-
/**
18-
* Return the array of host elements that represent the passed element.
19-
*
20-
* @param element The element start traversing from.
21-
* @returns If the passed element is a host element, it will return an array containing only that element,
22-
* if the passed element is a composite element, it will return an array containing its host children (zero, one or many).
23-
*/
24-
export function getHostSelves(element: HostElement | null): HostElement[] {
25-
return isHostElement(element) ? [element] : getHostChildren(element);
26-
}
27-
2817
/**
2918
* Returns host siblings for given element.
3019
* @param element The element start traversing from.
3120
*/
32-
export function getHostSiblings(element: HostElement | null): HostElement[] {
33-
const hostParent = element?.parent;
34-
const hostSelves = getHostSelves(element);
35-
return getHostChildren(hostParent).filter((sibling) => !hostSelves.includes(sibling));
21+
export function getHostSiblings(element: HostElement): HostElement[] {
22+
// Should not happen
23+
const parent = element.parent;
24+
if (!parent) {
25+
return [];
26+
}
27+
28+
return parent.children.filter(
29+
(sibling) => typeof sibling !== 'string' && sibling !== element,
30+
) as HostElement[];
3631
}
3732

3833
/**
Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { HostElement } from 'universal-test-renderer';
22

3-
import type { HostElement } from './component-tree';
4-
53
const HOST_TEXT_NAMES = ['Text', 'RCTText'];
64
const HOST_TEXT_INPUT_NAMES = ['TextInput'];
75
const HOST_IMAGE_NAMES = ['Image'];
@@ -13,46 +11,46 @@ const HOST_MODAL_NAMES = ['Modal'];
1311
* Checks if the given element is a host Text element.
1412
* @param element The element to check.
1513
*/
16-
export function isHostText(element: HostElement | null): element is HostElement {
14+
export function isHostText(element: HostElement | null) {
1715
return typeof element?.type === 'string' && HOST_TEXT_NAMES.includes(element.type);
1816
}
1917

2018
/**
2119
* Checks if the given element is a host TextInput element.
2220
* @param element The element to check.
2321
*/
24-
export function isHostTextInput(element: HostElement | null): element is HostElement {
22+
export function isHostTextInput(element: HostElement | null) {
2523
return typeof element?.type === 'string' && HOST_TEXT_INPUT_NAMES.includes(element.type);
2624
}
2725

2826
/**
2927
* Checks if the given element is a host Image element.
3028
* @param element The element to check.
3129
*/
32-
export function isHostImage(element: HostElement | null): element is HostElement {
30+
export function isHostImage(element: HostElement | null) {
3331
return typeof element?.type === 'string' && HOST_IMAGE_NAMES.includes(element.type);
3432
}
3533

3634
/**
3735
* Checks if the given element is a host Switch element.
3836
* @param element The element to check.
3937
*/
40-
export function isHostSwitch(element: HostElement | null): element is HostElement {
38+
export function isHostSwitch(element: HostElement | null) {
4139
return typeof element?.type === 'string' && HOST_SWITCH_NAMES.includes(element.type);
4240
}
4341

4442
/**
4543
* Checks if the given element is a host ScrollView element.
4644
* @param element The element to check.
4745
*/
48-
export function isHostScrollView(element: HostElement | null): element is HostElement {
46+
export function isHostScrollView(element: HostElement | null) {
4947
return typeof element?.type === 'string' && HOST_SCROLL_VIEW_NAMES.includes(element.type);
5048
}
5149

5250
/**
5351
* Checks if the given element is a host Modal element.
5452
* @param element The element to check.
5553
*/
56-
export function isHostModal(element: HostElement | null): element is HostElement {
54+
export function isHostModal(element: HostElement | null) {
5755
return typeof element?.type === 'string' && HOST_MODAL_NAMES.includes(element.type);
5856
}

src/helpers/pointer-events.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { StyleSheet } from 'react-native';
22
import type { HostElement } from 'universal-test-renderer';
33

4-
import { getHostParent } from './component-tree';
5-
64
/**
75
* pointerEvents controls whether the View can be the target of touch events.
86
* 'auto': The View and its children can be the target of touch events.

src/matchers/to-be-disabled.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import type { HostElement } from 'universal-test-renderer';
21
import { matcherHint } from 'jest-matcher-utils';
32
import redent from 'redent';
3+
import type { HostElement } from 'universal-test-renderer';
44

55
import { computeAriaDisabled } from '../helpers/accessibility';
6-
import { getHostParent } from '../helpers/component-tree';
76
import { formatElement } from '../helpers/format-element';
87
import { checkHostElement } from './utils';
98

src/matchers/to-be-on-the-screen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { HostElement } from 'universal-test-renderer';
21
import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
32
import redent from 'redent';
3+
import type { HostElement } from 'universal-test-renderer';
44

55
import { getContainerElement } from '../helpers/component-tree';
66
import { formatElement } from '../helpers/format-element';

0 commit comments

Comments
 (0)