Skip to content

Commit 692c55b

Browse files
refactor: format element & debug (#1730)
1 parent d21dbb4 commit 692c55b

26 files changed

+233
-309
lines changed

.vscode/settings.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
11
{
2-
"cSpell.words": ["labelledby", "Pressable", "RNTL", "Uncapitalize", "valuenow", "valuetext"]
2+
"cSpell.words": [
3+
"labelledby",
4+
"Pressable",
5+
"redent",
6+
"RNTL",
7+
"Uncapitalize",
8+
"valuenow",
9+
"valuetext"
10+
]
311
}

src/__tests__/__snapshots__/render-debug.test.tsx.snap

+5-106
Original file line numberDiff line numberDiff line change
@@ -29,35 +29,7 @@ exports[`debug 1`] = `
2929
value=""
3030
/>
3131
<View
32-
accessibilityState={
33-
{
34-
"busy": undefined,
35-
"checked": undefined,
36-
"disabled": undefined,
37-
"expanded": undefined,
38-
"selected": undefined,
39-
}
40-
}
41-
accessibilityValue={
42-
{
43-
"max": undefined,
44-
"min": undefined,
45-
"now": undefined,
46-
"text": undefined,
47-
}
48-
}
4932
accessible={true}
50-
collapsable={false}
51-
focusable={true}
52-
onBlur={[Function onBlur]}
53-
onClick={[Function onClick]}
54-
onFocus={[Function onFocus]}
55-
onResponderGrant={[Function onResponderGrant]}
56-
onResponderMove={[Function onResponderMove]}
57-
onResponderRelease={[Function onResponderRelease]}
58-
onResponderTerminate={[Function onResponderTerminate]}
59-
onResponderTerminationRequest={[Function onResponderTerminationRequest]}
60-
onStartShouldSetResponder={[Function onStartShouldSetResponder]}
6133
role="button"
6234
>
6335
<Text>
@@ -242,54 +214,8 @@ exports[`debug with only prop whose value is bananaChef 1`] = `
242214
</View>"
243215
`;
244216

245-
exports[`debug with only props from TextInput components 1`] = `
217+
exports[`debug: All Props 1`] = `
246218
"<View>
247-
<Text>
248-
Is the banana fresh?
249-
</Text>
250-
<Text>
251-
not fresh
252-
</Text>
253-
<TextInput
254-
placeholder="Add custom freshness"
255-
testID="bananaCustomFreshness"
256-
value="Custom Freshie"
257-
/>
258-
<TextInput
259-
defaultValue="What did you inspect?"
260-
placeholder="Who inspected freshness?"
261-
testID="bananaChef"
262-
value="I inspected freshie"
263-
/>
264-
<TextInput
265-
defaultValue="What banana?"
266-
/>
267-
<TextInput
268-
defaultValue="hello"
269-
value=""
270-
/>
271-
<View>
272-
<Text>
273-
Change freshness!
274-
</Text>
275-
</View>
276-
<Text>
277-
First Text
278-
</Text>
279-
<Text>
280-
Second Text
281-
</Text>
282-
<Text>
283-
0
284-
</Text>
285-
</View>"
286-
`;
287-
288-
exports[`debug: another custom message 1`] = `
289-
"another custom message
290-
291-
292-
<View>
293219
<Text>
294220
Is the banana fresh?
295221
</Text>
@@ -365,11 +291,12 @@ exports[`debug: another custom message 1`] = `
365291
<Text>
366292
0
367293
</Text>
368-
</View>"
294+
</View>
295+
undefined"
369296
`;
370297

371-
exports[`debug: with message 1`] = `
372-
"my custom message
298+
exports[`debug: Option message 1`] = `
299+
"another custom message
373300
374301
375302
<View>
@@ -400,35 +327,7 @@ exports[`debug: with message 1`] = `
400327
value=""
401328
/>
402329
<View
403-
accessibilityState={
404-
{
405-
"busy": undefined,
406-
"checked": undefined,
407-
"disabled": undefined,
408-
"expanded": undefined,
409-
"selected": undefined,
410-
}
411-
}
412-
accessibilityValue={
413-
{
414-
"max": undefined,
415-
"min": undefined,
416-
"now": undefined,
417-
"text": undefined,
418-
}
419-
}
420330
accessible={true}
421-
collapsable={false}
422-
focusable={true}
423-
onBlur={[Function onBlur]}
424-
onClick={[Function onClick]}
425-
onFocus={[Function onFocus]}
426-
onResponderGrant={[Function onResponderGrant]}
427-
onResponderMove={[Function onResponderMove]}
428-
onResponderRelease={[Function onResponderRelease]}
429-
onResponderTerminate={[Function onResponderTerminate]}
430-
onResponderTerminationRequest={[Function onResponderTerminationRequest]}
431-
onStartShouldSetResponder={[Function onStartShouldSetResponder]}
432331
role="button"
433332
>
434333
<Text>

src/__tests__/render-debug.test.tsx

+4-21
Original file line numberDiff line numberDiff line change
@@ -93,27 +93,20 @@ test('debug', () => {
9393
render(<Banana />);
9494

9595
screen.debug();
96-
screen.debug('my custom message');
9796
screen.debug({ message: 'another custom message' });
97+
screen.debug({ mapProps: null });
9898

9999
const mockCalls = jest.mocked(logger.info).mock.calls;
100100
expect(mockCalls[0][0]).toMatchSnapshot();
101-
expect(`${mockCalls[1][0]}\n${mockCalls[1][1]}`).toMatchSnapshot('with message');
102-
expect(`${mockCalls[2][0]}\n${mockCalls[2][1]}`).toMatchSnapshot('another custom message');
103-
104-
const mockWarnCalls = jest.mocked(logger.warn).mock.calls;
105-
expect(mockWarnCalls[0]).toMatchInlineSnapshot(`
106-
[
107-
"Using debug("message") is deprecated and will be removed in future release, please use debug({ message: "message" }) instead.",
108-
]
109-
`);
101+
expect(`${mockCalls[1][0]}\n${mockCalls[1][1]}`).toMatchSnapshot('Option message');
102+
expect(`${mockCalls[2][0]}\n${mockCalls[2][1]}`).toMatchSnapshot('All Props');
110103
});
111104

112105
test('debug changing component', () => {
113106
render(<Banana />);
114107
fireEvent.press(screen.getByRole('button', { name: 'Change freshness!' }));
115108

116-
screen.debug();
109+
screen.debug({ mapProps: null });
117110

118111
const mockCalls = jest.mocked(logger.info).mock.calls;
119112
expect(mockCalls[0][0]).toMatchSnapshot('bananaFresh button message should now be "fresh"');
@@ -145,16 +138,6 @@ test('debug with only prop whose value is bananaChef', () => {
145138
expect(mockCalls[0][0]).toMatchSnapshot();
146139
});
147140

148-
test('debug with only props from TextInput components', () => {
149-
render(<Banana />);
150-
screen.debug({
151-
mapProps: (props, node) => (node.type === 'TextInput' ? props : {}),
152-
});
153-
154-
const mockCalls = jest.mocked(logger.info).mock.calls;
155-
expect(mockCalls[0][0]).toMatchSnapshot();
156-
});
157-
158141
test('debug should use debugOptions from config when no option is specified', () => {
159142
configure({ defaultDebugOptions: { mapProps: () => ({}) } });
160143

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'), { mapProps: null })).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/__tests__/format-default.test.tsx src/helpers/__tests__/map-props.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defaultMapProps } from '../format-default';
1+
import { defaultMapProps } from '../map-props';
22

33
describe('mapPropsForQueryError', () => {
44
test('preserves props that are helpful for debugging', () => {

src/helpers/debug.ts

+6-10
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
11
import type { ReactTestRendererJSON } from 'react-test-renderer';
2-
import type { FormatOptions } from './format';
3-
import format from './format';
2+
import type { FormatElementOptions } from './format-element';
3+
import { formatJson } from './format-element';
44
import { logger } from './logger';
55

66
export type DebugOptions = {
77
message?: string;
8-
} & FormatOptions;
8+
} & FormatElementOptions;
99

1010
/**
1111
* Log pretty-printed deep test component instance
1212
*/
1313
export function debug(
1414
instance: ReactTestRendererJSON | ReactTestRendererJSON[],
15-
options?: DebugOptions | string,
15+
{ message, ...formatOptions }: DebugOptions = {},
1616
) {
17-
const message = typeof options === 'string' ? options : options?.message;
18-
19-
const formatOptions = typeof options === 'object' ? { mapProps: options?.mapProps } : undefined;
20-
2117
if (message) {
22-
logger.info(`${message}\n\n`, format(instance, formatOptions));
18+
logger.info(`${message}\n\n`, formatJson(instance, formatOptions));
2319
} else {
24-
logger.info(format(instance, formatOptions));
20+
logger.info(formatJson(instance, formatOptions));
2521
}
2622
}

src/helpers/format-element.ts

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { ReactTestInstance, ReactTestRendererJSON } from 'react-test-renderer';
2+
import type { NewPlugin } from 'pretty-format';
3+
import prettyFormat, { plugins } from 'pretty-format';
4+
import type { MapPropsFunction } from './map-props';
5+
import { defaultMapProps } from './map-props';
6+
7+
export type FormatElementOptions = {
8+
/** Minimize used space. */
9+
compact?: boolean;
10+
11+
/** Highlight the output. */
12+
highlight?: boolean;
13+
14+
/** Filter or map props to display. */
15+
mapProps?: MapPropsFunction | null;
16+
};
17+
18+
/***
19+
* Format given element as a pretty-printed string.
20+
*
21+
* @param element Element to format.
22+
*/
23+
export function formatElement(
24+
element: ReactTestInstance | null,
25+
{ compact, highlight = true, mapProps = defaultMapProps }: FormatElementOptions = {},
26+
) {
27+
if (element == null) {
28+
return '(null)';
29+
}
30+
31+
const { children, ...props } = element.props;
32+
const childrenToDisplay = typeof children === 'string' ? [children] : undefined;
33+
34+
return prettyFormat(
35+
{
36+
// This prop is needed persuade the prettyFormat that the element is
37+
// a ReactTestRendererJSON instance, so it is formatted as JSX.
38+
$$typeof: Symbol.for('react.test.json'),
39+
type: `${element.type}`,
40+
props: mapProps ? mapProps(props) : props,
41+
children: childrenToDisplay,
42+
},
43+
// See: https://www.npmjs.com/package/pretty-format#usage-with-options
44+
{
45+
plugins: [plugins.ReactTestComponent, plugins.ReactElement],
46+
printFunctionName: false,
47+
printBasicPrototype: false,
48+
highlight: highlight,
49+
min: compact,
50+
},
51+
);
52+
}
53+
54+
export function formatElementList(elements: ReactTestInstance[], options?: FormatElementOptions) {
55+
if (elements.length === 0) {
56+
return '(no elements)';
57+
}
58+
59+
return elements.map((element) => formatElement(element, options)).join('\n');
60+
}
61+
62+
export function formatJson(
63+
json: ReactTestRendererJSON | ReactTestRendererJSON[],
64+
{ compact, highlight = true, mapProps = defaultMapProps }: FormatElementOptions = {},
65+
) {
66+
return prettyFormat(json, {
67+
plugins: [getElementJsonPlugin(mapProps), plugins.ReactElement],
68+
highlight: highlight,
69+
printBasicPrototype: false,
70+
min: compact,
71+
});
72+
}
73+
74+
function getElementJsonPlugin(mapProps?: MapPropsFunction | null): NewPlugin {
75+
return {
76+
test: (val) => plugins.ReactTestComponent.test(val),
77+
serialize: (val, config, indentation, depth, refs, printer) => {
78+
let newVal = val;
79+
if (mapProps && val.props) {
80+
newVal = { ...val, props: mapProps(val.props) };
81+
}
82+
return plugins.ReactTestComponent.serialize(
83+
newVal,
84+
config,
85+
indentation,
86+
depth,
87+
refs,
88+
printer,
89+
);
90+
},
91+
};
92+
}

0 commit comments

Comments
 (0)