Skip to content

refactor: format element & debug #1730

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
{
"cSpell.words": ["labelledby", "Pressable", "RNTL", "Uncapitalize", "valuenow", "valuetext"]
"cSpell.words": [
"labelledby",
"Pressable",
"redent",
"RNTL",
"Uncapitalize",
"valuenow",
"valuetext"
]
}
111 changes: 5 additions & 106 deletions src/__tests__/__snapshots__/render-debug.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,7 @@ exports[`debug 1`] = `
value=""
/>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function onBlur]}
onClick={[Function onClick]}
onFocus={[Function onFocus]}
onResponderGrant={[Function onResponderGrant]}
onResponderMove={[Function onResponderMove]}
onResponderRelease={[Function onResponderRelease]}
onResponderTerminate={[Function onResponderTerminate]}
onResponderTerminationRequest={[Function onResponderTerminationRequest]}
onStartShouldSetResponder={[Function onStartShouldSetResponder]}
role="button"
>
<Text>
Expand Down Expand Up @@ -242,54 +214,8 @@ exports[`debug with only prop whose value is bananaChef 1`] = `
</View>"
`;

exports[`debug with only props from TextInput components 1`] = `
exports[`debug: All Props 1`] = `
"<View>
<Text>
Is the banana fresh?
</Text>
<Text>
not fresh
</Text>
<TextInput
placeholder="Add custom freshness"
testID="bananaCustomFreshness"
value="Custom Freshie"
/>
<TextInput
defaultValue="What did you inspect?"
placeholder="Who inspected freshness?"
testID="bananaChef"
value="I inspected freshie"
/>
<TextInput
defaultValue="What banana?"
/>
<TextInput
defaultValue="hello"
value=""
/>
<View>
<Text>
Change freshness!
</Text>
</View>
<Text>
First Text
</Text>
<Text>
Second Text
</Text>
<Text>
0
</Text>
</View>"
`;

exports[`debug: another custom message 1`] = `
"another custom message


<View>
<Text>
Is the banana fresh?
</Text>
Expand Down Expand Up @@ -365,11 +291,12 @@ exports[`debug: another custom message 1`] = `
<Text>
0
</Text>
</View>"
</View>
undefined"
`;

exports[`debug: with message 1`] = `
"my custom message
exports[`debug: Option message 1`] = `
"another custom message


<View>
Expand Down Expand Up @@ -400,35 +327,7 @@ exports[`debug: with message 1`] = `
value=""
/>
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function onBlur]}
onClick={[Function onClick]}
onFocus={[Function onFocus]}
onResponderGrant={[Function onResponderGrant]}
onResponderMove={[Function onResponderMove]}
onResponderRelease={[Function onResponderRelease]}
onResponderTerminate={[Function onResponderTerminate]}
onResponderTerminationRequest={[Function onResponderTerminationRequest]}
onStartShouldSetResponder={[Function onStartShouldSetResponder]}
role="button"
>
<Text>
Expand Down
25 changes: 4 additions & 21 deletions src/__tests__/render-debug.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,27 +93,20 @@ test('debug', () => {
render(<Banana />);

screen.debug();
screen.debug('my custom message');
screen.debug({ message: 'another custom message' });
screen.debug({ mapProps: null });

const mockCalls = jest.mocked(logger.info).mock.calls;
expect(mockCalls[0][0]).toMatchSnapshot();
expect(`${mockCalls[1][0]}\n${mockCalls[1][1]}`).toMatchSnapshot('with message');
expect(`${mockCalls[2][0]}\n${mockCalls[2][1]}`).toMatchSnapshot('another custom message');

const mockWarnCalls = jest.mocked(logger.warn).mock.calls;
expect(mockWarnCalls[0]).toMatchInlineSnapshot(`
[
"Using debug("message") is deprecated and will be removed in future release, please use debug({ message: "message" }) instead.",
]
`);
expect(`${mockCalls[1][0]}\n${mockCalls[1][1]}`).toMatchSnapshot('Option message');
expect(`${mockCalls[2][0]}\n${mockCalls[2][1]}`).toMatchSnapshot('All Props');
});

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

screen.debug();
screen.debug({ mapProps: null });

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

test('debug with only props from TextInput components', () => {
render(<Banana />);
screen.debug({
mapProps: (props, node) => (node.type === 'TextInput' ? props : {}),
});

const mockCalls = jest.mocked(logger.info).mock.calls;
expect(mockCalls[0][0]).toMatchSnapshot();
});

