Skip to content

Commit

Permalink
Merge pull request #4 from dabapps/bug-fix-nested-array-children
Browse files Browse the repository at this point in the history
Bug fix nested array children
  • Loading branch information
JakeSidSmith authored Apr 2, 2019
2 parents 8a68dfc + f245571 commit d5ec750
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 62 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dabapps/react-shallow-renderer",
"version": "1.0.0",
"version": "1.0.1",
"description": "A shallow renderer for React components",
"main": "dist/index.js",
"scripts": {
Expand Down
8 changes: 8 additions & 0 deletions src/guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
providerSymbol,
} from './constants';
import {
ReactAnyChildren,
ReactAnyChildrenArray,
ReactAnyNode,
ReactClassNode,
ReactConsumerNode,
Expand Down Expand Up @@ -84,3 +86,9 @@ export function isForwardRef(node: ReactAnyNode): node is ReactForwardRefNode {
export function isPortal(node: ReactAnyNode): node is ReactDOMPortalNode {
return node.$$typeof === portalSymbol;
}

export function isArrayOfChildren(
node: ReactAnyChildren
): node is ReactAnyChildrenArray {
return Array.isArray(node);
}
86 changes: 34 additions & 52 deletions src/renderer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { elementSymbol } from './constants';
import {
isArrayOfChildren,
isClass,
isConsumer,
isForwardRef,
Expand All @@ -14,9 +15,11 @@ import {
import {
ReactAnyChild,
ReactAnyChildren,
ReactAnyChildrenArray,
ReactAnyNode,
ReactResolvedChild,
ReactResolvedChildren,
ReactResolvedChildrenArray,
} from './types';

export class ReactShallowRenderer {
Expand All @@ -36,18 +39,16 @@ export class ReactShallowRenderer {
...node,
props: {
...node.props,
children: this.resolveChildren(node),
children: this.resolveChildren(node.props.children),
},
};
}

if (isFunction(node)) {
const rendered = node.type(node.props) as ReactAnyChildren;
const children = ([] as ReadonlyArray<ReactAnyChild>)
.concat(rendered)
.map(child => {
return this.resolveChild(child);
});
const children = this.resolveChildren(
([] as ReactAnyChildrenArray).concat(rendered)
);

if (children.length === 1) {
return children[0];
Expand All @@ -58,11 +59,9 @@ export class ReactShallowRenderer {

if (isClass(node)) {
const rendered = new node.type(node.props).render() as ReactAnyChildren;
const children = ([] as ReadonlyArray<ReactAnyChild>)
.concat(rendered)
.map(child => {
return this.resolveChild(child);
});
const children = this.resolveChildren(
([] as ReactAnyChildrenArray).concat(rendered)
);

if (children.length === 1) {
return children[0];
Expand Down Expand Up @@ -90,11 +89,9 @@ export class ReactShallowRenderer {
node.props,
node.ref
) as ReactAnyChildren;
const children = ([] as ReadonlyArray<ReactAnyChild>)
.concat(rendered)
.map(child => {
return this.resolveChild(child);
});
const children = this.resolveChildren(
([] as ReactAnyChildrenArray).concat(rendered)
);

if (children.length === 1) {
return children[0];
Expand All @@ -111,46 +108,31 @@ export class ReactShallowRenderer {
}

private resolveChildren(
node: ReactAnyNode
): ReadonlyArray<ReactResolvedChild> {
if (isHTML(node)) {
return typeof node.props.children !== 'undefined'
? ([] as ReadonlyArray<ReactAnyChild>)
.concat(node.props.children)
.map(child => this.resolveChild(child))
: [];
children: ReactAnyChildren
): ReactResolvedChildrenArray {
if (typeof children === 'undefined') {
return [];
}

if (isFunction(node) || isClass(node)) {
return typeof node.props.children !== 'undefined'
? ([] as ReadonlyArray<ReactAnyChild>)
.concat(node.props.children)
.map(child => this.resolveChild(child))
: [];
}
return ([] as ReactResolvedChildrenArray).concat(
this.resolveNestedChildren(children)
);
}

if (
isMemo(node) ||
isFragment(node) ||
isProvider(node) ||
isConsumer(node) ||
isForwardRef(node)
) {
return this.resolveChildren({
...node,
type: this.resolveChildName(node),
});
private resolveNestedChildren(
children: ReactAnyChildren
): ReactResolvedChildren {
if (!isArrayOfChildren(children)) {
return this.resolveChild(children);
}

if (isPortal(node)) {
return typeof node.children !== 'undefined'
? ([] as ReadonlyArray<ReactAnyChild>)
.concat(node.children)
.map(child => this.resolveChild(child))
: [];
}
return children.map(child => {
if (isArrayOfChildren(child)) {
return this.resolveNestedChildren(child);
}

throw new Error(this.constructInvalidNodeMessage(node));
return this.resolveChild(child);
});
}

private resolveChild(node: ReactAnyChild): ReactResolvedChild {
Expand All @@ -166,7 +148,7 @@ export class ReactShallowRenderer {
key: null,
ref: null,
props: {
children: this.resolveChildren(node),
children: this.resolveChildren(node.children),
},
_owner: null,
_store: {},
Expand All @@ -179,7 +161,7 @@ export class ReactShallowRenderer {
type: this.resolveChildName(node),
props: {
...node.props,
children: this.resolveChildren(node),
children: this.resolveChildren(node.props.children),
},
};
}
Expand Down
11 changes: 8 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface ForwardRefType {
}

export type ReactPrimitiveChild =
| undefined
| string
| number
| null
Expand All @@ -38,11 +39,15 @@ export type ReactPrimitiveChild =
| React.ComponentClass;

export type ReactAnyChild = ReactAnyNode | ReactPrimitiveChild;
export type ReactAnyChildren = ReadonlyArray<ReactAnyChild> | ReactAnyChild;
export interface ReactAnyChildrenArray
extends ReadonlyArray<ReactAnyChildren> {}
export type ReactAnyChildren = ReactAnyChildrenArray | ReactAnyChild;

export type ReactResolvedChild = ReactResolvedNode | ReactPrimitiveChild;
export interface ReactResolvedChildrenArray
extends ReadonlyArray<ReactResolvedChildren> {}
export type ReactResolvedChildren =
| ReadonlyArray<ReactResolvedChild>
| ReactResolvedChildrenArray
| ReactResolvedChild;

export type ReactAnyNode = ReactElementNode | ReactDOMPortalNode;
Expand Down Expand Up @@ -79,7 +84,7 @@ export interface ReactResolvedNode extends ReactElementNode {
ref: React.Ref<unknown>;
props: {
[i: string]: unknown;
children: ReadonlyArray<ReactResolvedChild>;
children: ReactResolvedChildrenArray;
};
_owner: unknown;
_store: unknown;
Expand Down
26 changes: 21 additions & 5 deletions tests/internal-methods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,29 @@ describe('ReactShallowRenderer', () => {
});

describe('resolveChildren', () => {
it('throws an error if the node is invalid', () => {
it('returns an empty array when undefined is provided', () => {
const renderer = new ReactShallowRenderer(<div />);

expect(() =>
// tslint:disable-next-line:no-string-literal
renderer['resolveChildren'](({} as unknown) as ReactAnyNode)
).toThrow(/invalid/i);
// tslint:disable-next-line:no-string-literal
expect(renderer['resolveChildren'](undefined)).toEqual([]);
});

it('calls resolveNestedChildren with non-undefined children', () => {
const renderer = new ReactShallowRenderer(<div />);
// tslint:disable-next-line:no-string-literal
renderer['resolveNestedChildren'] = jest.fn();

const nonUndefinedChildren = ['Hello'];

// tslint:disable-next-line:no-string-literal
renderer['resolveChildren'](nonUndefinedChildren);

// tslint:disable-next-line:no-string-literal
expect(renderer['resolveNestedChildren']).toHaveBeenCalledTimes(1);
// tslint:disable-next-line:no-string-literal
expect(renderer['resolveNestedChildren']).toHaveBeenCalledWith(
nonUndefinedChildren
);
});
});

Expand Down
78 changes: 78 additions & 0 deletions tests/to-json/basic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ describe('ReactShallowRenderer', () => {
</div>
);

const ComponentWithMappedChildren: React.FunctionComponent = () => (
<div>
{[1, 2, 3].map(child => (
<p key={child}>{child}</p>
))}
{[[<div key={1} />]]}
</div>
);

describe('toJSON', () => {
it('renders some simple HTML', () => {
const element = (
Expand Down Expand Up @@ -87,5 +96,74 @@ describe('ReactShallowRenderer', () => {
_store: {},
});
});

it('renders a component with mapped (nested) children', () => {
const element = <ComponentWithMappedChildren />;

const renderer = new ReactShallowRenderer(element);

compare(renderer.toJSON(), {
$$typeof: elementSymbol,
type: 'div',
key: null,
ref: null,
props: {
children: [
[
{
$$typeof: elementSymbol,
type: 'p',
key: '1',
ref: null,
props: {
children: [1],
},
_owner: null,
_store: {},
},
{
$$typeof: elementSymbol,
type: 'p',
key: '2',
ref: null,
props: {
children: [2],
},
_owner: null,
_store: {},
},
{
$$typeof: elementSymbol,
type: 'p',
key: '3',
ref: null,
props: {
children: [3],
},
_owner: null,
_store: {},
},
],
[
[
{
$$typeof: elementSymbol,
type: 'div',
key: '1',
ref: null,
props: {
children: [],
},
_owner: null,
_store: {},
},
],
],
],
},
_owner: null,
_store: {},
});
});
});
});

0 comments on commit d5ec750

Please sign in to comment.