diff --git a/src/__tests__/component-tree.tsx b/src/__tests__/component-tree.tsx
new file mode 100644
index 0000000..9ed0b38
--- /dev/null
+++ b/src/__tests__/component-tree.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { View } from 'react-native';
+import { render } from '@testing-library/react-native';
+import { getParentElement } from '../component-tree';
+
+function MultipleHostChildren() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+describe('getParentElement()', () => {
+ it('returns host parent for host component', () => {
+ const view = render(
+
+
+
+
+
+ ,
+ );
+
+ const hostParent = getParentElement(view.getByTestId('subject'));
+ expect(hostParent).toBe(view.getByTestId('parent'));
+
+ const hostGrandparent = getParentElement(hostParent);
+ expect(hostGrandparent).toBe(view.getByTestId('grandparent'));
+
+ expect(getParentElement(hostGrandparent)).toBe(null);
+ });
+
+ it('returns host parent for null', () => {
+ expect(getParentElement(null)).toBe(null);
+ });
+
+ it('returns host parent for composite component', () => {
+ const view = render(
+
+
+
+ ,
+ );
+
+ const compositeComponent = view.UNSAFE_getByType(MultipleHostChildren);
+ const hostParent = getParentElement(compositeComponent);
+ expect(hostParent).toBe(view.getByTestId('parent'));
+ });
+});
diff --git a/src/__tests__/to-be-visible.tsx b/src/__tests__/to-be-visible.tsx
index 589a079..281c02a 100644
--- a/src/__tests__/to-be-visible.tsx
+++ b/src/__tests__/to-be-visible.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Modal, View } from 'react-native';
+import { View, Pressable, Modal } from 'react-native';
import { render } from '@testing-library/react-native';
describe('.toBeVisible', () => {
@@ -120,16 +120,32 @@ describe('.toBeVisible', () => {
expect(getByTestId('test')).not.toBeVisible();
});
- it('handles non-React elements', () => {
+ test('handles null elements', () => {
+ expect(() => expect(null).toBeVisible()).toThrowErrorMatchingInlineSnapshot(`
+ "expect(received).toBeVisible()
+
+ received value must be a React Element.
+ Received has value: null"
+ `);
+ });
+
+ test('handles non-React elements', () => {
expect(() => expect({ name: 'Non-React element' }).not.toBeVisible()).toThrow();
expect(() => expect(true).not.toBeVisible()).toThrow();
});
- it('throws an error when expectation is not matched', () => {
+ test('throws an error when expectation is not matched', () => {
const { getByTestId, update } = render();
expect(() => expect(getByTestId('test')).not.toBeVisible()).toThrowErrorMatchingSnapshot();
update();
expect(() => expect(getByTestId('test')).toBeVisible()).toThrowErrorMatchingSnapshot();
});
+
+ test('handles Pressable with function style prop', () => {
+ const { getByTestId } = render(
+ ({ backgroundColor: 'blue' })} />,
+ );
+ expect(getByTestId('test')).toBeVisible();
+ });
});
diff --git a/src/__tests__/to-have-style.tsx b/src/__tests__/to-have-style.tsx
index 8dad4ba..9f621aa 100644
--- a/src/__tests__/to-have-style.tsx
+++ b/src/__tests__/to-have-style.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { StyleSheet, View, Text } from 'react-native';
+import { StyleSheet, View, Text, Pressable } from 'react-native';
import { render } from '@testing-library/react-native';
describe('.toHaveStyle', () => {
@@ -90,4 +90,11 @@ describe('.toHaveStyle', () => {
expect(container).toHaveStyle({ transform: [{ scale: 1 }] }),
).toThrowErrorMatchingSnapshot();
});
+
+ test('handles Pressable with function style prop', () => {
+ const { getByTestId } = render(
+ ({ backgroundColor: 'blue' })} />,
+ );
+ expect(getByTestId('test')).toHaveStyle({ backgroundColor: 'blue' });
+ });
});
diff --git a/src/component-tree.ts b/src/component-tree.ts
new file mode 100644
index 0000000..ce438c9
--- /dev/null
+++ b/src/component-tree.ts
@@ -0,0 +1,37 @@
+import type React from 'react';
+import type { ReactTestInstance } from 'react-test-renderer';
+
+/**
+ * Checks if the given element is a host element.
+ * @param element The element to check.
+ */
+export function isHostElement(element?: ReactTestInstance | null): boolean {
+ return typeof element?.type === 'string';
+}
+
+/**
+ * Returns first host ancestor for given element or first ancestor of one of
+ * passed component types.
+ *
+ * @param element The element start traversing from.
+ * @param componentTypes Additional component types to match.
+ */
+export function getParentElement(
+ element: ReactTestInstance | null,
+ componentTypes: React.ElementType[] = [],
+): ReactTestInstance | null {
+ if (element == null) {
+ return null;
+ }
+
+ let current = element.parent;
+ while (current) {
+ if (isHostElement(current) || componentTypes.includes(current.type)) {
+ return current;
+ }
+
+ current = current.parent;
+ }
+
+ return null;
+}
diff --git a/src/to-be-visible.ts b/src/to-be-visible.ts
index 323c1b3..66c1430 100644
--- a/src/to-be-visible.ts
+++ b/src/to-be-visible.ts
@@ -3,31 +3,40 @@ import { matcherHint } from 'jest-matcher-utils';
import type { ReactTestInstance } from 'react-test-renderer';
import { checkReactElement, printElement } from './utils';
+import { getParentElement } from './component-tree';
-function isStyleVisible(element: ReactTestInstance) {
+function isVisibleForStyles(element: ReactTestInstance) {
const style = element.props.style || {};
const { display, opacity } = StyleSheet.flatten(style);
return display !== 'none' && opacity !== 0;
}
-function isAttributeVisible(element: ReactTestInstance) {
- return element.type !== Modal || element.props.visible !== false;
+function isVisibleForAccessibility(element: ReactTestInstance) {
+ return (
+ !element.props.accessibilityElementsHidden &&
+ element.props.importantForAccessibility !== 'no-hide-descendants'
+ );
}
-function isVisibleForAccessibility(element: ReactTestInstance) {
- const visibleForiOSVoiceOver = !element.props.accessibilityElementsHidden;
- const visibleForAndroidTalkBack =
- element.props.importantForAccessibility !== 'no-hide-descendants';
- return visibleForiOSVoiceOver && visibleForAndroidTalkBack;
+function isModalVisible(element: ReactTestInstance) {
+ return element.type !== Modal || element.props.visible !== false;
}
function isElementVisible(element: ReactTestInstance): boolean {
- return (
- isStyleVisible(element) &&
- isAttributeVisible(element) &&
- isVisibleForAccessibility(element) &&
- (!element.parent || isElementVisible(element.parent))
- );
+ let current: ReactTestInstance | null = element;
+ while (current) {
+ if (
+ !isVisibleForStyles(current) ||
+ !isVisibleForAccessibility(current) ||
+ !isModalVisible(current)
+ ) {
+ return false;
+ }
+
+ current = getParentElement(current, [Modal]);
+ }
+
+ return true;
}
export function toBeVisible(this: jest.MatcherContext, element: ReactTestInstance) {