From 492c439574c6ede6a0753ea9ba8a2daec570443d Mon Sep 17 00:00:00 2001 From: Nathan Knowler <nathan@knowler.me> Date: Thu, 10 Sep 2020 01:28:30 -0500 Subject: [PATCH] [New] `childrenOf`/`childrenOfType`/`childrenSequenceOf`: support fragments via `renderableChildren` helper This includes children of fragments as renderable children. Fixes #71. --- package.json | 1 + src/helpers/renderableChildren.js | 9 +- test/childrenOf.jsx | 60 +++++++++++ test/childrenOfType.jsx | 170 ++++++++++++++++++++++++++++++ test/childrenSequenceOf.jsx | 68 ++++++++++++ 5 files changed, 307 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e1748ea..93540d2 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "dependencies": { "array.prototype.find": "^2.1.1", + "array.prototype.flatmap": "^1.2.3", "function.prototype.name": "^1.1.2", "is-regex": "^1.1.1", "object-is": "^1.1.2", diff --git a/src/helpers/renderableChildren.js b/src/helpers/renderableChildren.js index db22666..b7be7c9 100644 --- a/src/helpers/renderableChildren.js +++ b/src/helpers/renderableChildren.js @@ -1,5 +1,12 @@ import React from 'react'; +import { isFragment } from 'react-is'; +import flatMap from 'array.prototype.flatmap'; export default function renderableChildren(childrenProp) { - return React.Children.toArray(childrenProp).filter((child) => child === 0 || child); + return flatMap(React.Children.toArray(childrenProp), (child) => { + if (isFragment(child)) { + return renderableChildren(child.props.children); + } + return child === 0 || child ? child : []; + }); } diff --git a/test/childrenOf.jsx b/test/childrenOf.jsx index 7cd75c8..a261cec 100644 --- a/test/childrenOf.jsx +++ b/test/childrenOf.jsx @@ -9,6 +9,8 @@ import callValidator from './_callValidator'; function SFC() {} class Component extends React.Component {} // eslint-disable-line react/prefer-stateless-function +const describeIfFragments = React.Fragment ? describe : describe.skip; + describe('childrenOf', () => { function assertPasses(validator, element, propName, componentName) { expect(callValidator(validator, element, propName, componentName)).to.equal(null); @@ -298,4 +300,62 @@ describe('childrenOf', () => { 'Component!', )); }); + + describeIfFragments('it passes through fragments to validates its children instead', () => { + it('fails when an empty fragment is provided, but children are required', () => assertFails( + childrenOf(node).isRequired, + ( + <div><></></div> + ), + 'children', + )); + + it('passes when an empty fragment is provided and children are optional', () => assertPasses( + childrenOf(node), + ( + <div><></></div> + ), + 'children', + )); + + it('passes when a valid child is provided within a fragment', () => assertPasses( + childrenOf(number), + ( + <div> + <> + {0} + </> + </div> + ), + 'children', + 'number', + )); + + it('fails when a invalid child is provided within a fragment', () => assertFails( + childrenOf(number), + ( + <div> + <> + <span /> + </> + </div> + ), + 'children', + 'number!', + )); + + it('passes when fragments are mixed with other nodes', () => assertPasses( + childrenOf(node), + ( + <div> + <span /> + <> + <span /> + </> + </div> + ), + 'children', + 'node!', + )); + }); }); diff --git a/test/childrenOfType.jsx b/test/childrenOfType.jsx index 3a3914f..ed6583c 100644 --- a/test/childrenOfType.jsx +++ b/test/childrenOfType.jsx @@ -8,6 +8,8 @@ import callValidator from './_callValidator'; function SFC() {} class Component extends React.Component {} // eslint-disable-line react/prefer-stateless-function +const describeIfFragments = React.Fragment ? describe : describe.skip; + describe('childrenOfType', () => { it('throws when not given a type', () => { expect(() => childrenOfType()).to.throw(TypeError); @@ -350,6 +352,174 @@ describe('childrenOfType', () => { )); }); + describeIfFragments('with children of the specified types passed as a React fragment', () => { + it('passes with *', () => assertPasses( + childrenOfType('*').isRequired, + ( + <div> + <> + <span key="one" /> + <span key="two" /> + <span key="three" /> + </> + </div> + ), + 'children', + '*!', + )); + + it('passes with *', () => assertPasses( + childrenOfType('*').isRequired, + ( + <div> + <> + <span key="one" /> + <span key="two" /> + <span key="three" /> + </> + </div> + ), + 'children', + '*! required', + )); + + it('passes with a DOM element', () => assertPasses( + childrenOfType('span'), + ( + <div> + <> + <span key="one" /> + <span key="two" /> + <span key="three" /> + </> + </div> + ), + 'children', + 'span!', + )); + + it('passes with an SFC', () => assertPasses( + childrenOfType(SFC), + ( + <div> + <> + <SFC key="one" default="Foo" /> + <SFC key="two" default="Foo" /> + <SFC key="three" default="Foo" /> + </> + </div> + ), + 'children', + 'SFC!', + )); + + it('passes with a Component', () => assertPasses( + childrenOfType(Component), + ( + <div> + <> + <Component key="one" default="Foo" /> + <Component key="two" default="Foo" /> + <Component key="three" default="Foo" /> + </> + </div> + ), + 'children', + 'Component!', + )); + + it('passes with multiple types', () => assertPasses( + childrenOfType(SFC, Component, 'span'), + ( + <div> + <> + <span key="one" default="Foo" /> + <Component key="two" default="Foo" /> + <SFC key="three" default="Foo" /> + </> + </div> + ), + 'children', + 'all three', + )); + + it('passes with multiple types when required', () => assertPasses( + childrenOfType(SFC, Component, 'span').isRequired, + ( + <div> + <> + <span key="one" default="Foo" /> + <Component key="two" default="Foo" /> + <SFC key="three" default="Foo" /> + </> + </div> + ), + 'children', + 'all three required', + )); + + it('passes with multiple types including *', () => assertPasses( + childrenOfType(SFC, '*'), + ( + <div> + <> + <span key="one" default="Foo" /> + <Component key="two" default="Foo" /> + <SFC key="three" default="Foo" /> + text children + </> + </div> + ), + 'children', + 'SFC and *', + )); + + it('passes with multiple types including * when required', () => assertPasses( + childrenOfType(SFC, '*').isRequired, + ( + <div> + <> + <span key="one" default="Foo" /> + <Component key="two" default="Foo" /> + <SFC key="three" default="Foo" /> + text children + </> + </div> + ), + 'children', + 'SFC and *', + )); + + it('passes with a fragment of specified types and a specified type', () => assertPasses( + childrenOfType(SFC, 'span'), + ( + <div> + <> + <SFC key="one" default="Foo" /> + <span key="two" default="Foo" /> + </> + <span /> + </div> + ), + 'children', + 'SFC and span', + )); + + it('fails when the unspecified type is within a fragment', () => assertFails( + childrenOfType('span'), + ( + <div> + <> + <SFC key="one" default="Foo" /> + <Component key="two" default="Foo" /> + </> + </div> + ), + 'children', + 'span!', + )); + }); + describe('when an unspecified type is provided as a child', () => { it('fails expecting a DOM element', () => assertFails( childrenOfType('span'), diff --git a/test/childrenSequenceOf.jsx b/test/childrenSequenceOf.jsx index 55f1f8e..442120e 100644 --- a/test/childrenSequenceOf.jsx +++ b/test/childrenSequenceOf.jsx @@ -6,6 +6,8 @@ import { childrenSequenceOf } from '..'; import callValidator from './_callValidator'; +const describeIfFragments = React.Fragment ? describe : describe.skip; + describe('childrenSequenceOf', () => { it('is a function', () => { expect(typeof childrenSequenceOf).to.equal('function'); @@ -322,4 +324,70 @@ describe('childrenSequenceOf', () => { 'children', ); }); + + describeIfFragments('it passes through fragments to validates its children instead', () => { + it('passes when part of the sequence is in a fragment', () => assertPasses( + childrenSequenceOf({ validator: number, min: 2 }), + ( + <div> + {1} + <>{2}</> + </div> + ), + 'children', + )); + + it('fails when there is an invalid child in a fragment', () => assertFails( + childrenSequenceOf({ validator: number, min: 2 }), + ( + <div> + {1} + <>2</> + </div> + ), + 'children', + )); + + it('passes when the entire sequence is in a fragment', () => assertPasses( + childrenSequenceOf({ validator: number, min: 2 }), + ( + <div> + <> + {1} + {2} + {3} + </> + </div> + ), + 'children', + )); + + it('passes with a complex sequence in a fragment', () => assertPasses( + childrenSequenceOf({ validator: number }, { validator: string }, { validator: number }), + ( + <div> + <> + {1} + 2 + {3} + </> + </div> + ), + 'children', + )); + + it('fails with an invalid sequence in a fragment', () => assertFails( + childrenSequenceOf({ validator: number }, { validator: string }, { validator: number }), + ( + <div> + <> + {1} + {2} + 3 + </> + </div> + ), + 'children', + )); + }); });