} remaining
19 | * @param {Array} [results]
20 | * @returns {ReactElement}
21 | */
22 | function renderRecursive(render, remaining, results) {
23 | results = results || [];
24 | // Once components is exhausted, we can render out the results array.
25 | if (!remaining[0]) {
26 | return render(results);
27 | }
28 |
29 | // Continue recursion for remaining items.
30 | // results.concat([value]) ensures [...results, value] instead of [...results, ...value]
31 | function nextRender(value) {
32 | return renderRecursive(render, remaining.slice(1), results.concat([value]));
33 | }
34 |
35 | // Each props.components entry is either an element or function [element factory]
36 | return typeof remaining[0] === 'function'
37 | ? // When it is a function, produce an element by invoking it with "render component values".
38 | remaining[0]({ results, render: nextRender })
39 | : // When it is an element, enhance the element's props with the render prop.
40 | cloneElement(remaining[0], { children: nextRender });
41 | }
42 |
--------------------------------------------------------------------------------
/test/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow, mount } from 'enzyme';
3 | import Composer from '../src';
4 |
5 | // Does nothing other than render so that its props may be inspected
6 | function MyComponent(/* props */) {
7 | return Inspect my props!
;
8 | }
9 |
10 | function Echo({ value, children }) {
11 | return children({ value });
12 | }
13 |
14 | function EchoRenderProp({ value, renderProp }) {
15 | return renderProp({ value });
16 | }
17 |
18 | function DoubleEcho({ value, children }) {
19 | return children(value, value.toUpperCase());
20 | }
21 |
22 | /**
23 | * Assert expected hierarchy of components
24 | * @param {Wrapper} wrapper
25 | * @param {ReactComponent[]} components
26 | */
27 | function expectComponentTree(wrapper, components) {
28 | const expectedMsg =
29 | 'Expected component tree: ' +
30 | components.map(({ displayName, name }) => displayName || name).join(' > ');
31 | expect([
32 | components.reduce((wrapper, selector) => {
33 | return wrapper.find(selector);
34 | }, wrapper).length,
35 | expectedMsg
36 | ]).toEqual([1, expectedMsg]);
37 | }
38 |
39 | describe('React Composer', () => {
40 | describe('Render, no components', () => {
41 | test('It should render the return from `children`', () => {
42 | const mockChildren = jest.fn(() => Sandwiches
);
43 |
44 | const wrapper = shallow(
45 |
46 | );
47 | expect(wrapper.contains(Sandwiches
)).toBe(true);
48 | expect(mockChildren).toHaveBeenCalledTimes(1);
49 | // The outer array represents all of the arguments passed to the
50 | // mock. The inner array is the empty array of `results` that
51 | // is passed as the first argument.
52 | expect(mockChildren.mock.calls[0]).toEqual([[]]);
53 | });
54 | });
55 |
56 | describe('Render, one component', () => {
57 | test('It should render the expected result', () => {
58 | const mockChildren = jest.fn(([result]) => {result.value}
);
59 |
60 | const wrapper = mount(
61 | ]}
63 | children={mockChildren}
64 | />
65 | );
66 | expect(wrapper.contains(spaghetti
)).toBe(true);
67 | expect(mockChildren).toHaveBeenCalledTimes(1);
68 | expect(mockChildren.mock.calls[0]).toEqual([
69 | [
70 | {
71 | value: 'spaghetti'
72 | }
73 | ]
74 | ]);
75 | });
76 | });
77 |
78 | describe('Render, two components', () => {
79 | test('It should render the expected result', () => {
80 | const mockChildren = jest.fn(([resultOne, resultTwo]) => (
81 |
82 | {resultOne.value} {resultTwo.value}
83 |
84 | ));
85 |
86 | const wrapper = mount(
87 | , ]}
89 | children={mockChildren}
90 | />
91 | );
92 | expect(wrapper.contains(spaghetti pls
)).toBe(true);
93 | expect(mockChildren).toHaveBeenCalledTimes(1);
94 | expect(mockChildren.mock.calls[0]).toEqual([
95 | [
96 | {
97 | value: 'spaghetti'
98 | },
99 | {
100 | value: 'pls'
101 | }
102 | ]
103 | ]);
104 | });
105 | });
106 |
107 | describe('Render order', () => {
108 | test('It renders first:Outer, last:Inner', () => {
109 | const Outer = ({ children }) => children('Outer result');
110 | const Middle = ({ children }) => children('Middle result');
111 | const Inner = ({ children }) => children('Inner result');
112 |
113 | const wrapper = mount(
114 | , , ]}
116 | children={results => }
117 | />
118 | );
119 |
120 | [
121 | [Outer, Middle],
122 | [Outer, Middle, Inner],
123 | [Outer, Middle, Inner, MyComponent]
124 | ].forEach(expectedTree => {
125 | expectComponentTree(wrapper, expectedTree);
126 | });
127 |
128 | expect(wrapper.find(MyComponent).prop('results')).toEqual([
129 | 'Outer result',
130 | 'Middle result',
131 | 'Inner result'
132 | ]);
133 | });
134 | });
135 |
136 | describe('props.components as functions', () => {
137 | test('It enables utilizing outer results for inner components', () => {
138 | const wrapper = mount(
139 | ,
143 |
144 | // A function may be passed to produce an element.
145 | // It will be invoked with renderComponentValues.
146 | ({ render, results: [outerResult] }) => (
147 |
148 | ),
149 |
150 | ({ render, results: [, middleResult] }) => (
151 |
152 | )
153 | ]}
154 | children={results => }
155 | />
156 | );
157 |
158 | expect(wrapper.find(Echo).length).toEqual(3);
159 |
160 | const outer = wrapper.childAt(0);
161 | expect(outer.prop('value')).toBe('outer');
162 |
163 | const middle = outer.childAt(0);
164 | expect(middle.prop('value')).toBe('outer + middle');
165 |
166 | const inner = middle.childAt(0);
167 | expect(inner.prop('value')).toBe('outer + middle + inner');
168 |
169 | expect(wrapper.find(MyComponent).prop('results')).toEqual([
170 | { value: 'outer' },
171 | { value: 'outer + middle' },
172 | { value: 'outer + middle + inner' }
173 | ]);
174 | });
175 |
176 | test('It enables composing with varying render prop names', () => {
177 | const wrapper = mount(
178 | ,
181 | ({ render }) =>
182 | ]}
183 | children={results => }
184 | />
185 | );
186 |
187 | expect(wrapper.find(MyComponent).prop('results')).toEqual([
188 | { value: 'one' },
189 | { value: 'two' }
190 | ]);
191 | });
192 |
193 | test('It enables composing with multi-argument producers', () => {
194 | const wrapper = mount(
195 | ,
198 | // multi-argument producer
199 | ({ render }) => (
200 |
201 | {(one, two) => render([one, two])}
202 |
203 | )
204 | ]}
205 | children={results => }
206 | />
207 | );
208 |
209 | expect(wrapper.find(MyComponent).prop('results')).toEqual([
210 | { value: 'one' },
211 | ['two', 'TWO']
212 | ]);
213 | });
214 | });
215 | });
216 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 |
4 | Enzyme.configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------