Skip to content
Next Next commit
feat: add toContainElement matcher
  • Loading branch information
siepra authored and mdjastrzebski committed Sep 19, 2023
commit fe9e6f1c45da4fc7130e24acf3084d80918c833c
38 changes: 38 additions & 0 deletions src/matchers/__tests__/to-contain-element.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as React from 'react';
import { View } from 'react-native';
import { render, screen } from '../..';
import '../extend-expect';

test('toContainElement() on parent view', () => {
render(
<View testID="parent">
<View testID="child" />
</View>
);

const parent = screen.getByTestId('parent');
const child = screen.getByTestId('child');

expect(parent).toContainElement(child);

expect(() => expect(parent).not.toContainElement(child))
.toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toContainElement(element)

<View
children={
<View
testID="child"
/>
}
testID="parent"
/>

contains:

<View
testID="child"
/>
"
`);
});
2 changes: 2 additions & 0 deletions src/matchers/extend-expect.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { StyleProp } from 'react-native';
import { ReactTestInstance } from 'react-test-renderer';
import type { TextMatch, TextMatchOptions } from '../matches';
import type { Style } from './to-have-style';

Expand All @@ -12,6 +13,7 @@ export interface JestNativeMatchers<R> {
toBePartiallyChecked(): R;
toBeSelected(): R;
toBeVisible(): R;
toContainElement(element: ReactTestInstance | null): R;
toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
toHaveProp(name: string, expectedValue?: unknown): R;
toHaveStyle(style: StyleProp<Style>): R;
Expand Down
2 changes: 2 additions & 0 deletions src/matchers/extend-expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { toBeEmptyElement } from './to-be-empty-element';
import { toBePartiallyChecked } from './to-be-partially-checked';
import { toBeSelected } from './to-be-selected';
import { toBeVisible } from './to-be-visible';
import { toContainElement } from './to-contain-element';
import { toHaveDisplayValue } from './to-have-display-value';
import { toHaveProp } from './to-have-prop';
import { toHaveStyle } from './to-have-style';
Expand All @@ -23,6 +24,7 @@ expect.extend({
toBePartiallyChecked,
toBeSelected,
toBeVisible,
toContainElement,
toHaveDisplayValue,
toHaveProp,
toHaveStyle,
Expand Down
1 change: 1 addition & 0 deletions src/matchers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { toHaveProp } from './to-have-prop';
export { toHaveStyle } from './to-have-style';
export { toHaveTextContent } from './to-have-text-content';
export { toBeSelected } from './to-be-selected';
export { toContainElement } from './to-contain-element';
41 changes: 41 additions & 0 deletions src/matchers/to-contain-element.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ReactTestInstance } from 'react-test-renderer';
import { matcherHint, RECEIVED_COLOR as receivedColor } from 'jest-matcher-utils';
import { checkHostElement, printElement } from './utils';

export function toContainElement(
this: jest.MatcherContext,
container: ReactTestInstance,
element: ReactTestInstance | null
) {
if (element !== null || !this.isNot) {
checkHostElement(element, toContainElement, this);
}

let matches = [];

if (element) {
matches = container.findAll((node) => {
return (
node.type === element.type && this.equals(node.props, element.props)
);
});
}

return {
pass: Boolean(matches.length),
message: () => {
return [
matcherHint(
`${this.isNot ? '.not' : ''}.toContainElement`,
'element',
'element'
),
'',
receivedColor(`${printElement(container)} ${
this.isNot ? '\n\ncontains:\n\n' : '\n\ndoes not contain:\n\n'
} ${printElement(element)}
`),
].join('\n');
},
};
}
27 changes: 27 additions & 0 deletions src/matchers/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import redent from 'redent';
import { isHostElement } from '../helpers/component-tree';
import { defaultMapProps } from '../helpers/format-default';

const { ReactTestComponent, ReactElement } = plugins;

class HostElementTypeError extends Error {
constructor(
received: unknown,
Expand Down Expand Up @@ -127,3 +129,28 @@ export function formatMessage(
function formatValue(value: unknown) {
return typeof value === 'string' ? value : stringify(value);
}

export function printElement(element: ReactTestInstance | null) {
if (element == null) {
return 'null';
}

return redent(
prettyFormat(
{
// 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,
props: element.props,
},
{
plugins: [ReactTestComponent, ReactElement],
printFunctionName: false,
printBasicPrototype: false,
highlight: true,
}
),
2
);
}