Skip to content

Commit d65e9f2

Browse files
authored
fix: .toBeVisible error with Pressable function style (#134)
1 parent 5883ed7 commit d65e9f2

File tree

5 files changed

+139
-18
lines changed

5 files changed

+139
-18
lines changed

src/__tests__/component-tree.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import { View } from 'react-native';
3+
import { render } from '@testing-library/react-native';
4+
import { getParentElement } from '../component-tree';
5+
6+
function MultipleHostChildren() {
7+
return (
8+
<>
9+
<View testID="child1" />
10+
<View testID="child2" />
11+
<View testID="child3" />
12+
</>
13+
);
14+
}
15+
16+
describe('getParentElement()', () => {
17+
it('returns host parent for host component', () => {
18+
const view = render(
19+
<View testID="grandparent">
20+
<View testID="parent">
21+
<View testID="subject" />
22+
<View testID="sibling" />
23+
</View>
24+
</View>,
25+
);
26+
27+
const hostParent = getParentElement(view.getByTestId('subject'));
28+
expect(hostParent).toBe(view.getByTestId('parent'));
29+
30+
const hostGrandparent = getParentElement(hostParent);
31+
expect(hostGrandparent).toBe(view.getByTestId('grandparent'));
32+
33+
expect(getParentElement(hostGrandparent)).toBe(null);
34+
});
35+
36+
it('returns host parent for null', () => {
37+
expect(getParentElement(null)).toBe(null);
38+
});
39+
40+
it('returns host parent for composite component', () => {
41+
const view = render(
42+
<View testID="parent">
43+
<MultipleHostChildren />
44+
<View testID="subject" />
45+
</View>,
46+
);
47+
48+
const compositeComponent = view.UNSAFE_getByType(MultipleHostChildren);
49+
const hostParent = getParentElement(compositeComponent);
50+
expect(hostParent).toBe(view.getByTestId('parent'));
51+
});
52+
});

src/__tests__/to-be-visible.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { Modal, View } from 'react-native';
2+
import { View, Pressable, Modal } from 'react-native';
33
import { render } from '@testing-library/react-native';
44

55
describe('.toBeVisible', () => {
@@ -120,16 +120,32 @@ describe('.toBeVisible', () => {
120120
expect(getByTestId('test')).not.toBeVisible();
121121
});
122122

123-
it('handles non-React elements', () => {
123+
test('handles null elements', () => {
124+
expect(() => expect(null).toBeVisible()).toThrowErrorMatchingInlineSnapshot(`
125+
"expect(received).toBeVisible()
126+
127+
received value must be a React Element.
128+
Received has value: null"
129+
`);
130+
});
131+
132+
test('handles non-React elements', () => {
124133
expect(() => expect({ name: 'Non-React element' }).not.toBeVisible()).toThrow();
125134
expect(() => expect(true).not.toBeVisible()).toThrow();
126135
});
127136

128-
it('throws an error when expectation is not matched', () => {
137+
test('throws an error when expectation is not matched', () => {
129138
const { getByTestId, update } = render(<View testID="test" />);
130139
expect(() => expect(getByTestId('test')).not.toBeVisible()).toThrowErrorMatchingSnapshot();
131140

132141
update(<View testID="test" style={{ opacity: 0 }} />);
133142
expect(() => expect(getByTestId('test')).toBeVisible()).toThrowErrorMatchingSnapshot();
134143
});
144+
145+
test('handles Pressable with function style prop', () => {
146+
const { getByTestId } = render(
147+
<Pressable testID="test" style={() => ({ backgroundColor: 'blue' })} />,
148+
);
149+
expect(getByTestId('test')).toBeVisible();
150+
});
135151
});

src/__tests__/to-have-style.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { StyleSheet, View, Text } from 'react-native';
2+
import { StyleSheet, View, Text, Pressable } from 'react-native';
33
import { render } from '@testing-library/react-native';
44

55
describe('.toHaveStyle', () => {
@@ -90,4 +90,11 @@ describe('.toHaveStyle', () => {
9090
expect(container).toHaveStyle({ transform: [{ scale: 1 }] }),
9191
).toThrowErrorMatchingSnapshot();
9292
});
93+
94+
test('handles Pressable with function style prop', () => {
95+
const { getByTestId } = render(
96+
<Pressable testID="test" style={() => ({ backgroundColor: 'blue' })} />,
97+
);
98+
expect(getByTestId('test')).toHaveStyle({ backgroundColor: 'blue' });
99+
});
93100
});

src/component-tree.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type React from 'react';
2+
import type { ReactTestInstance } from 'react-test-renderer';
3+
4+
/**
5+
* Checks if the given element is a host element.
6+
* @param element The element to check.
7+
*/
8+
export function isHostElement(element?: ReactTestInstance | null): boolean {
9+
return typeof element?.type === 'string';
10+
}
11+
12+
/**
13+
* Returns first host ancestor for given element or first ancestor of one of
14+
* passed component types.
15+
*
16+
* @param element The element start traversing from.
17+
* @param componentTypes Additional component types to match.
18+
*/
19+
export function getParentElement(
20+
element: ReactTestInstance | null,
21+
componentTypes: React.ElementType[] = [],
22+
): ReactTestInstance | null {
23+
if (element == null) {
24+
return null;
25+
}
26+
27+
let current = element.parent;
28+
while (current) {
29+
if (isHostElement(current) || componentTypes.includes(current.type)) {
30+
return current;
31+
}
32+
33+
current = current.parent;
34+
}
35+
36+
return null;
37+
}

src/to-be-visible.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,40 @@ import { matcherHint } from 'jest-matcher-utils';
33
import type { ReactTestInstance } from 'react-test-renderer';
44

55
import { checkReactElement, printElement } from './utils';
6+
import { getParentElement } from './component-tree';
67

7-
function isStyleVisible(element: ReactTestInstance) {
8+
function isVisibleForStyles(element: ReactTestInstance) {
89
const style = element.props.style || {};
910
const { display, opacity } = StyleSheet.flatten(style);
1011
return display !== 'none' && opacity !== 0;
1112
}
1213

13-
function isAttributeVisible(element: ReactTestInstance) {
14-
return element.type !== Modal || element.props.visible !== false;
14+
function isVisibleForAccessibility(element: ReactTestInstance) {
15+
return (
16+
!element.props.accessibilityElementsHidden &&
17+
element.props.importantForAccessibility !== 'no-hide-descendants'
18+
);
1519
}
1620

17-
function isVisibleForAccessibility(element: ReactTestInstance) {
18-
const visibleForiOSVoiceOver = !element.props.accessibilityElementsHidden;
19-
const visibleForAndroidTalkBack =
20-
element.props.importantForAccessibility !== 'no-hide-descendants';
21-
return visibleForiOSVoiceOver && visibleForAndroidTalkBack;
21+
function isModalVisible(element: ReactTestInstance) {
22+
return element.type !== Modal || element.props.visible !== false;
2223
}
2324

2425
function isElementVisible(element: ReactTestInstance): boolean {
25-
return (
26-
isStyleVisible(element) &&
27-
isAttributeVisible(element) &&
28-
isVisibleForAccessibility(element) &&
29-
(!element.parent || isElementVisible(element.parent))
30-
);
26+
let current: ReactTestInstance | null = element;
27+
while (current) {
28+
if (
29+
!isVisibleForStyles(current) ||
30+
!isVisibleForAccessibility(current) ||
31+
!isModalVisible(current)
32+
) {
33+
return false;
34+
}
35+
36+
current = getParentElement(current, [Modal]);
37+
}
38+
39+
return true;
3140
}
3241

3342
export function toBeVisible(this: jest.MatcherContext, element: ReactTestInstance) {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy