Skip to content
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

feat: Render element tree in query error messages #1378

Merged
merged 24 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
55f8026
feat: Render virtual DOM in byText error message
stevehanson Mar 31, 2023
1f64a25
Preserve props that hide, since relevant to failure
stevehanson Mar 31, 2023
e960d91
Refactor to add props, move logic to makeQueries
stevehanson Apr 4, 2023
fb9e891
Add missing tests
stevehanson Apr 5, 2023
4f0389f
Optimize findBy, findAllBy to only render DOM on timeout
stevehanson Apr 5, 2023
d7cf14b
refactor: make queries clean up
mdjastrzebski Apr 5, 2023
f45b242
refactor: remove color control codes from error
mdjastrzebski Apr 5, 2023
0a0bb33
refactor: disable format element coloring just for our tests
mdjastrzebski Apr 5, 2023
e5d6080
fix: host elements error message
mdjastrzebski Apr 5, 2023
1b95f4d
refactor: cleanup host component names
mdjastrzebski Apr 5, 2023
701c39e
chore: reverse unnecessary reorder of imports
mdjastrzebski Apr 5, 2023
6be5df7
refactor: tweaks
mdjastrzebski Apr 5, 2023
5c7bb55
refactor: tweaks
mdjastrzebski Apr 5, 2023
7904d3a
fix: fix issue where findBy* doesn't print tree
stevehanson Apr 20, 2023
6d3d50c
refactor: update wording 'DOM' -> 'element tree'
stevehanson Apr 20, 2023
8f42584
refactor: tweaks
mdjastrzebski Apr 26, 2023
4fbdda0
refactor: tweaks
mdjastrzebski Apr 26, 2023
35c3393
refactor: tweaks
mdjastrzebski Apr 26, 2023
e13c56f
refactor: restore stack change
mdjastrzebski Apr 26, 2023
eb10229
refactor: improve onTimeout error typing
mdjastrzebski Apr 26, 2023
9f81589
chore: fix lint
mdjastrzebski Apr 26, 2023
139df37
chore: increase code coverage
mdjastrzebski Apr 27, 2023
37e4aeb
chore: tweaks
mdjastrzebski Apr 27, 2023
f5f64d5
refactor: improve unit tests
mdjastrzebski Apr 27, 2023
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
3 changes: 3 additions & 0 deletions jest-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
beforeEach(() => {
resetToDefaults();
});

// Disable colors in our local tests in order to generate clear snapshots
process.env.COLORS = 'false';
18 changes: 9 additions & 9 deletions src/__tests__/__snapshots__/render-debug.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ exports[`debug 1`] = `
/>
<View
accessibilityState={
Object {
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
Expand All @@ -39,7 +39,7 @@ exports[`debug 1`] = `
}
}
accessibilityValue={
Object {
{
"max": undefined,
"min": undefined,
"now": undefined,
Expand Down Expand Up @@ -109,7 +109,7 @@ exports[`debug changing component: bananaFresh button message should now be "fre
/>
<View
accessibilityState={
Object {
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
Expand All @@ -118,7 +118,7 @@ exports[`debug changing component: bananaFresh button message should now be "fre
}
}
accessibilityValue={
Object {
{
"max": undefined,
"min": undefined,
"now": undefined,
Expand Down Expand Up @@ -169,7 +169,7 @@ exports[`debug should use debugOptions from config when no option is specified 1
exports[`debug should use given options over config debugOptions 1`] = `
"<View
style={
Object {
{
"backgroundColor": "red",
}
}
Expand Down Expand Up @@ -315,7 +315,7 @@ exports[`debug: another custom message 1`] = `
/>
<View
accessibilityState={
Object {
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
Expand All @@ -324,7 +324,7 @@ exports[`debug: another custom message 1`] = `
}
}
accessibilityValue={
Object {
{
"max": undefined,
"min": undefined,
"now": undefined,
Expand Down Expand Up @@ -498,7 +498,7 @@ exports[`debug: with message 1`] = `
/>
<View
accessibilityState={
Object {
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
Expand All @@ -507,7 +507,7 @@ exports[`debug: with message 1`] = `
}
}
accessibilityValue={
Object {
{
"max": undefined,
"min": undefined,
"now": undefined,
Expand Down
31 changes: 7 additions & 24 deletions src/__tests__/host-component-names.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ import {
getHostComponentNames,
configureHostComponentNamesIfNeeded,
} from '../helpers/host-component-names';
import * as within from '../within';
import { act, render } from '..';

const mockCreate = jest.spyOn(TestRenderer, 'create') as jest.Mock;
const mockGetQueriesForElements = jest.spyOn(
within,
'getQueriesForElement'
) as jest.Mock;

beforeEach(() => {
mockCreate.mockReset();
});

describe('getHostComponentNames', () => {
test('returns host component names from internal config', () => {
Expand Down Expand Up @@ -79,8 +78,10 @@ describe('configureHostComponentNamesIfNeeded', () => {
});

test('throw an error when autodetection fails', () => {
const renderer = TestRenderer.create(<View />);

mockCreate.mockReturnValue({
root: { type: View, children: [], props: {} },
root: renderer.root,
});

expect(() => configureHostComponentNamesIfNeeded())
Expand All @@ -93,22 +94,4 @@ describe('configureHostComponentNamesIfNeeded', () => {
Please check if you are using compatible versions of React Native and React Native Testing Library."
`);
});

test('throw an error when autodetection fails due to getByTestId returning non-host component', () => {
mockGetQueriesForElements.mockReturnValue({
getByTestId: () => {
return { type: View };
},
});

expect(() => configureHostComponentNamesIfNeeded())
.toThrowErrorMatchingInlineSnapshot(`
"Trying to detect host component names triggered the following error:

getByTestId returned non-host component

There seems to be an issue with your configuration that prevents React Native Testing Library from working correctly.
Please check if you are using compatible versions of React Native and React Native Testing Library."
`);
});
});
9 changes: 9 additions & 0 deletions src/__tests__/waitFor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,12 @@ test.each([
expect(onPress).toHaveBeenCalledWith('red');
}
);

test('waitFor throws if expectation is not a function', async () => {
await expect(
// @ts-expect-error intentionally passing non-function
waitFor('not a function')
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Received \`expectation\` arg must be a function"`
);
});
114 changes: 114 additions & 0 deletions src/helpers/__tests__/format-default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { ReactTestRendererJSON } from 'react-test-renderer';
import { defaultMapProps } from '../format-default';

const node: ReactTestRendererJSON = {
type: 'View',
props: {},
children: null,
};

describe('mapPropsForQueryError', () => {
test('preserves props that are helpful for debugging', () => {
const props = {
accessibilityElementsHidden: true,
accessibilityViewIsModal: true,
importantForAccessibility: 'yes',
testID: 'TEST_ID',
nativeID: 'NATIVE_ID',
accessibilityLabel: 'LABEL',
accessibilityLabelledBy: 'LABELLED_BY',
accessibilityRole: 'ROLE',
accessibilityHint: 'HINT',
placeholder: 'PLACEHOLDER',
value: 'VALUE',
defaultValue: 'DEFAULT_VALUE',
};

const result = defaultMapProps(props, node);

expect(result).toStrictEqual(props);
});

test('does not preserve less helpful props', () => {
const result = defaultMapProps(
{
style: [{ flex: 1 }, { display: 'flex' }],
onPress: () => null,
key: 'foo',
},
node
);

expect(result).toStrictEqual({});
});

test('preserves "display: none" style but no other style', () => {
const result = defaultMapProps(
{ style: [{ flex: 1 }, { display: 'none', flex: 2 }] },
node
);

expect(result).toStrictEqual({
style: { display: 'none' },
});
});

test('removes undefined keys from accessibilityState', () => {
const result = defaultMapProps(
{ accessibilityState: { checked: undefined, selected: false } },
node
);

expect(result).toStrictEqual({
accessibilityState: { selected: false },
});
});

test('removes accessibilityState if all keys are undefined', () => {
const result = defaultMapProps(
{ accessibilityState: { checked: undefined, selected: undefined } },
node
);

expect(result).toStrictEqual({});
});

test('does not fail if accessibilityState is a string, passes through', () => {
const result = defaultMapProps({ accessibilityState: 'foo' }, node);
expect(result).toStrictEqual({ accessibilityState: 'foo' });
});

test('does not fail if accessibilityState is an array, passes through', () => {
const result = defaultMapProps({ accessibilityState: [1] }, node);
expect(result).toStrictEqual({ accessibilityState: [1] });
});

test('does not fail if accessibilityState is null, passes through', () => {
const result = defaultMapProps({ accessibilityState: null }, node);
expect(result).toStrictEqual({ accessibilityState: null });
});

test('does not fail if accessibilityState is nested object, passes through', () => {
const accessibilityState = { 1: { 2: 3 }, 2: undefined };
const result = defaultMapProps({ accessibilityState }, node);
expect(result).toStrictEqual({ accessibilityState: { 1: { 2: 3 } } });
});

test('removes undefined keys from accessibilityValue', () => {
const result = defaultMapProps(
{ accessibilityValue: { min: 1, max: undefined } },
node
);

expect(result).toStrictEqual({ accessibilityValue: { min: 1 } });
});

test('removes accessibilityValue if all keys are undefined', () => {
const result = defaultMapProps(
{ accessibilityValue: { min: undefined } },
node
);

expect(result).toStrictEqual({});
});
});
72 changes: 72 additions & 0 deletions src/helpers/format-default.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { StyleSheet, ViewStyle } from 'react-native';
import { MapPropsFunction } from './format';

const propsToDisplay = [
'testID',
'nativeID',
'accessibilityElementsHidden',
'accessibilityViewIsModal',
'importantForAccessibility',
'accessibilityRole',
'accessibilityLabel',
'accessibilityLabelledBy',
'accessibilityHint',
'placeholder',
'value',
'defaultValue',
'title',
];

/**
* Preserve props that are helpful in diagnosing test failures, while stripping rest
*/
export const defaultMapProps: MapPropsFunction = (props) => {
const result: Record<string, unknown> = {};

const styles = StyleSheet.flatten(props.style as ViewStyle);
if (styles?.display === 'none') {
result.style = { display: 'none' };
}

const accessibilityState = removeUndefinedKeys(props.accessibilityState);
if (accessibilityState !== undefined) {
result.accessibilityState = accessibilityState;
}

const accessibilityValue = removeUndefinedKeys(props.accessibilityValue);
if (accessibilityValue !== undefined) {
result.accessibilityValue = accessibilityValue;
}

propsToDisplay.forEach((propName) => {
if (propName in props) {
result[propName] = props[propName];
}
});

return result;
};

function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function removeUndefinedKeys(prop: unknown) {
if (!isObject(prop)) {
return prop;
}

const result: Record<string, unknown> = {};
Object.entries(prop).forEach(([key, value]) => {
if (value !== undefined) {
result[key] = value;
}
});

// If object does not have any props we will ignore it.
if (Object.keys(result).length === 0) {
return undefined;
}

return result;
}
9 changes: 7 additions & 2 deletions src/helpers/format.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactTestRendererJSON } from 'react-test-renderer';
import prettyFormat, { NewPlugin, plugins } from 'pretty-format';

type MapPropsFunction = (
export type MapPropsFunction = (
props: Record<string, unknown>,
node: ReactTestRendererJSON
) => Record<string, unknown>;
Expand All @@ -16,7 +16,8 @@ const format = (
) =>
prettyFormat(input, {
plugins: [getCustomPlugin(options.mapProps), plugins.ReactElement],
highlight: true,
highlight: shouldHighlight(),
printBasicPrototype: false,
});

const getCustomPlugin = (mapProps?: MapPropsFunction): NewPlugin => {
Expand All @@ -39,4 +40,8 @@ const getCustomPlugin = (mapProps?: MapPropsFunction): NewPlugin => {
};
};

function shouldHighlight() {
return process?.env?.COLORS !== 'false';
}

export default format;
Loading