Skip to content

Commit

Permalink
fix: .toBeVisible error with Pressable function style (#134)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaibarnes authored Dec 7, 2022
1 parent 5883ed7 commit d65e9f2
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 18 deletions.
52 changes: 52 additions & 0 deletions src/__tests__/component-tree.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<View testID="child1" />
<View testID="child2" />
<View testID="child3" />
</>
);
}

describe('getParentElement()', () => {
it('returns host parent for host component', () => {
const view = render(
<View testID="grandparent">
<View testID="parent">
<View testID="subject" />
<View testID="sibling" />
</View>
</View>,
);

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(
<View testID="parent">
<MultipleHostChildren />
<View testID="subject" />
</View>,
);

const compositeComponent = view.UNSAFE_getByType(MultipleHostChildren);
const hostParent = getParentElement(compositeComponent);
expect(hostParent).toBe(view.getByTestId('parent'));
});
});
22 changes: 19 additions & 3 deletions src/__tests__/to-be-visible.tsx
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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(<View testID="test" />);
expect(() => expect(getByTestId('test')).not.toBeVisible()).toThrowErrorMatchingSnapshot();

update(<View testID="test" style={{ opacity: 0 }} />);
expect(() => expect(getByTestId('test')).toBeVisible()).toThrowErrorMatchingSnapshot();
});

test('handles Pressable with function style prop', () => {
const { getByTestId } = render(
<Pressable testID="test" style={() => ({ backgroundColor: 'blue' })} />,
);
expect(getByTestId('test')).toBeVisible();
});
});
9 changes: 8 additions & 1 deletion src/__tests__/to-have-style.tsx
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -90,4 +90,11 @@ describe('.toHaveStyle', () => {
expect(container).toHaveStyle({ transform: [{ scale: 1 }] }),
).toThrowErrorMatchingSnapshot();
});

test('handles Pressable with function style prop', () => {
const { getByTestId } = render(
<Pressable testID="test" style={() => ({ backgroundColor: 'blue' })} />,
);
expect(getByTestId('test')).toHaveStyle({ backgroundColor: 'blue' });
});
});
37 changes: 37 additions & 0 deletions src/component-tree.ts
Original file line number Diff line number Diff line change
@@ -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;
}
37 changes: 23 additions & 14 deletions src/to-be-visible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit d65e9f2

Please sign in to comment.