Skip to content

Commit

Permalink
feat: toBeOnTheScreen() matcher (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
mdjastrzebski authored Dec 29, 2022
1 parent c3dc5ce commit a97ed16
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 10 deletions.
59 changes: 49 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
- [`toBeEnabled`](#tobeenabled)
- [`toBeEmptyElement`](#tobeemptyelement)
- [`toContainElement`](#tocontainelement)
- [`toBeOnTheScreen`](#tobeonthescreen)
- [`toHaveProp`](#tohaveprop)
- [`toHaveTextContent`](#tohavetextcontent)
- [`toHaveStyle`](#tohavestyle)
Expand Down Expand Up @@ -72,15 +73,23 @@ These will make your tests more declarative, clear to read and to maintain.

These matchers should, for the most part, be agnostic enough to work with any React Native testing
utilities, but they are primarily intended to be used with
[RNTL](https://github.com/callstack/react-native-testing-library). Any issues raised with existing
matchers or any newly proposed matchers must be viewed through compatibility with that library and
its guiding principles first.
[React Native Testing Library](https://github.com/callstack/react-native-testing-library). Any
issues raised with existing matchers or any newly proposed matchers must be viewed through
compatibility with that library and its guiding principles first.

## Installation

This module should be installed as one of your project's `devDependencies`:

#### Using `yarn`

```sh
yarn add --dev @testing-library/jest-native
```

#### Using `npm`

```sh
npm install --save-dev @testing-library/jest-native
```

Expand Down Expand Up @@ -108,8 +117,10 @@ expect.extend({ toBeEmptyElement, toHaveTextContent });

## Matchers

`jest-native` has only been tested to work with `RNTL`. Keep in mind that these queries will only
work on UI elements that bridge to native.
`jest-native` has only been tested to work with
[React Native Testing Library](https://github.com/callstack/react-native-testing-library). Keep in
mind that these queries are intended only to work with elements corresponding to
[host components](https://reactnative.dev/architecture/glossary#react-host-components-or-host-components).

### `toBeDisabled`

Expand All @@ -120,6 +131,7 @@ toBeDisabled();
Check whether or not an element is disabled from a user perspective.

This matcher will check if the element or its parent has any of the following props :

- `disabled`
- `accessibilityState={{ disabled: true }}`
- `editable={false}` (for `TextInput` only)
Expand Down Expand Up @@ -183,11 +195,9 @@ expect(getByTestId('empty')).toBeEmptyElement();

---

**NOTE**

`toBeEmptyElement()` matcher has been renamed from `toBeEmpty()` because of the naming conflict with
Jest Extended export with the
[same name](https://github.com/jest-community/jest-extended#tobeempty).
> **Note**<br/> This matcher has been previously named `toBeEmpty()`, but we changed that name in
> order to avoid conflict with Jest Extendend matcher with the
> [same name](https://github.com/jest-community/jest-extended#tobeempty).
---

Expand Down Expand Up @@ -224,6 +234,35 @@ expect(parent).toContainElement(child);
expect(parent).not.toContainElement(grandparent);
```

### `toBeOnTheScreen`

```ts
toBeOnTheScreen();
```

Check that the element is present in the element tree.

You can check that an already captured element has not been removed from the element tree.

> **Note**<br/> This matcher requires React Native Testing Library v10.1 or later, as it includes
> the `screen` object.
#### Examples

```tsx
render(
<View>
<View testID="child" />
</View>,
);

const child = screen.getByTestId('child');
expect(child).toBeOnTheScreen();

screen.update(<View />);
expect(child).not.toBeOnTheScreen();
```

### `toHaveProp`

```typescript
Expand Down
1 change: 1 addition & 0 deletions extend-expect.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface JestNativeMatchers<R> {
toBeDisabled(): R;
toBeEmptyElement(): R;
toBeEnabled(): R;
toBeOnTheScreen(): R;
toBeVisible(): R;
toContainElement(element: ReactTestInstance | null): R;
toHaveTextContent(text: string | RegExp, options?: { normalizeWhitespace: boolean }): R;
Expand Down
19 changes: 19 additions & 0 deletions src/__tests__/to-be-on-the-screen-import.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from 'react';
import { View } from 'react-native';
import { render } from '@testing-library/react-native';

jest.mock('@testing-library/react-native', () => ({
...jest.requireActual('@testing-library/react-native'),
screen: undefined,
}));

test('toBeOnTheScreen() on null element', () => {
const screen = render(<View testID="test" />);

const test = screen.getByTestId('test');
expect(() => expect(test).toBeOnTheScreen()).toThrowErrorMatchingInlineSnapshot(`
"Could not import \`screen\` object from @testing-library/react-native.
Using toBeOnTheScreen() matcher requires @testing-library/react-native v10.1.0 or later to be added to your devDependencies."
`);
});
64 changes: 64 additions & 0 deletions src/__tests__/to-be-on-the-screen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from 'react';
import { View, Text } from 'react-native';
import { render, screen } from '@testing-library/react-native';

function ShowChildren({ show }: { show: boolean }) {
return show ? (
<View>
<Text testID="text">Hello</Text>
</View>
) : (
<View />
);
}

test('toBeOnTheScreen() on attached element', () => {
render(<View testID="test" />);
const element = screen.getByTestId('test');
expect(element).toBeOnTheScreen();
expect(() => expect(element).not.toBeOnTheScreen()).toThrowErrorMatchingInlineSnapshot(`
"expect(element).not.toBeOnTheScreen()
expected element tree not to contain element but found:
<View
testID="test"
/>"
`);
});

test('toBeOnTheScreen() on detached element', () => {
render(<ShowChildren show />);
const element = screen.getByTestId('text');

screen.update(<ShowChildren show={false} />);
expect(element).toBeTruthy();
expect(element).not.toBeOnTheScreen();
expect(() => expect(element).toBeOnTheScreen()).toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeOnTheScreen()
element could not be found in the element tree"
`);
});

test('toBeOnTheScreen() on null element', () => {
expect(null).not.toBeOnTheScreen();
expect(() => expect(null).toBeOnTheScreen()).toThrowErrorMatchingInlineSnapshot(`
"expect(element).toBeOnTheScreen()
element could not be found in the element tree"
`);
});

test('example test', () => {
render(
<View>
<View testID="child" />
</View>,
);

const child = screen.getByTestId('child');
expect(child).toBeOnTheScreen();

screen.update(<View />);
expect(child).not.toBeOnTheScreen();
});
1 change: 1 addition & 0 deletions src/__types__/jest-explicit-extend.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { expect as jestExpect } from '@jest/globals';
jestExpect(null).toBeDisabled();
jestExpect(null).toBeEmptyElement();
jestExpect(null).toBeEnabled();
jestExpect(null).toBeOnTheScreen();
jestExpect(null).toBeVisible();
jestExpect(null).toContainElement(null);
jestExpect(null).toHaveTextContent('');
Expand Down
1 change: 1 addition & 0 deletions src/__types__/jest-implicit-extend.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
expect(null).toBeDisabled();
expect(null).toBeEmptyElement();
expect(null).toBeEnabled();
expect(null).toBeOnTheScreen();
expect(null).toBeVisible();
expect(null).toContainElement(null);
expect(null).toHaveTextContent('');
Expand Down
2 changes: 2 additions & 0 deletions src/extend-expect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { toBeDisabled, toBeEnabled } from './to-be-disabled';
import { toBeEmptyElement, toBeEmpty } from './to-be-empty-element';
import { toBeOnTheScreen } from './to-be-on-the-screen';
import { toContainElement } from './to-contain-element';
import { toHaveProp } from './to-have-prop';
import { toHaveStyle } from './to-have-style';
Expand All @@ -13,6 +14,7 @@ expect.extend({
toBeEnabled,
toBeEmptyElement,
toBeEmpty, // Deprecated
toBeOnTheScreen,
toContainElement,
toHaveProp,
toHaveStyle,
Expand Down
55 changes: 55 additions & 0 deletions src/to-be-on-the-screen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { ReactTestInstance } from 'react-test-renderer';
import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
import { checkReactElement, printElement } from './utils';

export function toBeOnTheScreen(this: jest.MatcherContext, element: ReactTestInstance) {
if (element !== null) {
checkReactElement(element, toBeOnTheScreen, this);
}

const pass = element === null ? false : getScreen().container === getRootElement(element);

const errorFound = () => {
return `expected element tree not to contain element but found:\n${printElement(element)}`;
};

const errorNotFound = () => {
return `element could not be found in the element tree`;
};

return {
pass,
message: () => {
return [
matcherHint(`${this.isNot ? '.not' : ''}.toBeOnTheScreen`, 'element', ''),
'',
RECEIVED_COLOR(this.isNot ? errorFound() : errorNotFound()),
].join('\n');
},
};
}

function getRootElement(element: ReactTestInstance) {
let root = element;
while (root.parent) {
root = root.parent;
}
return root;
}

function getScreen() {
try {
// eslint-disable-next-line import/no-extraneous-dependencies
const { screen } = require('@testing-library/react-native');
if (!screen) {
throw new Error('screen is undefined');
}

return screen;
} catch (error) {
throw new Error(
'Could not import `screen` object from @testing-library/react-native.\n\n' +
'Using toBeOnTheScreen() matcher requires @testing-library/react-native v10.1.0 or later to be added to your devDependencies.',
);
}
}

0 comments on commit a97ed16

Please sign in to comment.