Skip to content

Commit 28ee297

Browse files
committed
extract formatElement
1 parent d14badf commit 28ee297

14 files changed

+160
-32
lines changed

src/fire-event.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import type {
88
import type { ReactTestInstance } from 'react-test-renderer';
99
import act from './act';
1010
import { isElementMounted, isHostElement } from './helpers/component-tree';
11+
import { formatElement } from './helpers/format-element';
1112
import { isHostScrollView, isHostTextInput } from './helpers/host-component-names';
1213
import { logger } from './helpers/logger';
1314
import { isPointerEventEnabled } from './helpers/pointer-events';
1415
import { isEditableTextInput } from './helpers/text-input';
15-
import { formatElement } from './matchers/utils';
1616
import { nativeState } from './native-state';
1717
import type { Point, StringWithAutocomplete } from './types';
1818

@@ -87,7 +87,9 @@ function findEventHandler(
8787
return handler;
8888
} else {
8989
logger.warn(
90-
`${formatElement(element, { minimal: true })}: "${eventName}" event is not enabled.`,
90+
`FireEvent(${eventName}): event handler is disabled on ${formatElement(element, {
91+
minimal: true,
92+
})}`,
9193
);
9294
}
9395
}
@@ -140,9 +142,9 @@ function fireEvent(element: ReactTestInstance, eventName: EventName, ...data: un
140142
const handler = findEventHandler(element, eventName);
141143
if (!handler) {
142144
logger.warn(
143-
`${formatElement(element, {
145+
`FireEvent(${eventName}): no event handler found on ${formatElement(element, {
144146
minimal: true,
145-
})}: no "${eventName}" event handler found on element or any of it's ancestors`,
147+
})}`,
146148
);
147149
return;
148150
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as React from 'react';
2+
import { Text, View } from 'react-native';
3+
import { render, screen } from '../..';
4+
import { formatElement } from '../format-element';
5+
6+
test('formatElement', () => {
7+
render(
8+
<View testID="root">
9+
<View testID="view" />
10+
<Text>Hello</Text>
11+
</View>,
12+
);
13+
14+
expect(formatElement(screen.getByTestId('view'))).toMatchInlineSnapshot(`
15+
"<View
16+
testID="view"
17+
/>"
18+
`);
19+
expect(formatElement(screen.getByText('Hello'))).toMatchInlineSnapshot(`
20+
"<Text>
21+
Hello
22+
</Text>"
23+
`);
24+
expect(formatElement(null)).toMatchInlineSnapshot(`"null"`);
25+
});

src/helpers/format-element.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { ElementType } from 'react';
2+
import { ReactTestInstance } from 'react-test-renderer';
3+
import prettyFormat, { plugins } from 'pretty-format';
4+
import redent from 'redent';
5+
import { defaultMapProps } from './format-default';
6+
7+
export type FormatElementOptions = {
8+
// Minimize used space.
9+
minimal?: boolean;
10+
};
11+
12+
/***
13+
* Format given element as a pretty-printed string.
14+
*
15+
* @param element Element to format.
16+
*/
17+
export function formatElement(
18+
element: ReactTestInstance | null,
19+
{ minimal = false }: FormatElementOptions = {},
20+
) {
21+
if (element == null) {
22+
return 'null';
23+
}
24+
25+
const { children, ...props } = element.props;
26+
const childrenToDisplay = typeof children === 'string' ? [children] : undefined;
27+
28+
return prettyFormat(
29+
{
30+
// This prop is needed persuade the prettyFormat that the element is
31+
// a ReactTestRendererJSON instance, so it is formatted as JSX.
32+
$$typeof: Symbol.for('react.test.json'),
33+
type: formatElementType(element.type),
34+
props: defaultMapProps(props),
35+
children: childrenToDisplay,
36+
},
37+
// See: https://www.npmjs.com/package/pretty-format#usage-with-options
38+
{
39+
plugins: [plugins.ReactTestComponent, plugins.ReactElement],
40+
printFunctionName: false,
41+
printBasicPrototype: false,
42+
highlight: true,
43+
min: minimal,
44+
},
45+
);
46+
}
47+
48+
export function formatElementType(type: ElementType): string {
49+
if (typeof type === 'function') {
50+
return type.displayName ?? type.name;
51+
}
52+
53+
// if (typeof type === 'object') {
54+
// console.log('OBJECT', type);
55+
// }
56+
57+
if (typeof type === 'object' && 'type' in type) {
58+
// @ts-expect-error
59+
const nestedType = formatElementType(type.type);
60+
if (nestedType) {
61+
return nestedType;
62+
}
63+
}
64+
65+
if (typeof type === 'object' && 'render' in type) {
66+
// @ts-expect-error
67+
const nestedType = formatElementType(type.render);
68+
if (nestedType) {
69+
return nestedType;
70+
}
71+
}
72+
73+
return `${type}`;
74+
}
75+
76+
export function formatElementList(elements: ReactTestInstance[], options?: FormatElementOptions) {
77+
if (elements.length === 0) {
78+
return '(no elements)';
79+
}
80+
81+
return elements.map((element) => formatElement(element, options)).join('\n');
82+
}

src/matchers/__tests__/utils.test.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import React from 'react';
22
import { View } from 'react-native';
33
import { render, screen } from '../..';
4-
import { checkHostElement, formatElement } from '../utils';
4+
import { checkHostElement } from '../utils';
55

66
function fakeMatcher() {
77
return { pass: true, message: () => 'fake' };
88
}
99

10-
test('formatElement', () => {
11-
expect(formatElement(null)).toMatchInlineSnapshot(`" null"`);
12-
});
13-
1410
test('checkHostElement allows host element', () => {
1511
render(<View testID="view" />);
1612

src/matchers/to-be-busy.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3+
import redent from 'redent';
34
import { computeAriaBusy } from '../helpers/accessibility';
4-
import { checkHostElement, formatElement } from './utils';
5+
import { formatElement } from '../helpers/format-element';
6+
import { checkHostElement } from './utils';
57

68
export function toBeBusy(this: jest.MatcherContext, element: ReactTestInstance) {
79
checkHostElement(element, toBeBusy, this);
@@ -14,7 +16,7 @@ export function toBeBusy(this: jest.MatcherContext, element: ReactTestInstance)
1416
matcher,
1517
'',
1618
`Received element is ${this.isNot ? '' : 'not '}busy:`,
17-
formatElement(element),
19+
redent(formatElement(element), 2),
1820
].join('\n');
1921
},
2022
};

src/matchers/to-be-checked.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3+
import redent from 'redent';
34
import {
45
computeAriaChecked,
56
getRole,
67
isAccessibilityElement,
78
rolesSupportingCheckedState,
89
} from '../helpers/accessibility';
910
import { ErrorWithStack } from '../helpers/errors';
11+
import { formatElement } from '../helpers/format-element';
1012
import { isHostSwitch } from '../helpers/host-component-names';
11-
import { checkHostElement, formatElement } from './utils';
13+
import { checkHostElement } from './utils';
1214

1315
export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstance) {
1416
checkHostElement(element, toBeChecked, this);
@@ -28,7 +30,7 @@ export function toBeChecked(this: jest.MatcherContext, element: ReactTestInstanc
2830
matcherHint(`${this.isNot ? '.not' : ''}.toBeChecked`, 'element', ''),
2931
'',
3032
`Received element ${is} checked:`,
31-
formatElement(element),
33+
redent(formatElement(element), 2),
3234
].join('\n');
3335
},
3436
};

src/matchers/to-be-disabled.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3+
import redent from 'redent';
34
import { computeAriaDisabled } from '../helpers/accessibility';
45
import { getHostParent } from '../helpers/component-tree';
5-
import { checkHostElement, formatElement } from './utils';
6+
import { formatElement } from '../helpers/format-element';
7+
import { checkHostElement } from './utils';
68

79
export function toBeDisabled(this: jest.MatcherContext, element: ReactTestInstance) {
810
checkHostElement(element, toBeDisabled, this);
@@ -17,7 +19,7 @@ export function toBeDisabled(this: jest.MatcherContext, element: ReactTestInstan
1719
matcherHint(`${this.isNot ? '.not' : ''}.toBeDisabled`, 'element', ''),
1820
'',
1921
`Received element ${is} disabled:`,
20-
formatElement(element),
22+
redent(formatElement(element), 2),
2123
].join('\n');
2224
},
2325
};
@@ -36,7 +38,7 @@ export function toBeEnabled(this: jest.MatcherContext, element: ReactTestInstanc
3638
matcherHint(`${this.isNot ? '.not' : ''}.toBeEnabled`, 'element', ''),
3739
'',
3840
`Received element ${is} enabled:`,
39-
formatElement(element),
41+
redent(formatElement(element), 2),
4042
].join('\n');
4143
},
4244
};

src/matchers/to-be-empty-element.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
3+
import redent from 'redent';
34
import { getHostChildren } from '../helpers/component-tree';
4-
import { checkHostElement, formatElementArray } from './utils';
5+
import { formatElementList } from '../helpers/format-element';
6+
import { checkHostElement } from './utils';
57

68
export function toBeEmptyElement(this: jest.MatcherContext, element: ReactTestInstance) {
79
checkHostElement(element, toBeEmptyElement, this);
@@ -15,7 +17,7 @@ export function toBeEmptyElement(this: jest.MatcherContext, element: ReactTestIn
1517
matcherHint(`${this.isNot ? '.not' : ''}.toBeEmptyElement`, 'element', ''),
1618
'',
1719
'Received:',
18-
`${RECEIVED_COLOR(formatElementArray(hostChildren))}`,
20+
`${RECEIVED_COLOR(redent(formatElementList(hostChildren), 2))}`,
1921
].join('\n');
2022
},
2123
};

src/matchers/to-be-expanded.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3+
import redent from 'redent';
34
import { computeAriaExpanded } from '../helpers/accessibility';
4-
import { checkHostElement, formatElement } from './utils';
5+
import { formatElement } from '../helpers/format-element';
6+
import { checkHostElement } from './utils';
57

68
export function toBeExpanded(this: jest.MatcherContext, element: ReactTestInstance) {
79
checkHostElement(element, toBeExpanded, this);
@@ -14,7 +16,7 @@ export function toBeExpanded(this: jest.MatcherContext, element: ReactTestInstan
1416
matcher,
1517
'',
1618
`Received element is ${this.isNot ? '' : 'not '}expanded:`,
17-
formatElement(element),
19+
redent(formatElement(element), 2),
1820
].join('\n');
1921
},
2022
};
@@ -31,7 +33,7 @@ export function toBeCollapsed(this: jest.MatcherContext, element: ReactTestInsta
3133
matcher,
3234
'',
3335
`Received element is ${this.isNot ? '' : 'not '}collapsed:`,
34-
formatElement(element),
36+
redent(formatElement(element), 2),
3537
].join('\n');
3638
},
3739
};

src/matchers/to-be-on-the-screen.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
3+
import redent from 'redent';
34
import { getUnsafeRootElement } from '../helpers/component-tree';
5+
import { formatElement } from '../helpers/format-element';
46
import { screen } from '../screen';
5-
import { checkHostElement, formatElement } from './utils';
7+
import { checkHostElement } from './utils';
68

79
export function toBeOnTheScreen(this: jest.MatcherContext, element: ReactTestInstance) {
810
if (element !== null || !this.isNot) {
@@ -12,7 +14,10 @@ export function toBeOnTheScreen(this: jest.MatcherContext, element: ReactTestIns
1214
const pass = element === null ? false : screen.UNSAFE_root === getUnsafeRootElement(element);
1315

1416
const errorFound = () => {
15-
return `expected element tree not to contain element, but found\n${formatElement(element)}`;
17+
return `expected element tree not to contain element, but found\n${redent(
18+
formatElement(element),
19+
2,
20+
)}`;
1621
};
1722

1823
const errorNotFound = () => {

src/matchers/to-be-partially-checked.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3+
import redent from 'redent';
34
import { computeAriaChecked, getRole, isAccessibilityElement } from '../helpers/accessibility';
45
import { ErrorWithStack } from '../helpers/errors';
5-
import { checkHostElement, formatElement } from './utils';
6+
import { formatElement } from '../helpers/format-element';
7+
import { checkHostElement } from './utils';
68

79
export function toBePartiallyChecked(this: jest.MatcherContext, element: ReactTestInstance) {
810
checkHostElement(element, toBePartiallyChecked, this);
@@ -22,7 +24,7 @@ export function toBePartiallyChecked(this: jest.MatcherContext, element: ReactTe
2224
matcherHint(`${this.isNot ? '.not' : ''}.toBePartiallyChecked`, 'element', ''),
2325
'',
2426
`Received element ${is} partially checked:`,
25-
formatElement(element),
27+
redent(formatElement(element), 2),
2628
].join('\n');
2729
},
2830
};

src/matchers/to-be-selected.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint } from 'jest-matcher-utils';
3+
import redent from 'redent';
34
import { computeAriaSelected } from '../helpers/accessibility';
4-
import { checkHostElement, formatElement } from './utils';
5+
import { formatElement } from '../helpers/format-element';
6+
import { checkHostElement } from './utils';
57

68
export function toBeSelected(this: jest.MatcherContext, element: ReactTestInstance) {
79
checkHostElement(element, toBeSelected, this);
@@ -14,7 +16,7 @@ export function toBeSelected(this: jest.MatcherContext, element: ReactTestInstan
1416
matcherHint(`${this.isNot ? '.not' : ''}.toBeSelected`, 'element', ''),
1517
'',
1618
`Received element ${is} selected`,
17-
formatElement(element),
19+
redent(formatElement(element), 2),
1820
].join('\n');
1921
},
2022
};

src/matchers/to-be-visible.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { StyleSheet } from 'react-native';
22
import type { ReactTestInstance } from 'react-test-renderer';
33
import { matcherHint } from 'jest-matcher-utils';
4+
import redent from 'redent';
45
import { isHiddenFromAccessibility } from '../helpers/accessibility';
56
import { getHostParent } from '../helpers/component-tree';
7+
import { formatElement } from '../helpers/format-element';
68
import { isHostModal } from '../helpers/host-component-names';
7-
import { checkHostElement, formatElement } from './utils';
9+
import { checkHostElement } from './utils';
810

911
export function toBeVisible(this: jest.MatcherContext, element: ReactTestInstance) {
1012
if (element !== null || !this.isNot) {
@@ -19,7 +21,7 @@ export function toBeVisible(this: jest.MatcherContext, element: ReactTestInstanc
1921
matcherHint(`${this.isNot ? '.not' : ''}.toBeVisible`, 'element', ''),
2022
'',
2123
`Received element ${is} visible:`,
22-
formatElement(element),
24+
redent(formatElement(element), 2),
2325
].join('\n');
2426
},
2527
};

src/matchers/to-contain-element.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { ReactTestInstance } from 'react-test-renderer';
22
import { matcherHint, RECEIVED_COLOR } from 'jest-matcher-utils';
3-
import { checkHostElement, formatElement } from './utils';
3+
import redent from 'redent';
4+
import { formatElement } from '../helpers/format-element';
5+
import { checkHostElement } from './utils';
46

57
export function toContainElement(
68
this: jest.MatcherContext,
@@ -24,9 +26,9 @@ export function toContainElement(
2426
return [
2527
matcherHint(`${this.isNot ? '.not' : ''}.toContainElement`, 'container', 'element'),
2628
'',
27-
RECEIVED_COLOR(`${formatElement(container)} ${
29+
RECEIVED_COLOR(`${redent(formatElement(container), 2)} ${
2830
this.isNot ? '\n\ncontains:\n\n' : '\n\ndoes not contain:\n\n'
29-
} ${formatElement(element)}
31+
} ${redent(formatElement(element), 2)}
3032
`),
3133
].join('\n');
3234
},

0 commit comments

Comments
 (0)