Skip to content

Commit

Permalink
feat: toContainElement matcher (#1495)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
siepra and mdjastrzebski authored Sep 19, 2023
1 parent d36137b commit d898a9d
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 3 deletions.
141 changes: 141 additions & 0 deletions src/matchers/__tests__/to-contain-element.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as React from 'react';
import { View } from 'react-native';
import { render, screen } from '../..';
import '../extend-expect';

test('toContainElement() supports basic case', () => {
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(container).not.toContainElement(element)
<View
testID="parent"
/>
contains:
<View
testID="child"
/>
"
`);
});

test('toContainElement() supports negative case', () => {
render(
<>
<View testID="view1" />
<View testID="view2" />
</>
);

const view1 = screen.getByTestId('view1');
const view2 = screen.getByTestId('view2');

expect(view1).not.toContainElement(view2);
expect(view2).not.toContainElement(view1);

expect(() => expect(view1).toContainElement(view2))
.toThrowErrorMatchingInlineSnapshot(`
"expect(container).toContainElement(element)
<View
testID="view1"
/>
does not contain:
<View
testID="view2"
/>
"
`);
});

test('toContainElement() handles null container', () => {
render(<View testID="view" />);

const view = screen.getByTestId('view');

expect(() => expect(null).toContainElement(view))
.toThrowErrorMatchingInlineSnapshot(`
"expect(received).toContainElement()
received value must be a host element.
Received has value: null"
`);
});

test('toContainElement() handles null element', () => {
render(<View testID="view" />);

const view = screen.getByTestId('view');

expect(view).not.toContainElement(null);

expect(() => expect(view).toContainElement(null))
.toThrowErrorMatchingInlineSnapshot(`
"expect(container).toContainElement(element)
<View
testID="view"
/>
does not contain:
null
"
`);
});

test('toContainElement() handles non-element container', () => {
render(<View testID="view" />);

const view = screen.getByTestId('view');

expect(() => expect({ name: 'non-element' }).not.toContainElement(view))
.toThrowErrorMatchingInlineSnapshot(`
"expect(received).not.toContainElement()
received value must be a host element.
Received has type: object
Received has value: {"name": "non-element"}"
`);

expect(() => expect(true).not.toContainElement(view))
.toThrowErrorMatchingInlineSnapshot(`
"expect(received).not.toContainElement()
received value must be a host element.
Received has type: boolean
Received has value: true"
`);
});

test('toContainElement() handles non-element element', () => {
render(<View testID="view" />);

const view = screen.getByTestId('view');

expect(() =>
// @ts-expect-error
expect(view).not.toContainElement({ name: 'non-element' })
).toThrowErrorMatchingInlineSnapshot(`
"expect(received).not.toContainElement()
received value must be a host element.
Received has type: object
Received has value: {"name": "non-element"}"
`);
});
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 type { 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
7 changes: 4 additions & 3 deletions src/matchers/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
export { toBeOnTheScreen } from './to-be-on-the-screen';
export { toBeBusy } from './to-be-busy';
export { toBeChecked } from './to-be-checked';
export { toBeDisabled, toBeEnabled } from './to-be-disabled';
export { toBeBusy } from './to-be-busy';
export { toBeEmptyElement } from './to-be-empty-element';
export { toBeOnTheScreen } from './to-be-on-the-screen';
export { toBePartiallyChecked } from './to-be-partially-checked';
export { toBeSelected } from './to-be-selected';
export { toBeVisible } from './to-be-visible';
export { toContainElement } from './to-contain-element';
export { toHaveDisplayValue } from './to-have-display-value';
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';
38 changes: 38 additions & 0 deletions src/matchers/to-contain-element.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ReactTestInstance } from 'react-test-renderer';
import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
import { checkHostElement, formatElement } from './utils';

export function toContainElement(
this: jest.MatcherContext,
container: ReactTestInstance,
element: ReactTestInstance | null
) {
checkHostElement(container, toContainElement, this);

if (element !== null) {
checkHostElement(element, toContainElement, this);
}

let matches: ReactTestInstance[] = [];
if (element) {
matches = container.findAll((node) => node === element);
}

return {
pass: matches.length > 0,
message: () => {
return [
matcherHint(
`${this.isNot ? '.not' : ''}.toContainElement`,
'container',
'element'
),
'',
RECEIVED_COLOR(`${formatElement(container)} ${
this.isNot ? '\n\ncontains:\n\n' : '\n\ndoes not contain:\n\n'
} ${formatElement(element)}
`),
].join('\n');
},
};
}

0 comments on commit d898a9d

Please sign in to comment.