test('debug should use debugOptions from config when no option is specified', () => {
configure({ defaultDebugOptions: { mapProps: () => ({}) } });

Expand Down
25 changes: 25 additions & 0 deletions src/helpers/__tests__/format-element.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from 'react';
import { Text, View } from 'react-native';
import { render, screen } from '../..';
import { formatElement } from '../format-element';

test('formatElement', () => {
render(
<View testID="root">
<View testID="view" />
<Text>Hello</Text>
</View>,
);

expect(formatElement(screen.getByTestId('view'), { mapProps: null })).toMatchInlineSnapshot(`
"<View
testID="view"
/>"
`);
expect(formatElement(screen.getByText('Hello'))).toMatchInlineSnapshot(`
"<Text>
Hello
</Text>"
`);
expect(formatElement(null)).toMatchInlineSnapshot(`"(null)"`);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defaultMapProps } from '../format-default';
import { defaultMapProps } from '../map-props';

describe('mapPropsForQueryError', () => {
test('preserves props that are helpful for debugging', () => {
Expand Down
16 changes: 6 additions & 10 deletions src/helpers/debug.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import type { ReactTestRendererJSON } from 'react-test-renderer';
import type { FormatOptions } from './format';
import format from './format';
import type { FormatElementOptions } from './format-element';
import { formatJson } from './format-element';
import { logger } from './logger';

export type DebugOptions = {
message?: string;
} & FormatOptions;
} & FormatElementOptions;

/**
* Log pretty-printed deep test component instance
*/
export function debug(
instance: ReactTestRendererJSON | ReactTestRendererJSON[],
options?: DebugOptions | string,
{ message, ...formatOptions }: DebugOptions = {},
) {
const message = typeof options === 'string' ? options : options?.message;

const formatOptions = typeof options === 'object' ? { mapProps: options?.mapProps } : undefined;

if (message) {
logger.info(`${message}\n\n`, format(instance, formatOptions));
logger.info(`${message}\n\n`, formatJson(instance, formatOptions));
} else {
logger.info(format(instance, formatOptions));
logger.info(formatJson(instance, formatOptions));
}
}
92 changes: 92 additions & 0 deletions src/helpers/format-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { ReactTestInstance, ReactTestRendererJSON } from 'react-test-renderer';
import type { NewPlugin } from 'pretty-format';
import prettyFormat, { plugins } from 'pretty-format';
import type { MapPropsFunction } from './map-props';
import { defaultMapProps } from './map-props';

export type FormatElementOptions = {
/** Minimize used space. */
compact?: boolean;

/** Highlight the output. */
highlight?: boolean;

/** Filter or map props to display. */
mapProps?: MapPropsFunction | null;
};

/***
* Format given element as a pretty-printed string.
*
* @param element Element to format.
*/
export function formatElement(
element: ReactTestInstance | null,
{ compact, highlight = true, mapProps = defaultMapProps }: FormatElementOptions = {},
) {
if (element == null) {
return '(null)';
}

const { children, ...props } = element.props;
const childrenToDisplay = typeof children === 'string' ? [children] : undefined;

return 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: mapProps ? mapProps(props) : props,
children: childrenToDisplay,
},
// See: https://www.npmjs.com/package/pretty-format#usage-with-options
{
plugins: [plugins.ReactTestComponent, plugins.ReactElement],
printFunctionName: false,
printBasicPrototype: false,
highlight: highlight,
min: compact,
},
);
}

export function formatElementList(elements: ReactTestInstance[], options?: FormatElementOptions) {
if (elements.length === 0) {
return '(no elements)';
}

return elements.map((element) => formatElement(element, options)).join('\n');
}

export function formatJson(
json: ReactTestRendererJSON | ReactTestRendererJSON[],
{ compact, highlight = true, mapProps = defaultMapProps }: FormatElementOptions = {},
) {
return prettyFormat(json, {
plugins: [getElementJsonPlugin(mapProps), plugins.ReactElement],
highlight: highlight,
printBasicPrototype: false,
min: compact,
});
}

function getElementJsonPlugin(mapProps?: MapPropsFunction | null): NewPlugin {
return {
test: (val) => plugins.ReactTestComponent.test(val),
serialize: (val, config, indentation, depth, refs, printer) => {
let newVal = val;
if (mapProps && val.props) {
newVal = { ...val, props: mapProps(val.props) };
}
return plugins.ReactTestComponent.serialize(
newVal,
config,
indentation,
depth,
refs,
printer,
);
},
};
}
Loading
Loading