Loading...
');
181 | });
182 |
183 | test('displays loading component before actions have been dispatched', () => {
184 | class Indicator extends React.Component {
185 | render() {
186 | return stateless
} location={location} routes={defaultRoutes} dispatchActionsOnFirstRender={true} actionNames={DEFAULT_ACTION_NAMES} />);
196 | expect(wrapper.html()).toBe('stateless
');
197 | });
198 |
199 | test('displays loading markup before actions have been dispatched', () => {
200 | const wrapper = shallow();
201 | expect(wrapper.html()).toBe('markup
');
202 | });
203 |
204 | test('renders routes', () => {
205 | const mockRender = jest.fn(() => null);
206 | const routes = [];
207 | mount(
208 |
209 |
210 |
211 | );
212 |
213 | expect(mockRender.mock.calls).toHaveLength(1);
214 | expect(mockRender.mock.calls[0][0]).toEqual(routes);
215 | expect(mockRender.mock.calls[0][1]).toEqual({ renderProp: '1' });
216 | });
217 |
218 | test('returns null if no routes exist', () => {
219 | const wrapper = mount(
220 |
221 |
222 |
223 | );
224 | expect(wrapper.html()).toBe(null);
225 | });
226 | });
227 | });
228 |
--------------------------------------------------------------------------------
/src/__tests__/dispatchRouteActions.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { matchRoutes } from 'react-router-config';
4 | import withActions from '../withActions';
5 | import {
6 | dispatchRouteActions,
7 | dispatchServerActions,
8 | dispatchClientActions,
9 | resolveActionSets,
10 | resolveRouteComponents,
11 | reduceActionSets,
12 | dispatchComponentActions,
13 | parseDispatchActions
14 | } from '../dispatchRouteActions';
15 |
16 | let order = [];
17 | let orderedParams = [];
18 | const appendOrder = (id) => order.push(id);
19 | const appendParams = (props, routeCtx) => orderedParams.push([props, routeCtx]);
20 |
21 | const LOAD_DATA = 'loadData';
22 | const PARSE_DATA = 'parseData';
23 |
24 | const defaultActionParams = {
25 | httpResponse: {
26 | statusCode: 200
27 | }
28 | };
29 |
30 | function initRoutes(opts = {}) {
31 | const {
32 | mockInitServerAction,
33 | mockLoadDataMapToProps,
34 | mockInitClientAction,
35 | mockParseDataMapToProps,
36 | mockRootAction,
37 | mockHomeAction
38 | } = Object.assign({
39 | mockInitServerAction: jest.fn(p => p),
40 | mockLoadDataMapToProps: jest.fn(p => p),
41 | mockInitClientAction: jest.fn(p => p),
42 | mockParseDataMapToProps: jest.fn(p => p),
43 | mockRootAction: jest.fn((actionProps, routerCtx) => {
44 | appendOrder(0); appendParams(actionProps, routerCtx);
45 | }),
46 | mockHomeAction: jest.fn((actionProps, routerCtx) => {
47 | appendOrder(1); appendParams(actionProps, routerCtx);
48 | })
49 | }, opts);
50 |
51 | function loadDataAction() {
52 | return {
53 | name: LOAD_DATA,
54 | staticMethodName: 'primary',
55 | initServerAction: mockInitServerAction,
56 | filterParamsToProps: mockLoadDataMapToProps
57 | };
58 | }
59 |
60 | function parseDataAction() {
61 | return {
62 | name: PARSE_DATA,
63 | staticMethodName: 'secondary',
64 | initClientAction: mockInitClientAction,
65 | filterParamsToProps: mockParseDataMapToProps
66 | };
67 | }
68 |
69 | let Root = ({children}) => {children}
;
70 | Root.propTypes = {children: PropTypes.any};
71 | Root.primary = mockRootAction;
72 | Root = withActions(null, loadDataAction())(Root);
73 |
74 | let Home = () => Hello World
;
75 | Home.secondary = mockHomeAction;
76 | Home = withActions(null, parseDataAction())(Home);
77 |
78 | const routes = [
79 | { component: Root,
80 | routes: [
81 | { path: '/',
82 | exact: true,
83 | component: Home
84 | }
85 | ]
86 | }
87 | ];
88 |
89 | return {
90 | Home,
91 | Root,
92 | routes,
93 | mocks: {
94 | mockInitServerAction,
95 | mockLoadDataMapToProps,
96 | mockInitClientAction,
97 | mockParseDataMapToProps,
98 | mockRootAction,
99 | mockHomeAction
100 | }
101 | };
102 | }
103 |
104 | describe('dispatchRouteActions', () => {
105 |
106 | const actions = [[LOAD_DATA, PARSE_DATA]];
107 | const routeComponentPropNames = ['component'];
108 | const actionParams = {};
109 | let location;
110 | let routes, Home, Root, mocks;
111 |
112 | beforeEach(() => {
113 | order = []; // reset
114 | orderedParams = [];
115 |
116 | const init = initRoutes();
117 | routes = init.routes;
118 | mocks = init.mocks;
119 | Home = init.Home;
120 | Root = init.Root;
121 | location = '/';
122 | });
123 |
124 | describe('dispatchRouteActions', () => {
125 | test('resolveRouteComponents', () => {
126 | const branch = matchRoutes(routes, location);
127 | const resolved = resolveRouteComponents(branch, routeComponentPropNames);
128 |
129 | expect(resolved).toHaveLength(2);
130 | expect(resolved[0][0]).toEqual(Root);
131 | expect(resolved[1][0]).toEqual(Home);
132 | });
133 |
134 | test('resolveActionSets - flat', () => {
135 | const match0 = {match: '0'};
136 | const match1 = {match: '1'};
137 |
138 | const routeComponents = [
139 | [Root, match0],
140 | [Home, match1]
141 | ];
142 |
143 | const actionSets = resolveActionSets(routeComponents, actions);
144 |
145 | expect(actionSets).toHaveLength(2);
146 | expect(actionSets[0].routeActions).toHaveLength(1);
147 | expect(actionSets[1].routeActions).toHaveLength(1);
148 |
149 | expect(actionSets[0].routeActions[0][0]).toEqual(Root.primary);
150 | expect(actionSets[0].routeActions[0][1]).toEqual(match0);
151 |
152 | expect(actionSets[1].routeActions[0][0]).toEqual(Home.secondary);
153 | expect(actionSets[1].routeActions[0][1]).toEqual(match1);
154 | });
155 |
156 | test('resolveActionSets - serial', () => {
157 | const match0 = {match: '0'};
158 | const match1 = {match: '1'};
159 |
160 | const routeComponents = [
161 | [Root, match0],
162 | [Home, match1]
163 | ];
164 |
165 | const actionSets = resolveActionSets(routeComponents, [[LOAD_DATA], [PARSE_DATA]]);
166 | expect(actionSets).toHaveLength(2);
167 |
168 | expect(actionSets[0].routeActions).toHaveLength(1);
169 | expect(actionSets[0].routeActions[0][0]).toEqual(Root.primary);
170 | expect(actionSets[0].routeActions[0][1]).toEqual(match0);
171 |
172 | expect(actionSets[1].routeActions).toHaveLength(1);
173 | expect(actionSets[1].routeActions[0][0]).toEqual(Home.secondary);
174 | expect(actionSets[1].routeActions[0][1]).toEqual(match1);
175 | });
176 |
177 | test('reduceActionSets - parallel', done => {
178 |
179 | jest.setTimeout(2500);
180 |
181 | const mocks = [
182 | jest.fn(() => appendOrder(0)),
183 | jest.fn(() => appendOrder(1)),
184 | jest.fn(() => appendOrder(2))
185 | ];
186 |
187 | const mockFilterFn = jest.fn((params) => params);
188 | const mockInitFn = jest.fn((params) => params);
189 |
190 | const mockMapFns = [
191 | jest.fn((params) => params),
192 | jest.fn((params) => params),
193 | jest.fn((params) => params)
194 | ];
195 |
196 | let inputParams = { hello: 'world' };
197 | const match = {match: '0'};
198 | const location = { pathname: '/' };
199 | const routerCtx = {};
200 | const reduced = reduceActionSets([{
201 | initParams: mockInitFn,
202 | filterParams: mockFilterFn,
203 | actionErrorHandler: e => { throw e; },
204 | actionSuccessHandler: () => null,
205 | stopServerActions: () => false,
206 | routeActions: [
207 | // TODO: Add MAP FNs
208 | [(m, h) => new Promise(resolve => { setTimeout(() => { mocks[0](m, h); resolve(); }, 300) }), match, routerCtx, mockMapFns[0]],
209 | [(m, h) => new Promise(resolve => { setTimeout(() => { mocks[1](m, h); resolve(); }, 200) }), match, routerCtx, mockMapFns[1]],
210 | [(m, h) => new Promise(resolve => { setTimeout(() => { mocks[2](m, h); resolve(); }, 100) }), match, routerCtx, mockMapFns[2]]
211 | ]
212 | }], location, inputParams);
213 |
214 | reduced.then((outputParams) => {
215 | // verify output
216 | expect(outputParams).toEqual(defaultActionParams);
217 |
218 | // verify mocks
219 | expect(mockFilterFn.mock.calls).toHaveLength(1);
220 | expect(mockFilterFn.mock.calls[0][0]).toEqual(defaultActionParams);
221 |
222 | expect(mockInitFn.mock.calls).toHaveLength(1);
223 | expect(mockInitFn.mock.calls[0][0]).toEqual(defaultActionParams);
224 |
225 | expect(mocks[0].mock.calls).toHaveLength(1);
226 | expect(mocks[0].mock.calls[0][0]).toEqual({ ...defaultActionParams, ...inputParams, location, match });
227 |
228 | expect(mockMapFns[0].mock.calls).toHaveLength(1);
229 | expect(mockMapFns[0].mock.calls[0][0]).toEqual(inputParams);
230 |
231 | expect(mockMapFns[1].mock.calls).toHaveLength(1);
232 | expect(mockMapFns[1].mock.calls[0][0]).toEqual(inputParams);
233 |
234 | expect(mocks[1].mock.calls).toHaveLength(1);
235 | expect(mocks[1].mock.calls[0][0]).toEqual({ ...defaultActionParams, ...inputParams, location, match });
236 |
237 | expect(mockMapFns[2].mock.calls).toHaveLength(1);
238 | expect(mockMapFns[2].mock.calls[0][0]).toEqual(inputParams);
239 |
240 | expect(mocks[2].mock.calls).toHaveLength(1);
241 | expect(mocks[2].mock.calls[0][0]).toEqual({ ...defaultActionParams, ...inputParams, location, match });
242 |
243 | // verify order
244 | expect(order).toEqual([2,1,0]);
245 | done();
246 | });
247 | });
248 |
249 | test('reduceActionSets - serial', done => {
250 |
251 | jest.setTimeout(2500);
252 |
253 | const mockActionFns = [
254 | jest.fn(() => appendOrder(0)),
255 | jest.fn(() => appendOrder(1)),
256 | jest.fn(() => appendOrder(2))
257 | ];
258 |
259 | const mockFilterFns = [
260 | jest.fn((params) => params),
261 | jest.fn((params) => params),
262 | jest.fn((params) => params)
263 | ];
264 |
265 | const mockInitFns = [
266 | jest.fn((params) => params),
267 | jest.fn((params) => params),
268 | jest.fn((params) => params)
269 | ];
270 |
271 | const mockMapFns = [
272 | jest.fn((params) => params),
273 | jest.fn((params) => params),
274 | jest.fn((params) => params)
275 | ];
276 |
277 | let inputParams = { hello: 'world' };
278 | const match = {match: '0'};
279 | const location = { pathname: '/' };
280 | const routerCtx = {};
281 | const reduced = reduceActionSets([{
282 | initParams: mockInitFns[0],
283 | filterParams: mockFilterFns[0],
284 | actionErrorHandler: e => { throw e; },
285 | actionSuccessHandler: () => null,
286 | stopServerActions: () => false,
287 | routeActions: [
288 | [(m, h) => new Promise(resolve => { setTimeout(() => { mockActionFns[0](m, h); resolve(); }, 300) }), match, routerCtx, mockMapFns[0]]
289 | ]
290 | }, {
291 | initParams: mockInitFns[1],
292 | filterParams: mockFilterFns[1],
293 | actionErrorHandler: e => { throw e; },
294 | actionSuccessHandler: () => null,
295 | stopServerActions: () => false,
296 | routeActions: [
297 | [(m, h) => new Promise(resolve => { setTimeout(() => { mockActionFns[1](m, h); resolve(); }, 200) }), match, routerCtx, mockMapFns[1]]
298 | ]
299 | }, {
300 | initParams: mockInitFns[2],
301 | filterParams: mockFilterFns[2],
302 | actionErrorHandler: e => { throw e; },
303 | actionSuccessHandler: () => null,
304 | stopServerActions: () => false,
305 | routeActions: [
306 | [(m, h) => new Promise(resolve => { setTimeout(() => { mockActionFns[2](m, h); resolve(); }, 100) }), match, routerCtx, mockMapFns[2]]
307 | ]
308 | }], location, inputParams);
309 |
310 | reduced.then((outputParams) => {
311 | // verify output
312 | expect(outputParams).toEqual(defaultActionParams);
313 |
314 | // verify mocks
315 | expect(mockActionFns[0].mock.calls).toHaveLength(1);
316 | expect(mockActionFns[0].mock.calls[0][0]).toEqual({ ...defaultActionParams, ...inputParams, location, match });
317 |
318 | expect(mockFilterFns[0].mock.calls).toHaveLength(1);
319 | expect(mockFilterFns[0].mock.calls[0][0]).toEqual(defaultActionParams);
320 |
321 | expect(mockInitFns[0].mock.calls).toHaveLength(1);
322 | expect(mockInitFns[0].mock.calls[0][0]).toEqual(defaultActionParams);
323 |
324 | expect(mockMapFns[0].mock.calls).toHaveLength(1);
325 | expect(mockMapFns[0].mock.calls[0][0]).toEqual(inputParams);
326 |
327 | expect(mockActionFns[1].mock.calls).toHaveLength(1);
328 | expect(mockActionFns[1].mock.calls[0][0]).toEqual({ ...defaultActionParams, ...inputParams, location, match });
329 |
330 | expect(mockFilterFns[1].mock.calls).toHaveLength(1);
331 | expect(mockFilterFns[1].mock.calls[0][0]).toEqual(defaultActionParams);
332 |
333 | expect(mockInitFns[1].mock.calls).toHaveLength(1);
334 | expect(mockInitFns[1].mock.calls[0][0]).toEqual(defaultActionParams);
335 |
336 | expect(mockMapFns[1].mock.calls).toHaveLength(1);
337 | expect(mockMapFns[1].mock.calls[0][0]).toEqual(inputParams);
338 |
339 | expect(mockActionFns[2].mock.calls).toHaveLength(1);
340 | expect(mockActionFns[2].mock.calls[0][0]).toEqual({ ...defaultActionParams, ...inputParams, location, match });
341 |
342 | expect(mockFilterFns[2].mock.calls).toHaveLength(1);
343 | expect(mockFilterFns[2].mock.calls[0][0]).toEqual(defaultActionParams);
344 |
345 | expect(mockInitFns[2].mock.calls).toHaveLength(1);
346 | expect(mockInitFns[2].mock.calls[0][0]).toEqual(defaultActionParams);
347 |
348 | expect(mockMapFns[2].mock.calls).toHaveLength(1);
349 | expect(mockMapFns[2].mock.calls[0][0]).toEqual(inputParams);
350 |
351 | // verify order
352 | expect(order).toEqual([0,1,2]);
353 | done();
354 | });
355 | });
356 |
357 | test('reduceActionSets - serial with stopServerAction should prevent action invocations', done => {
358 |
359 | jest.setTimeout(2500);
360 |
361 | const mockActionFns = [
362 | jest.fn(() => appendOrder(0)),
363 | jest.fn(() => appendOrder(1)),
364 | jest.fn(() => appendOrder(2))
365 | ];
366 |
367 | const mockInitFns = [
368 | jest.fn((params) => params),
369 | jest.fn((params) => params),
370 | jest.fn((params) => params)
371 | ];
372 |
373 | const mockFilterFns = [
374 | jest.fn((params) => params),
375 | jest.fn((params) => params),
376 | jest.fn((params) => params)
377 | ];
378 |
379 | const mockMapFns = [
380 | jest.fn((params) => params),
381 | jest.fn((params) => params),
382 | jest.fn((params) => params)
383 | ];
384 |
385 | const mockStopServerActions = jest.fn();
386 | let inputParams = { hello: 'world' };
387 | const match = {match: '0'};
388 | const location = { pathname: '/' };
389 | const routerCtx = {};
390 | const reduced = reduceActionSets([{
391 | initParams: mockInitFns[0],
392 | filterParams: mockFilterFns[0],
393 | actionErrorHandler: e => { throw e; },
394 | actionSuccessHandler: () => null,
395 | stopServerActions: () => false,
396 | routeActions: [
397 | [(m, h) => new Promise(resolve => { setTimeout(() => { mockActionFns[0](m, h); resolve(); }, 300) }), match, routerCtx, mockMapFns[0]]
398 | ]
399 | }, {
400 | initParams: mockInitFns[1],
401 | filterParams: mockFilterFns[1],
402 | actionErrorHandler: e => { throw e; },
403 | actionSuccessHandler: () => null,
404 | stopServerActions: () => true,
405 | routeActions: [
406 | [(m, h) => new Promise(resolve => { setTimeout(() => { mockActionFns[1](m, h); resolve(); }, 200) }), match, routerCtx, mockMapFns[1]]
407 | ]
408 | }, {
409 | initParams: mockInitFns[2],
410 | filterParams: mockFilterFns[2],
411 | actionErrorHandler: e => { throw e; },
412 | actionSuccessHandler: () => null,
413 | stopServerActions: mockStopServerActions,
414 | routeActions: [
415 | [(m, h) => new Promise(resolve => { setTimeout(() => { mockActionFns[2](m, h); resolve(); }, 100) }), match, routerCtx, mockMapFns[2]]
416 | ]
417 | }], location, inputParams);
418 |
419 | reduced.then((outputParams) => {
420 | // verify output
421 | expect(outputParams).toEqual(defaultActionParams);
422 |
423 | // verify mocks
424 | expect(mockActionFns[0].mock.calls).toHaveLength(1);
425 | expect(mockActionFns[0].mock.calls[0][0]).toEqual({ ...defaultActionParams, ...inputParams, location, match });
426 |
427 | expect(mockFilterFns[0].mock.calls).toHaveLength(1);
428 | expect(mockFilterFns[0].mock.calls[0][0]).toEqual(defaultActionParams);
429 |
430 | expect(mockInitFns[0].mock.calls).toHaveLength(1);
431 | expect(mockInitFns[0].mock.calls[0][0]).toEqual(defaultActionParams);
432 |
433 | expect(mockMapFns[0].mock.calls).toHaveLength(1);
434 |
435 | expect(mockActionFns[1].mock.calls).toHaveLength(1);
436 | expect(mockActionFns[1].mock.calls[0][0]).toEqual({ ...defaultActionParams, ...inputParams, location, match });
437 |
438 | expect(mockFilterFns[1].mock.calls).toHaveLength(1);
439 | expect(mockFilterFns[1].mock.calls[0][0]).toEqual(defaultActionParams);
440 |
441 | expect(mockInitFns[1].mock.calls).toHaveLength(1);
442 | expect(mockInitFns[1].mock.calls[0][0]).toEqual(defaultActionParams);
443 |
444 | expect(mockMapFns[1].mock.calls).toHaveLength(1);
445 |
446 | // The last actionSet should NOT be invoked
447 | expect(mockActionFns[2].mock.calls).toHaveLength(0);
448 | expect(mockInitFns[2].mock.calls).toHaveLength(0);
449 | expect(mockFilterFns[2].mock.calls).toHaveLength(0);
450 | expect(mockMapFns[2].mock.calls).toHaveLength(0);
451 |
452 | expect(mockStopServerActions.mock.calls).toHaveLength(0);
453 |
454 | // verify order
455 | expect(order).toEqual([0,1]);
456 | done();
457 | });
458 | });
459 |
460 | test('returns promise when no routes matched', done => {
461 | const { mockHomeAction, mockRootAction } = mocks;
462 | const p = dispatchRouteActions(
463 | { pathname: '/helloworld' },
464 | actions,
465 | { routes: [], routeComponentPropNames },
466 | actionParams);
467 |
468 | p.then(() => {
469 | expect(mockHomeAction.mock.calls).toHaveLength(0);
470 | expect(mockRootAction.mock.calls).toHaveLength(0);
471 | done();
472 | });
473 | });
474 |
475 | test('returns promise when routes matched - dispatchAction function', done => {
476 | const { mockHomeAction, mockRootAction } = mocks;
477 | const p = dispatchRouteActions(
478 | { pathname: '/' },
479 | () => [[LOAD_DATA, PARSE_DATA]],
480 | { routes, routeComponentPropNames },
481 | actionParams);
482 |
483 | p.then(() => {
484 | expect(mockHomeAction.mock.calls).toHaveLength(1);
485 | expect(mockRootAction.mock.calls).toHaveLength(1);
486 | expect(order).toEqual([0, 1]);
487 |
488 | // verify props
489 | expect(orderedParams[0][0].location).toBeDefined();
490 | expect(orderedParams[0][0].match).toBeDefined();
491 | expect(orderedParams[0][0].httpResponse).toBeDefined();
492 |
493 | // verify route params
494 | expect(orderedParams[0][1].route).toBeDefined();
495 | expect(orderedParams[0][1].routeComponentKey).toBeDefined();
496 | done();
497 | });
498 | });
499 |
500 | test('returns promise when routes matched - flat', done => {
501 | const { mockHomeAction, mockRootAction } = mocks;
502 | const p = dispatchRouteActions(
503 | { pathname: '/' },
504 | actions,
505 | { routes, routeComponentPropNames },
506 | actionParams);
507 |
508 | p.then(() => {
509 | expect(mockHomeAction.mock.calls).toHaveLength(1);
510 | expect(mockRootAction.mock.calls).toHaveLength(1);
511 | expect(order).toEqual([0, 1]);
512 | done();
513 | });
514 | });
515 |
516 | test('returns promise when routes matched - serial', done => {
517 | const { mockHomeAction, mockRootAction } = mocks;
518 | const p = dispatchRouteActions(
519 | { pathname: '/' },
520 | [[LOAD_DATA], [PARSE_DATA]],
521 | { routes, routeComponentPropNames },
522 | actionParams);
523 |
524 | p.then(() => {
525 | expect(mockHomeAction.mock.calls).toHaveLength(1);
526 | expect(mockRootAction.mock.calls).toHaveLength(1);
527 | expect(order).toEqual([0, 1]);
528 | done();
529 | });
530 | });
531 |
532 | test('dispatchServerActions does not invoke client actions', done => {
533 | const {
534 | mockHomeAction,
535 | mockRootAction,
536 | mockInitServerAction,
537 | mockLoadDataMapToProps,
538 | mockInitClientAction,
539 | mockParseDataMapToProps
540 | } = mocks;
541 |
542 | const p = dispatchServerActions(
543 | { pathname: '/' },
544 | [[LOAD_DATA], [PARSE_DATA]],
545 | { routes, routeComponentPropNames },
546 | actionParams);
547 |
548 | p.then(() => {
549 | expect(mockRootAction.mock.calls).toHaveLength(1);
550 | expect(mockHomeAction.mock.calls).toHaveLength(0);
551 | expect(order).toEqual([0]);
552 |
553 | // Verify action mocks
554 | expect(mockInitServerAction.mock.calls).toHaveLength(1);
555 | expect(mockLoadDataMapToProps.mock.calls).toHaveLength(1);
556 | expect(mockInitClientAction.mock.calls).toHaveLength(0);
557 | expect(mockParseDataMapToProps.mock.calls).toHaveLength(0);
558 |
559 | done();
560 | });
561 | });
562 |
563 | test('dispatchComponentActions does not invoke mapper or init functions', done => {
564 | const {
565 | mockHomeAction,
566 | mockRootAction,
567 | mockInitServerAction,
568 | mockLoadDataMapToProps,
569 | mockInitClientAction,
570 | mockParseDataMapToProps
571 | } = mocks;
572 |
573 | const p = dispatchComponentActions(
574 | { pathname: '/' },
575 | [[LOAD_DATA], [PARSE_DATA]],
576 | { routes, routeComponentPropNames },
577 | actionParams);
578 |
579 | p.then(() => {
580 | expect(mockRootAction.mock.calls).toHaveLength(1);
581 | expect(mockHomeAction.mock.calls).toHaveLength(1);
582 | expect(order).toEqual([0, 1]);
583 |
584 | // Verify action mocks
585 | expect(mockInitServerAction.mock.calls).toHaveLength(0);
586 | expect(mockLoadDataMapToProps.mock.calls).toHaveLength(0);
587 | expect(mockInitClientAction.mock.calls).toHaveLength(0);
588 | expect(mockParseDataMapToProps.mock.calls).toHaveLength(0);
589 |
590 | done();
591 | });
592 | });
593 |
594 | test('dispatchClientActions does not invoke server actions', () => {
595 |
596 | // Custom init for client dispatcher tests
597 | const { routes, mocks } = initRoutes({
598 | mockInitClientAction: jest.fn(p => ({ ...p, clientData: {} })),
599 | mockHomeAction: jest.fn((actionParams/*, routerCtx*/) => {
600 | actionParams.clientData.value = 1
601 | })
602 | });
603 | const {
604 | mockHomeAction,
605 | mockRootAction,
606 | mockInitServerAction,
607 | mockLoadDataMapToProps,
608 | mockInitClientAction,
609 | mockParseDataMapToProps
610 | } = mocks;
611 |
612 | const props = dispatchClientActions(
613 | { pathname: '/' },
614 | [[LOAD_DATA], [PARSE_DATA]],
615 | { routes, routeComponentPropNames },
616 | actionParams);
617 |
618 | expect(mockRootAction.mock.calls).toHaveLength(0);
619 | expect(mockHomeAction.mock.calls).toHaveLength(1);
620 |
621 | // Verify action mocks
622 | expect(mockInitClientAction.mock.calls).toHaveLength(1);
623 | expect(mockParseDataMapToProps.mock.calls).toHaveLength(1);
624 | expect(mockInitServerAction.mock.calls).toHaveLength(0);
625 | expect(mockLoadDataMapToProps.mock.calls).toHaveLength(0);
626 |
627 | expect(props).toEqual({ clientData: { value: 1 }, httpResponse: { statusCode: 200 } });
628 | });
629 | });
630 |
631 | describe('parseDispatchActions', () => {
632 | test('convert single action to action set', () => {
633 | expect(parseDispatchActions('redir')).toEqual([['redir']]);
634 | });
635 |
636 | test('convert single action array to action set', () => {
637 | expect(parseDispatchActions(['redir'])).toEqual([['redir']]);
638 | });
639 |
640 | test('convert multiple action array to action set', () => {
641 | expect(parseDispatchActions(['redir', 'status'])).toEqual([['redir', 'status']]);
642 | });
643 |
644 | test('returns single action set as action set', () => {
645 | expect(parseDispatchActions([['redir']])).toEqual([['redir']]);
646 | });
647 |
648 | test('returns action set', () => {
649 | expect(parseDispatchActions([['redir', 'status']])).toEqual([['redir', 'status']]);
650 | });
651 |
652 | test('converts combination of actions to action sets (1a)', () => {
653 | expect(parseDispatchActions([
654 | 'first',
655 | ['second'],
656 | ['third', 'fourth']
657 | ])).toEqual([
658 | ['first'],
659 | ['second'],
660 | ['third', 'fourth']
661 | ]);
662 | });
663 |
664 | test('converts combination of actions to action sets (2a)', () => {
665 | expect(parseDispatchActions([
666 | ['first', 'second'],
667 | 'third',
668 | ['fourth']
669 | ])).toEqual([
670 | ['first', 'second'],
671 | ['third'],
672 | ['fourth']
673 | ]);
674 | });
675 |
676 | test('converts combination of actions to action sets (3a)', () => {
677 | expect(parseDispatchActions([
678 | ['first', 'second'],
679 | ['third'],
680 | 'fourth'
681 | ])).toEqual([
682 | ['first', 'second'],
683 | ['third'],
684 | ['fourth']
685 | ]);
686 | });
687 |
688 | test('converts combination of actions to action sets (4a)', () => {
689 | expect(parseDispatchActions([
690 | ['first'],
691 | ['second', 'third'],
692 | 'fourth'
693 | ])).toEqual([
694 | ['first'],
695 | ['second', 'third'],
696 | ['fourth']
697 | ]);
698 | });
699 | });
700 | });
701 |
--------------------------------------------------------------------------------
/src/__tests__/enzyme.js:
--------------------------------------------------------------------------------
1 | import { render, shallow, mount, ShallowWrapper, ReactWrapper, configure, EnzymeAdapter } from 'enzyme';
2 | import Adapter from 'enzyme-adapter-react-16';
3 | configure({ adapter: new Adapter() });
4 |
5 | export {
6 | render,
7 | shallow,
8 | mount,
9 | ShallowWrapper,
10 | ReactWrapper,
11 | configure,
12 | EnzymeAdapter
13 | };
14 |
--------------------------------------------------------------------------------
/src/__tests__/withActions.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import withActions from '../withActions';
3 | import { mount } from './enzyme';
4 |
5 | class TestComponent extends React.Component {
6 |
7 | static redundantMethod() {
8 | return '';
9 | }
10 |
11 | render() {
12 | return test
;
13 | }
14 | }
15 |
16 | describe('withActions', () => {
17 |
18 | beforeEach(() => {
19 | });
20 |
21 | afterEach(() => {
22 | });
23 |
24 | test('validates action', () => {
25 | expect(() => withActions({})).toThrow(/mapParamsToProps/);
26 | expect(() => withActions(false)).toThrow(/mapParamsToProps/);
27 |
28 | expect(() => withActions(null, {})).toThrow(/name/);
29 | expect(() => withActions(null, { name: 1 })).toThrow(/name/);
30 |
31 | expect(() => withActions(null, { name: 'n' })).toThrow(/staticMethodName/);
32 | expect(() => withActions(null, { name: 'n', staticMethodName: 1 })).toThrow(/staticMethodName/);
33 |
34 | expect(() => withActions(null, {
35 | name: 'n',
36 | staticMethodName: 's',
37 | filterParamsToProps: false
38 | })).toThrow(/filterParamsToProps/);
39 |
40 | expect(() => withActions(null, {
41 | name: 'n',
42 | staticMethodName: 's',
43 | })).toThrow(/filterParamsToProps/);
44 |
45 | expect(() => withActions(null, {
46 | name: 'n',
47 | staticMethodName: 's',
48 | filterParamsToProps: () => {},
49 | initServerAction: 1
50 | })).toThrow(/initServerAction/);
51 |
52 | expect(() => withActions(null, {
53 | name: 'n',
54 | staticMethodName: 's',
55 | filterParamsToProps: () => {},
56 | initClientAction: 1
57 | })).toThrow(/initClientAction/);
58 |
59 | expect(() => withActions(null, {
60 | name: 'n',
61 | staticMethodName: 's',
62 | filterParamsToProps: () => {},
63 | hoc: 1
64 | })).toThrow(/hoc/);
65 | });
66 |
67 | test('throws when component has not defined the static method required by the action', () => {
68 | expect(() => withActions(null, {
69 | name: 'n',
70 | staticMethodName: 's',
71 | filterParamsToProps: () => {},
72 | })(TestComponent)).toThrow(/missing the required static/);
73 | });
74 |
75 | test('does not throw when action defines static method', () => {
76 | expect(() => withActions(null, {
77 | name: 'n',
78 | staticMethod: () => {},
79 | staticMethodName: 'redundantMethod',
80 | filterParamsToProps: () => {},
81 | })(TestComponent)).not.toThrow();
82 | });
83 |
84 | describe('getDispatcherActions', () => {
85 | let actionComponent;
86 | beforeAll(() => {
87 | actionComponent = withActions(null, {
88 | name: 'action1',
89 | staticMethod: () => {},
90 | staticMethodName: 'method1',
91 | filterParamsToProps: () => {},
92 | }, {
93 | name: 'action2',
94 | staticMethod: () => {},
95 | initServerAction: p => p,
96 | staticMethodName: 'method1',
97 | filterParamsToProps: () => {},
98 | }, {
99 | name: 'action3',
100 | initClientAction: p => p,
101 | staticMethod: () => {},
102 | staticMethodName: 'method1',
103 | filterParamsToProps: () => {},
104 | })(TestComponent);
105 | });
106 |
107 | test('returns all assigned methods', () => {
108 | expect(
109 | actionComponent.getDispatcherActions().map(a => a.name))
110 | .toEqual(['action1', 'action2', 'action3']);
111 | });
112 |
113 | test('returns filtered action names', () => {
114 | expect(
115 | actionComponent.getDispatcherActions(['action1', 'action3']).map(a => a.name))
116 | .toEqual(['action1', 'action3']);
117 | });
118 |
119 | test('returns filtered actions', () => {
120 | expect(
121 | actionComponent
122 | .getDispatcherActions(null, a => typeof a.initServerAction === 'function')
123 | .map(a => a.name))
124 | .toEqual(['action2']);
125 | });
126 |
127 | test('applies multiple withActions() actions to component', () => {
128 | actionComponent = withActions(null, {
129 | name: 'action1',
130 | staticMethod: () => {},
131 | staticMethodName: 'method1',
132 | filterParamsToProps: () => {},
133 | })(TestComponent);
134 | actionComponent = withActions(null, {
135 | name: 'action2',
136 | staticMethod: () => {},
137 | initServerAction: p => p,
138 | staticMethodName: 'method2',
139 | filterParamsToProps: () => {},
140 | })(actionComponent);
141 | actionComponent = withActions(null, {
142 | name: 'action3',
143 | initClientAction: p => p,
144 | staticMethod: () => {},
145 | staticMethodName: 'method3',
146 | filterParamsToProps: () => {},
147 | })(actionComponent);
148 |
149 | expect(
150 | actionComponent.getDispatcherActions().map(a => a.name))
151 | .toEqual(['action1', 'action2', 'action3']);
152 | });
153 |
154 | test('applies withActions() to a null component', () => {
155 | actionComponent = withActions(null, {
156 | name: 'action1',
157 | staticMethod: () => {},
158 | filterParamsToProps: () => {},
159 | })(null);
160 |
161 | expect(
162 | actionComponent.getDispatcherActions().map(a => a.name))
163 | .toEqual(['action1']);
164 | });
165 | });
166 |
167 | test('applies action HOC', () => {
168 | const HOC = (actionName) => (Component) => {
169 | const wrapped = (props) => (
170 |
171 | {actionName}
172 |
173 |
174 | );
175 | wrapped.displayName = 'wrapped';
176 | return wrapped;
177 | };
178 |
179 | const ActionComponent = withActions(null, {
180 | name: 'action1',
181 | staticMethod: () => {},
182 | filterParamsToProps: () => {},
183 | hoc: HOC('action1')
184 | }, {
185 | name: 'action2',
186 | staticMethod: () => {},
187 | filterParamsToProps: () => {},
188 | hoc: HOC('action2')
189 | }, {
190 | name: 'action3',
191 | staticMethod: () => {},
192 | filterParamsToProps: () => {},
193 | hoc: HOC('action3')
194 | })(TestComponent);
195 |
196 | const wrapper = mount(Hello World);
197 | expect(
198 | wrapper.html())
199 | .toBe('');
200 | });
201 | });
202 |
--------------------------------------------------------------------------------
/src/createRouteDispatchers.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react';
3 | import { parsePath } from 'history'
4 | import hoistNonReactStatic from 'hoist-non-react-statics';
5 | import reactDisplayName from 'react-display-name';
6 | import invariant from 'invariant';
7 | import warning from 'warning';
8 | import RouteDispatcher, { DEFAULT_COMPONENT_PROP_NAMES } from './RouteDispatcher';
9 | import { getRouteComponents } from './dispatchRouteActions';
10 |
11 | const __DEV__ = process.env.NODE_ENV !== 'production';
12 |
13 | function RouteDispatcherHoc(displayNamePrefix, routeConfig, options) {
14 | const routerDispatcher = ({ routes, ...props }) => {
15 | return (
16 | );
21 | };
22 |
23 | routerDispatcher.displayName = `${displayNamePrefix}(${reactDisplayName(RouteDispatcher)})`;
24 |
25 | routerDispatcher.propTypes = RouteDispatcher.propTypes;
26 |
27 | return hoistNonReactStatic(routerDispatcher, RouteDispatcher);
28 | }
29 |
30 | function mergeActions(flatActions, actionNames) {
31 | return flatActions.reduce((actions, actionName) => {
32 | if (actions.indexOf(actionName) === -1) {
33 | actions.push(actionName);
34 | }
35 |
36 | return actions;
37 | }, actionNames);
38 | }
39 |
40 | function getActionNames(actions) {
41 | return actions.map(action => {
42 | if (Array.isArray(action)) {
43 | return getActionNames(action);
44 | }
45 |
46 | if (typeof action.name === 'string') {
47 | return action.name;
48 | }
49 |
50 | invariant(false, `invalid dispatcher action 'name', expected string but encountered ${action.name}`);
51 | });
52 | }
53 |
54 | function findRouteActions(routes, routeComponentPropNames, actionNames = []) {
55 | routes.forEach(route => {
56 | getRouteComponents(route, routeComponentPropNames).forEach(({ routeComponent }) => {
57 | if (typeof routeComponent.getDispatcherActions === 'function') {
58 | mergeActions(getActionNames(routeComponent.getDispatcherActions()), actionNames);
59 | }
60 | });
61 |
62 | if (Array.isArray(route.routes)) {
63 | findRouteActions(route.routes, routeComponentPropNames, actionNames);
64 | }
65 | });
66 |
67 | return actionNames;
68 | }
69 |
70 | function flattenActions(actions) {
71 | if (!Array.isArray(actions)) {
72 | return [actions];
73 | }
74 |
75 | return actions.reduce((flatActions, action) => {
76 | if (Array.isArray(action)) {
77 | Array.prototype.push.apply(flatActions, flattenActions(action));
78 | }
79 | else {
80 | flatActions.push(action);
81 | }
82 |
83 | return flatActions;
84 | }, []);
85 | }
86 |
87 | function createDispatchAction(dispatchFuncName, pathAndQuery, params, options) {
88 | invariant(typeof pathAndQuery === 'string', 'pathAnyQuery expects a string');
89 |
90 | const { actionNames, routes, routeComponentPropNames } = options;
91 | return RouteDispatcher[dispatchFuncName](
92 | parsePath(pathAndQuery),
93 | actionNames,
94 | { routes, routeComponentPropNames },
95 | Object.assign({}, params));
96 | }
97 |
98 | // use a factory method to simplify server usage
99 | export default function createRouteDispatchers(routeConfig, actionNames, options = {}) {
100 | invariant(Array.isArray(routeConfig), 'routeConfig expects an array of routes');
101 |
102 | const dispatchOpts = Object.assign(
103 | { routeComponentPropNames: DEFAULT_COMPONENT_PROP_NAMES },
104 | options,
105 | { actionNames: actionNames, routes: routeConfig });
106 |
107 | const { routes, ...componentOptions } = dispatchOpts;
108 |
109 | // If no actions are configured, determine actions from component configuration
110 | if (typeof dispatchOpts.actionNames === 'undefined' || dispatchOpts.actionNames === null) {
111 | dispatchOpts.actionNames = findRouteActions(routes, dispatchOpts.routeComponentPropNames);
112 | }
113 | else if (__DEV__) {
114 | const configuredActionNames = flattenActions(dispatchOpts.actionNames);
115 | const routeActionNames = findRouteActions(routes, dispatchOpts.routeComponentPropNames);
116 |
117 | const unconfiguredActions = routeActionNames.filter(actionName => configuredActionNames.indexOf(actionName) === -1);
118 | const unusedActions = configuredActionNames.filter(actionName => routeActionNames.indexOf(actionName) === -1);
119 | warning(unconfiguredActions.length === 0, `The actions '${unconfiguredActions.join(', ')}' are used by route components, but are not configured for use by the route dispatcher.`);
120 | warning(unusedActions.length === 0, `The actions '${unusedActions.join(', ')}' are configured for use with the route dispatcher, but no route components have the action(s) applied.`);
121 | }
122 |
123 | return {
124 |
125 | /**
126 | * The configured action name(s). Useful for debugging purposes.
127 | */
128 | actionNames: dispatchOpts.actionNames.slice(),
129 |
130 | /**
131 | * dispatch route actions on the server.
132 | *
133 | * @param pathAndQuery string the requested url path and query
134 | * @param params Object params for actions
135 | * @param options [Object] options for server dispatching
136 | * @returns {*} Components for rendering routes
137 | */
138 | dispatchServerActions: (pathAndQuery, params = {}, options = {}) =>
139 | createDispatchAction('dispatchServerActions', pathAndQuery, params, { ...dispatchOpts, ...options }),
140 |
141 | /**
142 | * Synchronous client dispatcher
143 | *
144 | * @param pathAndQuery
145 | * @param params
146 | * @param options
147 | */
148 | dispatchClientActions: (pathAndQuery, params = {}, options = {}) =>
149 | createDispatchAction('dispatchClientActions', pathAndQuery, params, { ...dispatchOpts, ...options }),
150 |
151 | ClientRouteDispatcher: RouteDispatcherHoc(
152 | 'ClientRouteDispatcher',
153 | routes,
154 | componentOptions),
155 |
156 | UniversalRouteDispatcher: RouteDispatcherHoc(
157 | 'UniversalRouteDispatcher',
158 | routes,
159 | { ...componentOptions, dispatchActionsOnFirstRender: false })
160 | };
161 | }
162 |
--------------------------------------------------------------------------------
/src/defineRoutes.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | /**
4 | * Define react-router-config routes, and assigns a key to each route.
5 | *
6 | * @param routes
7 | * @param idx
8 | * @returns {Array} Routes with a key value assigned to each route
9 | */
10 | export default function defineRoutes(routes: Array