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',
+    ));
+  });
 });