From 9caa54a3b7cdd5c190b7663f0789707a11605387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 14 Aug 2023 14:20:36 +0200 Subject: [PATCH 1/8] chore: jest matchers. --- package.json | 4 +- .../__tests__/to-be-on-the-screen.test.tsx | 68 +++++++++++++++ src/matchers/extend-expect.ts | 5 ++ src/matchers/index.tsx | 1 + src/matchers/to-be-on-the-screen.tsx | 65 +++++++++++++++ src/matchers/utils.tsx | 82 +++++++++++++++++++ yarn.lock | 29 +++++++ 7 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/matchers/__tests__/to-be-on-the-screen.test.tsx create mode 100644 src/matchers/extend-expect.ts create mode 100644 src/matchers/index.tsx create mode 100644 src/matchers/to-be-on-the-screen.tsx create mode 100644 src/matchers/utils.tsx diff --git a/package.json b/package.json index 99420c6c2..1d21cd3e3 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,9 @@ "typescript": "^5.0.2" }, "dependencies": { - "pretty-format": "^29.0.0" + "jest-matcher-utils": "^29.6.2", + "pretty-format": "^29.0.0", + "redent": "^3.0.0" }, "peerDependencies": { "jest": ">=28.0.0", diff --git a/src/matchers/__tests__/to-be-on-the-screen.test.tsx b/src/matchers/__tests__/to-be-on-the-screen.test.tsx new file mode 100644 index 000000000..e36c75b2f --- /dev/null +++ b/src/matchers/__tests__/to-be-on-the-screen.test.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { View, Text } from 'react-native'; +import { render, screen } from '../..'; +import '../extend-expect'; + +function ShowChildren({ show }: { show: boolean }) { + return show ? ( + + Hello + + ) : ( + + ); +} + +test('toBeOnTheScreen() on attached element', () => { + render(); + 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: + " + `); +}); + +test('toBeOnTheScreen() on detached element', () => { + render(); + const element = screen.getByTestId('text'); + + screen.update(); + 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( + + + + ); + + const child = screen.getByTestId('child'); + expect(child).toBeOnTheScreen(); + + screen.update(); + expect(child).not.toBeOnTheScreen(); +}); diff --git a/src/matchers/extend-expect.ts b/src/matchers/extend-expect.ts new file mode 100644 index 000000000..4eb7b373f --- /dev/null +++ b/src/matchers/extend-expect.ts @@ -0,0 +1,5 @@ +import { toBeOnTheScreen } from './to-be-on-the-screen'; + +expect.extend({ + toBeOnTheScreen, +}); diff --git a/src/matchers/index.tsx b/src/matchers/index.tsx new file mode 100644 index 000000000..8d1540b14 --- /dev/null +++ b/src/matchers/index.tsx @@ -0,0 +1 @@ +export { toBeOnTheScreen } from './to-be-on-the-screen'; diff --git a/src/matchers/to-be-on-the-screen.tsx b/src/matchers/to-be-on-the-screen.tsx new file mode 100644 index 000000000..7d029e127 --- /dev/null +++ b/src/matchers/to-be-on-the-screen.tsx @@ -0,0 +1,65 @@ +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 : getScreenRoot() === 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 getScreenRoot() { + 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.UNSAFE_root ?? screen.container; + } 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.' + ); + } +} diff --git a/src/matchers/utils.tsx b/src/matchers/utils.tsx new file mode 100644 index 000000000..c402ee4e2 --- /dev/null +++ b/src/matchers/utils.tsx @@ -0,0 +1,82 @@ +import { ReactTestInstance } from 'react-test-renderer'; +import { + RECEIVED_COLOR, + matcherHint, + printWithType, + printReceived, +} from 'jest-matcher-utils'; +import prettyFormat, { plugins } from 'pretty-format'; +import redent from 'redent'; + +class ReactElementTypeError extends Error { + constructor( + received: unknown, + matcherFn: jest.CustomMatcher, + context: jest.MatcherContext + ) { + super(); + + /* istanbul ignore next */ + if (Error.captureStackTrace) { + Error.captureStackTrace(this, matcherFn); + } + let withType = ''; + try { + withType = printWithType('Received', received, printReceived); + } catch (e) { + // Deliberately empty. + } + + /* istanbul ignore next */ + this.message = [ + matcherHint( + `${context.isNot ? '.not' : ''}.${matcherFn.name}`, + 'received', + '' + ), + '', + `${RECEIVED_COLOR('received')} value must be a React Element.`, + withType, + ].join('\n'); + } +} + +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: [plugins.ReactTestComponent, plugins.ReactElement], + printFunctionName: false, + printBasicPrototype: false, + highlight: true, + } + ), + 2 + ); +} + +export function checkReactElement( + element: ReactTestInstance | null | undefined, + matcherFn: jest.CustomMatcher, + context: jest.MatcherContext +): asserts element is ReactTestInstance { + if (!element) { + throw new ReactElementTypeError(element, matcherFn, context); + } + + // @ts-expect-error internal _fiber property of ReactTestInstance + if (!element._fiber && !VALID_ELEMENTS.includes(element.type.toString())) { + throw new ReactElementTypeError(element, matcherFn, context); + } +} diff --git a/yarn.lock b/yarn.lock index d0b3bedc8..86e8789dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4664,6 +4664,16 @@ jest-diff@^29.0.1, jest-diff@^29.6.1: jest-get-type "^29.4.3" pretty-format "^29.6.1" +jest-diff@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.6.2.tgz#c36001e5543e82a0805051d3ceac32e6825c1c46" + integrity sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.4.3" + jest-get-type "^29.4.3" + pretty-format "^29.6.2" + jest-docblock@^29.4.3: version "29.4.3" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.3.tgz#90505aa89514a1c7dceeac1123df79e414636ea8" @@ -4741,6 +4751,16 @@ jest-matcher-utils@^29.0.1, jest-matcher-utils@^29.6.1: jest-get-type "^29.4.3" pretty-format "^29.6.1" +jest-matcher-utils@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz#39de0be2baca7a64eacb27291f0bd834fea3a535" + integrity sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ== + dependencies: + chalk "^4.0.0" + jest-diff "^29.6.2" + jest-get-type "^29.4.3" + pretty-format "^29.6.2" + jest-message-util@^29.6.1: version "29.6.1" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.6.1.tgz#d0b21d87f117e1b9e165e24f245befd2ff34ff8d" @@ -6279,6 +6299,15 @@ pretty-format@^29.0.0, pretty-format@^29.0.3, pretty-format@^29.6.1: ansi-styles "^5.0.0" react-is "^18.0.0" +pretty-format@^29.6.2: + version "29.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.6.2.tgz#3d5829261a8a4d89d8b9769064b29c50ed486a47" + integrity sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg== + dependencies: + "@jest/schemas" "^29.6.0" + ansi-styles "^5.0.0" + react-is "^18.0.0" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" From 9bd45270f5a30f775f75eb9ef58566910a160391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 14 Aug 2023 17:26:50 +0200 Subject: [PATCH 2/8] refactor: cleanup --- .../__tests__/to-be-on-the-screen.test.tsx | 7 +++-- src/matchers/to-be-on-the-screen.tsx | 28 ++++--------------- src/matchers/utils.tsx | 17 +++++------ 3 files changed, 17 insertions(+), 35 deletions(-) diff --git a/src/matchers/__tests__/to-be-on-the-screen.test.tsx b/src/matchers/__tests__/to-be-on-the-screen.test.tsx index e36c75b2f..c11631c8b 100644 --- a/src/matchers/__tests__/to-be-on-the-screen.test.tsx +++ b/src/matchers/__tests__/to-be-on-the-screen.test.tsx @@ -21,7 +21,7 @@ test('toBeOnTheScreen() on attached element', () => { .toThrowErrorMatchingInlineSnapshot(` "expect(element).not.toBeOnTheScreen() - expected element tree not to contain element but found: + expected element tree not to contain element, but found " @@ -47,9 +47,10 @@ test('toBeOnTheScreen() on null element', () => { expect(null).not.toBeOnTheScreen(); expect(() => expect(null).toBeOnTheScreen()) .toThrowErrorMatchingInlineSnapshot(` - "expect(element).toBeOnTheScreen() + "expect(received).toBeOnTheScreen() - element could not be found in the element tree" + received value must be a host element. + Received has value: null" `); }); diff --git a/src/matchers/to-be-on-the-screen.tsx b/src/matchers/to-be-on-the-screen.tsx index 7d029e127..d7da605a0 100644 --- a/src/matchers/to-be-on-the-screen.tsx +++ b/src/matchers/to-be-on-the-screen.tsx @@ -1,20 +1,21 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils'; -import { checkReactElement, printElement } from './utils'; +import { screen } from '../screen'; +import { checkHostElement, printElement } from './utils'; export function toBeOnTheScreen( this: jest.MatcherContext, element: ReactTestInstance ) { - if (element !== null) { - checkReactElement(element, toBeOnTheScreen, this); + if (element !== null || !this.isNot) { + checkHostElement(element, toBeOnTheScreen, this); } const pass = - element === null ? false : getScreenRoot() === getRootElement(element); + element === null ? false : screen.UNSAFE_root === getRootElement(element); const errorFound = () => { - return `expected element tree not to contain element but found:\n${printElement( + return `expected element tree not to contain element, but found\n${printElement( element )}`; }; @@ -46,20 +47,3 @@ function getRootElement(element: ReactTestInstance) { } return root; } - -function getScreenRoot() { - 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.UNSAFE_root ?? screen.container; - } 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.' - ); - } -} diff --git a/src/matchers/utils.tsx b/src/matchers/utils.tsx index c402ee4e2..c6e8e96f9 100644 --- a/src/matchers/utils.tsx +++ b/src/matchers/utils.tsx @@ -7,8 +7,9 @@ import { } from 'jest-matcher-utils'; import prettyFormat, { plugins } from 'pretty-format'; import redent from 'redent'; +import { isHostElement } from '../helpers/component-tree'; -class ReactElementTypeError extends Error { +class HostElementTypeError extends Error { constructor( received: unknown, matcherFn: jest.CustomMatcher, @@ -20,6 +21,7 @@ class ReactElementTypeError extends Error { if (Error.captureStackTrace) { Error.captureStackTrace(this, matcherFn); } + let withType = ''; try { withType = printWithType('Received', received, printReceived); @@ -35,7 +37,7 @@ class ReactElementTypeError extends Error { '' ), '', - `${RECEIVED_COLOR('received')} value must be a React Element.`, + `${RECEIVED_COLOR('received')} value must be a host element.`, withType, ].join('\n'); } @@ -66,17 +68,12 @@ export function printElement(element: ReactTestInstance | null) { ); } -export function checkReactElement( +export function checkHostElement( element: ReactTestInstance | null | undefined, matcherFn: jest.CustomMatcher, context: jest.MatcherContext ): asserts element is ReactTestInstance { - if (!element) { - throw new ReactElementTypeError(element, matcherFn, context); - } - - // @ts-expect-error internal _fiber property of ReactTestInstance - if (!element._fiber && !VALID_ELEMENTS.includes(element.type.toString())) { - throw new ReactElementTypeError(element, matcherFn, context); + if (!isHostElement(element)) { + throw new HostElementTypeError(element, matcherFn, context); } } From 67b2402eaed54437803640deca34fae874273a54 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 16 Aug 2023 14:22:51 +0200 Subject: [PATCH 3/8] chore: test for not exposing matchers by default --- src/matchers/__tests__/matchers.test.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/matchers/__tests__/matchers.test.tsx diff --git a/src/matchers/__tests__/matchers.test.tsx b/src/matchers/__tests__/matchers.test.tsx new file mode 100644 index 000000000..1889926aa --- /dev/null +++ b/src/matchers/__tests__/matchers.test.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { View } from 'react-native'; + +// Note: that must point to root of the /src to reliably replicate default import. +import { render } from '../..'; + +// This is check that RNTL does not extend "expect" by default, until we actually want to expose Jest matchers publically. +test('does not extend "expect" by default', () => { + render(); + + // @ts-expect-error + expect(expect.toBeOnTheScreen).toBeUndefined(); +}); From b8120b3c2f9b1e6c0469b9b39493ef70c3694e18 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 16 Aug 2023 16:02:38 +0200 Subject: [PATCH 4/8] chore: cleanup --- src/helpers/component-tree.ts | 19 +++++++++++++++++++ ...tchers.test.tsx => extend-expect.test.tsx} | 0 src/matchers/to-be-on-the-screen.tsx | 9 +-------- 3 files changed, 20 insertions(+), 8 deletions(-) rename src/matchers/__tests__/{matchers.test.tsx => extend-expect.test.tsx} (100%) diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts index 46993e956..6c74946dc 100644 --- a/src/helpers/component-tree.ts +++ b/src/helpers/component-tree.ts @@ -92,3 +92,22 @@ export function getHostSiblings( (sibling) => !hostSelves.includes(sibling) ); } + +/** + * Returns the unsafe root element of the tree (probably composite). + * + * @param element The element start traversing from. + * @returns The root element of the tree (host or composite). + */ +export function getRootElement(element: ReactTestInstance | null) { + if (element == null) { + return null; + } + + let current = element; + while (current.parent) { + current = current.parent; + } + + return current; +} diff --git a/src/matchers/__tests__/matchers.test.tsx b/src/matchers/__tests__/extend-expect.test.tsx similarity index 100% rename from src/matchers/__tests__/matchers.test.tsx rename to src/matchers/__tests__/extend-expect.test.tsx diff --git a/src/matchers/to-be-on-the-screen.tsx b/src/matchers/to-be-on-the-screen.tsx index d7da605a0..69e71ab90 100644 --- a/src/matchers/to-be-on-the-screen.tsx +++ b/src/matchers/to-be-on-the-screen.tsx @@ -1,5 +1,6 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils'; +import { getRootElement } from '../helpers/component-tree'; import { screen } from '../screen'; import { checkHostElement, printElement } from './utils'; @@ -39,11 +40,3 @@ export function toBeOnTheScreen( }, }; } - -function getRootElement(element: ReactTestInstance) { - let root = element; - while (root.parent) { - root = root.parent; - } - return root; -} From 5cfaee577cc343f3cf260fcf86020f17fb2d99ac Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 16 Aug 2023 17:36:01 +0200 Subject: [PATCH 5/8] refactor: finishing touches --- .../__tests__/to-be-on-the-screen.test.tsx | 47 ++++++++++--------- src/matchers/__tests__/utils.test.tsx | 42 +++++++++++++++++ src/matchers/to-be-on-the-screen.tsx | 4 +- src/matchers/utils.tsx | 36 +++++++++----- 4 files changed, 93 insertions(+), 36 deletions(-) create mode 100644 src/matchers/__tests__/utils.test.tsx diff --git a/src/matchers/__tests__/to-be-on-the-screen.test.tsx b/src/matchers/__tests__/to-be-on-the-screen.test.tsx index c11631c8b..cfb6c8c8a 100644 --- a/src/matchers/__tests__/to-be-on-the-screen.test.tsx +++ b/src/matchers/__tests__/to-be-on-the-screen.test.tsx @@ -3,18 +3,23 @@ import { View, Text } from 'react-native'; import { render, screen } from '../..'; import '../extend-expect'; -function ShowChildren({ show }: { show: boolean }) { - return show ? ( +test('example test', () => { + render( - Hello + - ) : ( - ); -} + + const child = screen.getByTestId('child'); + expect(child).toBeOnTheScreen(); + + screen.update(); + expect(child).not.toBeOnTheScreen(); +}); test('toBeOnTheScreen() on attached element', () => { render(); + const element = screen.getByTestId('test'); expect(element).toBeOnTheScreen(); expect(() => expect(element).not.toBeOnTheScreen()) @@ -28,11 +33,23 @@ test('toBeOnTheScreen() on attached element', () => { `); }); +function ShowChildren({ show }: { show: boolean }) { + return show ? ( + + Hello + + ) : ( + + ); +} + test('toBeOnTheScreen() on detached element', () => { - render(); - const element = screen.getByTestId('text'); + render(); + const element = screen.getByTestId('text'); + // Next line will unmount the element, yet `element` variable will still hold reference to it. screen.update(); + expect(element).toBeTruthy(); expect(element).not.toBeOnTheScreen(); expect(() => expect(element).toBeOnTheScreen()) @@ -53,17 +70,3 @@ test('toBeOnTheScreen() on null element', () => { Received has value: null" `); }); - -test('example test', () => { - render( - - - - ); - - const child = screen.getByTestId('child'); - expect(child).toBeOnTheScreen(); - - screen.update(); - expect(child).not.toBeOnTheScreen(); -}); diff --git a/src/matchers/__tests__/utils.test.tsx b/src/matchers/__tests__/utils.test.tsx new file mode 100644 index 000000000..0e210459d --- /dev/null +++ b/src/matchers/__tests__/utils.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { View } from 'react-native'; +import { render } from '../..'; +import { formatElement, checkHostElement } from '../utils'; + +function fakeMatcher() { + // Do nothing. +} + +test('formatElement', () => { + expect(formatElement(null)).toMatchInlineSnapshot(`"null"`); +}); + +test('checkHostElement allows host element', () => { + const screen = render(); + + expect(() => { + // @ts-expect-error + checkHostElement(screen.getByTestId('view'), fakeMatcher, {}); + }).not.toThrow(); +}); + +test('checkHostElement allows rejects composite element', () => { + const screen = render(); + + expect(() => { + // @ts-expect-error + checkHostElement(screen.UNSAFE_root, fakeMatcher, {}); + }).toThrow(/value must be a host element./); +}); + +test('checkHostElement allows rejects null element', () => { + expect(() => { + // @ts-expect-error + checkHostElement(null, fakeMatcher, {}); + }).toThrowErrorMatchingInlineSnapshot(` + "expect(received).fakeMatcher() + + received value must be a host element. + Received has value: null" + `); +}); diff --git a/src/matchers/to-be-on-the-screen.tsx b/src/matchers/to-be-on-the-screen.tsx index 69e71ab90..52f880708 100644 --- a/src/matchers/to-be-on-the-screen.tsx +++ b/src/matchers/to-be-on-the-screen.tsx @@ -2,7 +2,7 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils'; import { getRootElement } from '../helpers/component-tree'; import { screen } from '../screen'; -import { checkHostElement, printElement } from './utils'; +import { checkHostElement, formatElement } from './utils'; export function toBeOnTheScreen( this: jest.MatcherContext, @@ -16,7 +16,7 @@ export function toBeOnTheScreen( element === null ? false : screen.UNSAFE_root === getRootElement(element); const errorFound = () => { - return `expected element tree not to contain element, but found\n${printElement( + return `expected element tree not to contain element, but found\n${formatElement( element )}`; }; diff --git a/src/matchers/utils.tsx b/src/matchers/utils.tsx index c6e8e96f9..51d9f7d1f 100644 --- a/src/matchers/utils.tsx +++ b/src/matchers/utils.tsx @@ -25,11 +25,11 @@ class HostElementTypeError extends Error { let withType = ''; try { withType = printWithType('Received', received, printReceived); + /* istanbul ignore next */ } catch (e) { // Deliberately empty. } - /* istanbul ignore next */ this.message = [ matcherHint( `${context.isNot ? '.not' : ''}.${matcherFn.name}`, @@ -43,7 +43,29 @@ class HostElementTypeError extends Error { } } -export function printElement(element: ReactTestInstance | null) { +/** + * Throws HostElementTypeError if passed element is not a host element. + * + * @param element ReactTestInstance to check. + * @param matcherFn Matcher function calling the check used for formatting error. + * @param context Jest matcher context used for formatting error. + */ +export function checkHostElement( + element: ReactTestInstance | null | undefined, + matcherFn: jest.CustomMatcher, + context: jest.MatcherContext +): asserts element is ReactTestInstance { + if (!isHostElement(element)) { + throw new HostElementTypeError(element, matcherFn, context); + } +} + +/*** + * Format given element as a pretty-printed string. + * + * @param element Element to format. + */ +export function formatElement(element: ReactTestInstance | null) { if (element == null) { return 'null'; } @@ -67,13 +89,3 @@ export function printElement(element: ReactTestInstance | null) { 2 ); } - -export function checkHostElement( - element: ReactTestInstance | null | undefined, - matcherFn: jest.CustomMatcher, - context: jest.MatcherContext -): asserts element is ReactTestInstance { - if (!isHostElement(element)) { - throw new HostElementTypeError(element, matcherFn, context); - } -} From c5cd4b208cebf5468fe6ee6392403e847540a610 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 16 Aug 2023 17:47:01 +0200 Subject: [PATCH 6/8] chore: improve code cov --- src/helpers/__tests__/component-tree.test.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/helpers/__tests__/component-tree.test.tsx b/src/helpers/__tests__/component-tree.test.tsx index e275a58be..b1b222ca5 100644 --- a/src/helpers/__tests__/component-tree.test.tsx +++ b/src/helpers/__tests__/component-tree.test.tsx @@ -6,6 +6,7 @@ import { getHostParent, getHostSelves, getHostSiblings, + getRootElement, } from '../component-tree'; function ZeroHostChildren() { @@ -224,3 +225,20 @@ describe('getHostSiblings()', () => { ]); }); }); + +describe('getRootElement()', () => { + it('returns UNSAFE_root for mounted view', () => { + const screen = render( + + + + ); + + const view = screen.getByTestId('view'); + expect(getRootElement(view)).toEqual(screen.UNSAFE_root); + }); + + it('returns null for null', () => { + expect(getRootElement(null)).toEqual(null); + }); +}); From 6af95f8f6b5dfb27ece98186bb669ed663192e5b Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 16 Aug 2023 17:47:38 +0200 Subject: [PATCH 7/8] Update package.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michał Pierzchała --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1d21cd3e3..f8b5fe97f 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ }, "dependencies": { "jest-matcher-utils": "^29.6.2", - "pretty-format": "^29.0.0", + "pretty-format": "^29.6.2", "redent": "^3.0.0" }, "peerDependencies": { From b8802466c17b37db50e2fba5991d35c03c00d1d8 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Wed, 16 Aug 2023 17:50:40 +0200 Subject: [PATCH 8/8] refactor: code review changes --- src/helpers/__tests__/component-tree.test.tsx | 8 ++++---- src/helpers/component-tree.ts | 2 +- src/matchers/to-be-on-the-screen.tsx | 6 ++++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/helpers/__tests__/component-tree.test.tsx b/src/helpers/__tests__/component-tree.test.tsx index b1b222ca5..4e64250bd 100644 --- a/src/helpers/__tests__/component-tree.test.tsx +++ b/src/helpers/__tests__/component-tree.test.tsx @@ -6,7 +6,7 @@ import { getHostParent, getHostSelves, getHostSiblings, - getRootElement, + getUnsafeRootElement, } from '../component-tree'; function ZeroHostChildren() { @@ -226,7 +226,7 @@ describe('getHostSiblings()', () => { }); }); -describe('getRootElement()', () => { +describe('getUnsafeRootElement()', () => { it('returns UNSAFE_root for mounted view', () => { const screen = render( @@ -235,10 +235,10 @@ describe('getRootElement()', () => { ); const view = screen.getByTestId('view'); - expect(getRootElement(view)).toEqual(screen.UNSAFE_root); + expect(getUnsafeRootElement(view)).toEqual(screen.UNSAFE_root); }); it('returns null for null', () => { - expect(getRootElement(null)).toEqual(null); + expect(getUnsafeRootElement(null)).toEqual(null); }); }); diff --git a/src/helpers/component-tree.ts b/src/helpers/component-tree.ts index 6c74946dc..6fe6fd13b 100644 --- a/src/helpers/component-tree.ts +++ b/src/helpers/component-tree.ts @@ -99,7 +99,7 @@ export function getHostSiblings( * @param element The element start traversing from. * @returns The root element of the tree (host or composite). */ -export function getRootElement(element: ReactTestInstance | null) { +export function getUnsafeRootElement(element: ReactTestInstance | null) { if (element == null) { return null; } diff --git a/src/matchers/to-be-on-the-screen.tsx b/src/matchers/to-be-on-the-screen.tsx index 52f880708..3fd1ce615 100644 --- a/src/matchers/to-be-on-the-screen.tsx +++ b/src/matchers/to-be-on-the-screen.tsx @@ -1,6 +1,6 @@ import type { ReactTestInstance } from 'react-test-renderer'; import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils'; -import { getRootElement } from '../helpers/component-tree'; +import { getUnsafeRootElement } from '../helpers/component-tree'; import { screen } from '../screen'; import { checkHostElement, formatElement } from './utils'; @@ -13,7 +13,9 @@ export function toBeOnTheScreen( } const pass = - element === null ? false : screen.UNSAFE_root === getRootElement(element); + element === null + ? false + : screen.UNSAFE_root === getUnsafeRootElement(element); const errorFound = () => { return `expected element tree not to contain element, but found\n${formatElement(