Skip to content

Commit d898a9d

Browse files
feat: toContainElement matcher (#1495)
* feat: add toContainElement matcher * chore: replace printElement with formatElement * chore: mimic jest-dom instead of jest-native * fix: checking host element * test: passing null as argument * chore: rename basic test case * test: negative case * test: on null elements * fix: validating host container and element * refactor: code review changes * refactor: tweaks * refactors: cleanup --------- Co-authored-by: Maciej Jastrzebski <mdjastrzebski@gmail.com>
1 parent d36137b commit d898a9d

File tree

5 files changed

+187
-3
lines changed

5 files changed

+187
-3
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import * as React from 'react';
2+
import { View } from 'react-native';
3+
import { render, screen } from '../..';
4+
import '../extend-expect';
5+
6+
test('toContainElement() supports basic case', () => {
7+
render(
8+
<View testID="parent">
9+
<View testID="child" />
10+
</View>
11+
);
12+
13+
const parent = screen.getByTestId('parent');
14+
const child = screen.getByTestId('child');
15+
16+
expect(parent).toContainElement(child);
17+
18+
expect(() => expect(parent).not.toContainElement(child))
19+
.toThrowErrorMatchingInlineSnapshot(`
20+
"expect(container).not.toContainElement(element)
21+
22+
<View
23+
testID="parent"
24+
/>
25+
26+
contains:
27+
28+
<View
29+
testID="child"
30+
/>
31+
"
32+
`);
33+
});
34+
35+
test('toContainElement() supports negative case', () => {
36+
render(
37+
<>
38+
<View testID="view1" />
39+
<View testID="view2" />
40+
</>
41+
);
42+
43+
const view1 = screen.getByTestId('view1');
44+
const view2 = screen.getByTestId('view2');
45+
46+
expect(view1).not.toContainElement(view2);
47+
expect(view2).not.toContainElement(view1);
48+
49+
expect(() => expect(view1).toContainElement(view2))
50+
.toThrowErrorMatchingInlineSnapshot(`
51+
"expect(container).toContainElement(element)
52+
53+
<View
54+
testID="view1"
55+
/>
56+
57+
does not contain:
58+
59+
<View
60+
testID="view2"
61+
/>
62+
"
63+
`);
64+
});
65+
66+
test('toContainElement() handles null container', () => {
67+
render(<View testID="view" />);
68+
69+
const view = screen.getByTestId('view');
70+
71+
expect(() => expect(null).toContainElement(view))
72+
.toThrowErrorMatchingInlineSnapshot(`
73+
"expect(received).toContainElement()
74+
75+
received value must be a host element.
76+
Received has value: null"
77+
`);
78+
});
79+
80+
test('toContainElement() handles null element', () => {
81+
render(<View testID="view" />);
82+
83+
const view = screen.getByTestId('view');
84+
85+
expect(view).not.toContainElement(null);
86+
87+
expect(() => expect(view).toContainElement(null))
88+
.toThrowErrorMatchingInlineSnapshot(`
89+
"expect(container).toContainElement(element)
90+
91+
<View
92+
testID="view"
93+
/>
94+
95+
does not contain:
96+
97+
null
98+
"
99+
`);
100+
});
101+
102+
test('toContainElement() handles non-element container', () => {
103+
render(<View testID="view" />);
104+
105+
const view = screen.getByTestId('view');
106+
107+
expect(() => expect({ name: 'non-element' }).not.toContainElement(view))
108+
.toThrowErrorMatchingInlineSnapshot(`
109+
"expect(received).not.toContainElement()
110+
111+
received value must be a host element.
112+
Received has type: object
113+
Received has value: {"name": "non-element"}"
114+
`);
115+
116+
expect(() => expect(true).not.toContainElement(view))
117+
.toThrowErrorMatchingInlineSnapshot(`
118+
"expect(received).not.toContainElement()
119+
120+
received value must be a host element.
121+
Received has type: boolean
122+
Received has value: true"
123+
`);
124+
});
125+
126+
test('toContainElement() handles non-element element', () => {
127+
render(<View testID="view" />);
128+
129+
const view = screen.getByTestId('view');
130+
131+
expect(() =>
132+
// @ts-expect-error
133+
expect(view).not.toContainElement({ name: 'non-element' })
134+
).toThrowErrorMatchingInlineSnapshot(`
135+
"expect(received).not.toContainElement()
136+
137+
received value must be a host element.
138+
Received has type: object
139+
Received has value: {"name": "non-element"}"
140+
`);
141+
});

src/matchers/extend-expect.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { StyleProp } from 'react-native';
2+
import type { ReactTestInstance } from 'react-test-renderer';
23
import type { TextMatch, TextMatchOptions } from '../matches';
34
import type { Style } from './to-have-style';
45

@@ -12,6 +13,7 @@ export interface JestNativeMatchers<R> {
1213
toBePartiallyChecked(): R;
1314
toBeSelected(): R;
1415
toBeVisible(): R;
16+
toContainElement(element: ReactTestInstance | null): R;
1517
toHaveDisplayValue(expectedValue: TextMatch, options?: TextMatchOptions): R;
1618
toHaveProp(name: string, expectedValue?: unknown): R;
1719
toHaveStyle(style: StyleProp<Style>): R;

src/matchers/extend-expect.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { toBeEmptyElement } from './to-be-empty-element';
88
import { toBePartiallyChecked } from './to-be-partially-checked';
99
import { toBeSelected } from './to-be-selected';
1010
import { toBeVisible } from './to-be-visible';
11+
import { toContainElement } from './to-contain-element';
1112
import { toHaveDisplayValue } from './to-have-display-value';
1213
import { toHaveProp } from './to-have-prop';
1314
import { toHaveStyle } from './to-have-style';
@@ -23,6 +24,7 @@ expect.extend({
2324
toBePartiallyChecked,
2425
toBeSelected,
2526
toBeVisible,
27+
toContainElement,
2628
toHaveDisplayValue,
2729
toHaveProp,
2830
toHaveStyle,

src/matchers/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
export { toBeOnTheScreen } from './to-be-on-the-screen';
1+
export { toBeBusy } from './to-be-busy';
22
export { toBeChecked } from './to-be-checked';
33
export { toBeDisabled, toBeEnabled } from './to-be-disabled';
4-
export { toBeBusy } from './to-be-busy';
54
export { toBeEmptyElement } from './to-be-empty-element';
5+
export { toBeOnTheScreen } from './to-be-on-the-screen';
66
export { toBePartiallyChecked } from './to-be-partially-checked';
7+
export { toBeSelected } from './to-be-selected';
78
export { toBeVisible } from './to-be-visible';
9+
export { toContainElement } from './to-contain-element';
810
export { toHaveDisplayValue } from './to-have-display-value';
911
export { toHaveProp } from './to-have-prop';
1012
export { toHaveStyle } from './to-have-style';
1113
export { toHaveTextContent } from './to-have-text-content';
12-
export { toBeSelected } from './to-be-selected';

src/matchers/to-contain-element.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ReactTestInstance } from 'react-test-renderer';
2+
import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
3+
import { checkHostElement, formatElement } from './utils';
4+
5+
export function toContainElement(
6+
this: jest.MatcherContext,
7+
container: ReactTestInstance,
8+
element: ReactTestInstance | null
9+
) {
10+
checkHostElement(container, toContainElement, this);
11+
12+
if (element !== null) {
13+
checkHostElement(element, toContainElement, this);
14+
}
15+
16+
let matches: ReactTestInstance[] = [];
17+
if (element) {
18+
matches = container.findAll((node) => node === element);
19+
}
20+
21+
return {
22+
pass: matches.length > 0,
23+
message: () => {
24+
return [
25+
matcherHint(
26+
`${this.isNot ? '.not' : ''}.toContainElement`,
27+
'container',
28+
'element'
29+
),
30+
'',
31+
RECEIVED_COLOR(`${formatElement(container)} ${
32+
this.isNot ? '\n\ncontains:\n\n' : '\n\ndoes not contain:\n\n'
33+
} ${formatElement(element)}
34+
`),
35+
].join('\n');
36+
},
37+
};
38+
}

0 commit comments

Comments
 (0)