Skip to content

Commit

Permalink
refactor: format element & debug (#1730)
Browse files Browse the repository at this point in the history
  • Loading branch information
mdjastrzebski authored Jan 13, 2025
1 parent d21dbb4 commit 692c55b
Show file tree
Hide file tree
Showing 26 changed files with 233 additions and 309 deletions.
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

0 comments on commit 692c55b

Please sign in to comment.