feat: add debug to render helpers for easier console debugging (#65)

* feat: add debug to render helpers for easier console debugging

* bring back toJSON to docs

* revert unnecessary change

* update TS

* remove cyclic dependency of debug <-> render
This commit is contained in:
Michał Pierzchała
2018-11-19 13:51:38 +01:00
committed by Kacper Wiszczuk
parent fa198c4037
commit 909916e84c
11 changed files with 286 additions and 29 deletions
+27
View File
@@ -147,6 +147,33 @@ Unmount the in-memory tree, triggering the appropriate lifecycle events
When using React context providers, like Redux Provider, you'll likely want to wrap rendered component with them. In such cases it's convenient to create your custom `render` method. [Follow this great guide on how to set this up](https://github.com/kentcdodds/react-testing-library#custom-render).
### `debug: (message?: string) => void`
Prints deeply rendered component passed to `render` with optional message on top. Uses [debug.deep](#debug) under the hood, but it's easier to use.
```jsx
const { debug } = render(<Component />);
debug('optional message');
```
logs optional message and colored JSX:
```jsx
optional message
<TouchableOpacity
activeOpacity={0.2}
onPress={[Function bound fn]}
>
<Text>Press me</Text>
</TouchableOpacity>
```
### `debug.shallow: (message?: string) => void`
Prints shallowly rendered component passed to `render` with optional message on top. Uses [debug.shallow](#debug) under the hood, but it's easier to use.
### `toJSON: () => ?ReactTestRendererJSON`
Get the rendered component JSON representation, e.g. for snapshot testing.
@@ -1,5 +1,143 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`debug 1`] = `
"<View>
<Text>
Is the banana fresh?
</Text>
<Text
testID=\\"bananaFresh\\"
>
not fresh
</Text>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function bound touchableHandleResponderGrant]}
onResponderMove={[Function bound touchableHandleResponderMove]}
onResponderRelease={[Function bound touchableHandleResponderRelease]}
onResponderTerminate={[Function bound touchableHandleResponderTerminate]}
onResponderTerminationRequest={[Function bound touchableHandleResponderTerminationRequest]}
onStartShouldSetResponder={[Function bound touchableHandleStartShouldSetResponder]}
style={
Object {
\\"opacity\\": 1,
}
}
>
<Text>
Change freshness!
</Text>
</View>
</View>"
`;
exports[`debug changing component: bananaFresh button message should now be "fresh" 1`] = `
"<View>
<Text>
Is the banana fresh?
</Text>
<Text
testID=\\"bananaFresh\\"
>
fresh
</Text>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function bound touchableHandleResponderGrant]}
onResponderMove={[Function bound touchableHandleResponderMove]}
onResponderRelease={[Function bound touchableHandleResponderRelease]}
onResponderTerminate={[Function bound touchableHandleResponderTerminate]}
onResponderTerminationRequest={[Function bound touchableHandleResponderTerminationRequest]}
onStartShouldSetResponder={[Function bound touchableHandleStartShouldSetResponder]}
style={
Object {
\\"opacity\\": 1,
}
}
>
<Text>
Change freshness!
</Text>
</View>
</View>"
`;
exports[`debug: shallow 1`] = `
"<View>
<Text>
Is the banana fresh?
</Text>
<Text
testID=\\"bananaFresh\\"
>
not fresh
</Text>
<Button
onPress={[Function anonymous]}
type=\\"primary\\"
>
Change freshness!
</Button>
</View>"
`;
exports[`debug: shallow with message 1`] = `
"my other custom message
<View>
<Text>
Is the banana fresh?
</Text>
<Text
testID=\\"bananaFresh\\"
>
not fresh
</Text>
<Button
onPress={[Function anonymous]}
type=\\"primary\\"
>
Change freshness!
</Button>
</View>"
`;
exports[`debug: with message 1`] = `
"my custom message
<View>
<Text>
Is the banana fresh?
</Text>
<Text
testID=\\"bananaFresh\\"
>
not fresh
</Text>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function bound touchableHandleResponderGrant]}
onResponderMove={[Function bound touchableHandleResponderMove]}
onResponderRelease={[Function bound touchableHandleResponderRelease]}
onResponderTerminate={[Function bound touchableHandleResponderTerminate]}
onResponderTerminationRequest={[Function bound touchableHandleResponderTerminationRequest]}
onStartShouldSetResponder={[Function bound touchableHandleStartShouldSetResponder]}
style={
Object {
\\"opacity\\": 1,
}
}
>
<Text>
Change freshness!
</Text>
</View>
</View>"
`;
exports[`toJSON 1`] = `
<View
accessible={true}
+4 -3
View File
@@ -4,6 +4,7 @@ import React from 'react';
import { TouchableOpacity, Text } from 'react-native';
import stripAnsi from 'strip-ansi';
import { debug, render, fireEvent, flushMicrotasksQueue } from '..';
import debugShallow from '../helpers/debugShallow';
function TextComponent({ text }) {
return <Text>{text}</Text>;
@@ -43,11 +44,11 @@ test('debug', () => {
debug(component, 'test message');
expect(console.log).toBeCalledWith(output, 'test message');
expect(console.log).toBeCalledWith('test message\n\n', output);
});
test('debug.shallow', () => {
expect(debug.shallow).toBe(debug);
expect(debug.shallow).toBe(debugShallow);
});
test('debug.deep', () => {
@@ -65,7 +66,7 @@ test('debug.deep', () => {
debug.deep(component, 'test message');
expect(console.log).toBeCalledWith(output, 'test message');
expect(console.log).toBeCalledWith('test message\n\n', output);
});
test('debug.deep async test', async () => {
+42 -1
View File
@@ -2,7 +2,8 @@
/* eslint-disable react/no-multi-comp */
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { render } from '..';
import stripAnsi from 'strip-ansi';
import { render, fireEvent } from '..';
class Button extends React.Component<*> {
render() {
@@ -186,3 +187,43 @@ test('toJSON', () => {
expect(toJSON()).toMatchSnapshot();
});
test('debug', () => {
jest.spyOn(console, 'log').mockImplementation(x => x);
const { debug } = render(<Banana />);
debug();
debug('my custom message');
debug.shallow();
debug.shallow('my other custom message');
// eslint-disable-next-line no-console
const mockCalls = console.log.mock.calls;
expect(stripAnsi(mockCalls[0][0])).toMatchSnapshot();
expect(stripAnsi(mockCalls[1][0] + mockCalls[1][1])).toMatchSnapshot(
'with message'
);
expect(stripAnsi(mockCalls[2][0])).toMatchSnapshot('shallow');
expect(stripAnsi(mockCalls[3][0] + mockCalls[3][1])).toMatchSnapshot(
'shallow with message'
);
});
test('debug changing component', () => {
jest.spyOn(console, 'log').mockImplementation(x => x);
const { getByProps, debug } = render(<Banana />);
fireEvent.press(getByProps({ type: 'primary' }));
debug();
// eslint-disable-next-line no-console
const mockCalls = console.log.mock.calls;
expect(stripAnsi(mockCalls[4][0])).toMatchSnapshot(
'bananaFresh button message should now be "fresh"'
);
});
+15 -25
View File
@@ -1,26 +1,15 @@
// @flow
/* eslint-disable no-console */
import * as React from 'react';
import prettyFormat, { plugins } from 'pretty-format';
import shallow from './shallow';
import render from './render';
/**
* Log pretty-printed shallow test component instance
*/
function debugShallow(
instance: ReactTestInstance | React.Element<*>,
message?: any
) {
const { output } = shallow(instance);
console.log(format(output), message || '');
}
import debugShallow from './helpers/debugShallow';
import debugDeep from './helpers/debugDeep';
import format from './helpers/format';
/**
* Log pretty-printed deep test component instance
*/
function debugDeep(
function debugDeepElementOrInstance(
instance: React.Element<*> | ?ReactTestRendererJSON,
message?: any = ''
) {
@@ -29,21 +18,22 @@ function debugDeep(
// rendering ?ReactTestRendererJSON
// $FlowFixMe
const { toJSON } = render(instance);
console.log(format(toJSON()), message);
if (message) {
console.log(`${message}\n\n`, format(toJSON()));
} else {
console.log(format(toJSON()));
}
} catch (e) {
console.log(format(instance), message);
// $FlowFixMe
debugDeep(instance);
}
}
const format = input =>
prettyFormat(input, {
plugins: [plugins.ReactTestComponent, plugins.ReactElement],
highlight: true,
});
const debug = debugShallow;
function debug(instance: ReactTestInstance | React.Element<*>, message?: any) {
return debugShallow(instance, message);
}
debug.shallow = debugShallow;
debug.deep = debugDeep;
debug.deep = debugDeepElementOrInstance;
export default debug;
+16
View File
@@ -0,0 +1,16 @@
// @flow
import format from './format';
/**
* Log pretty-printed deep test component instance
*/
export default function debugDeep(
instance: ?ReactTestRendererJSON,
message?: any = ''
) {
if (message) {
console.log(`${message}\n\n`, format(instance));
} else {
console.log(format(instance));
}
}
+20
View File
@@ -0,0 +1,20 @@
// @flow
import * as React from 'react';
import shallow from '../shallow';
import format from './format';
/**
* Log pretty-printed shallow test component instance
*/
export default function debugShallow(
instance: ReactTestInstance | React.Element<*>,
message?: any
) {
const { output } = shallow(instance);
if (message) {
console.log(`${message}\n\n`, format(output));
} else {
console.log(format(output));
}
}
+10
View File
@@ -0,0 +1,10 @@
// @flow
import prettyFormat, { plugins } from 'pretty-format';
const format = (input: ?ReactTestRendererJSON) =>
prettyFormat(input, {
plugins: [plugins.ReactTestComponent, plugins.ReactElement],
highlight: true,
});
export default format;
+11
View File
@@ -3,6 +3,8 @@ import * as React from 'react';
import TestRenderer from 'react-test-renderer'; // eslint-disable-line import/no-extraneous-dependencies
import { getByAPI } from './helpers/getByAPI';
import { queryByAPI } from './helpers/queryByAPI';
import debugShallow from './helpers/debugShallow';
import debugDeep from './helpers/debugDeep';
/**
* Renders test component deeply using react-test-renderer and exposes helpers
@@ -21,5 +23,14 @@ export default function render(
update: renderer.update,
unmount: renderer.unmount,
toJSON: renderer.toJSON,
debug: debug(instance, renderer),
};
}
function debug(instance: ReactTestInstance, renderer) {
function debugImpl(message?: string) {
return debugDeep(renderer.toJSON(), message);
}
debugImpl.shallow = message => debugShallow(instance, message);
return debugImpl;
}
+2
View File
@@ -67,6 +67,8 @@ const queryAllByTextRegExp: Array<ReactTestInstance> = tree.queryAllByText(
const queryAllByProps: Array<ReactTestInstance> = tree.getAllByProps({
value: 2,
});
const debugFn = tree.debug()
const debugFnWithMessage = tree.debug('my message')
tree.update(<View />);
tree.unmount();
+1
View File
@@ -35,6 +35,7 @@ export interface RenderAPI extends GetByAPI, QueryByAPI {
update(nextElement: React.ReactElement<any>): void;
unmount(nextElement?: React.ReactElement<any>): void;
toJSON(): ReactTestRendererJSON | null;
debug(message?: string): void;
}
export type FireEventFunction = (