navigation-stack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.babelrc.cjs +17 -0
  2. package/.eslintignore +8 -0
  3. package/.eslintrc.cjs +10 -0
  4. package/.github/workflows/main.yml +39 -0
  5. package/.yarn/install-state.gz +0 -0
  6. package/.yarnrc.yml +1 -0
  7. package/CODE_OF_CONDUCT.md +77 -0
  8. package/LICENSE +21 -0
  9. package/README.md +249 -0
  10. package/codecov.yml +1 -0
  11. package/karma.conf.cjs +63 -0
  12. package/lib/cjs/ActionTypes.js +14 -0
  13. package/lib/cjs/Actions.js +27 -0
  14. package/lib/cjs/LocationStateStorage.js +60 -0
  15. package/lib/cjs/addNavigationBlocker.js +7 -0
  16. package/lib/cjs/basePath.js +58 -0
  17. package/lib/cjs/createMiddlewares.js +43 -0
  18. package/lib/cjs/createSearchFromQuery.js +13 -0
  19. package/lib/cjs/environment/BrowserEnvironment.js +111 -0
  20. package/lib/cjs/environment/MemoryEnvironment.js +150 -0
  21. package/lib/cjs/environment/ServerEnvironment.js +53 -0
  22. package/lib/cjs/getLocationUrl.js +20 -0
  23. package/lib/cjs/index.js +30 -0
  24. package/lib/cjs/isPromise.js +9 -0
  25. package/lib/cjs/locationReducer.js +13 -0
  26. package/lib/cjs/middleware/createBasePathMiddleware.js +24 -0
  27. package/lib/cjs/middleware/createEnvironmentMiddleware.js +58 -0
  28. package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +128 -0
  29. package/lib/cjs/middleware/createTransformLocationMiddleware.js +38 -0
  30. package/lib/cjs/middleware/navigationActionMiddleware.js +37 -0
  31. package/lib/cjs/middleware/normalizeInputLocationMiddleware.js +27 -0
  32. package/lib/cjs/navigationBlockers.js +146 -0
  33. package/lib/cjs/normalizeInputLocation.js +46 -0
  34. package/lib/cjs/onlyAllowedOnClientSide.js +10 -0
  35. package/lib/cjs/parseLocationUrl.js +39 -0
  36. package/lib/cjs/parseQueryFromSearch.js +16 -0
  37. package/lib/esm/ActionTypes.js +9 -0
  38. package/lib/esm/Actions.js +21 -0
  39. package/lib/esm/LocationStateStorage.js +53 -0
  40. package/lib/esm/addNavigationBlocker.js +2 -0
  41. package/lib/esm/basePath.js +53 -0
  42. package/lib/esm/createMiddlewares.js +37 -0
  43. package/lib/esm/createSearchFromQuery.js +8 -0
  44. package/lib/esm/environment/BrowserEnvironment.js +104 -0
  45. package/lib/esm/environment/MemoryEnvironment.js +143 -0
  46. package/lib/esm/environment/ServerEnvironment.js +46 -0
  47. package/lib/esm/getLocationUrl.js +15 -0
  48. package/lib/esm/index.js +12 -0
  49. package/lib/esm/isPromise.js +4 -0
  50. package/lib/esm/locationReducer.js +7 -0
  51. package/lib/esm/middleware/createBasePathMiddleware.js +19 -0
  52. package/lib/esm/middleware/createEnvironmentMiddleware.js +52 -0
  53. package/lib/esm/middleware/createNavigationBlockerMiddleware.js +123 -0
  54. package/lib/esm/middleware/createTransformLocationMiddleware.js +33 -0
  55. package/lib/esm/middleware/navigationActionMiddleware.js +32 -0
  56. package/lib/esm/middleware/normalizeInputLocationMiddleware.js +22 -0
  57. package/lib/esm/navigationBlockers.js +138 -0
  58. package/lib/esm/normalizeInputLocation.js +41 -0
  59. package/lib/esm/onlyAllowedOnClientSide.js +5 -0
  60. package/lib/esm/parseLocationUrl.js +33 -0
  61. package/lib/esm/parseQueryFromSearch.js +11 -0
  62. package/lib/index.d.ts +301 -0
  63. package/package.json +100 -0
  64. package/renovate.json +3 -0
  65. package/src/ActionTypes.js +9 -0
  66. package/src/Actions.js +26 -0
  67. package/src/LocationStateStorage.js +59 -0
  68. package/src/addNavigationBlocker.js +2 -0
  69. package/src/basePath.js +65 -0
  70. package/src/createMiddlewares.js +41 -0
  71. package/src/createSearchFromQuery.js +9 -0
  72. package/src/environment/BrowserEnvironment.js +109 -0
  73. package/src/environment/MemoryEnvironment.js +151 -0
  74. package/src/environment/ServerEnvironment.js +54 -0
  75. package/src/getLocationUrl.js +12 -0
  76. package/src/index.js +12 -0
  77. package/src/isPromise.js +8 -0
  78. package/src/locationReducer.js +8 -0
  79. package/src/middleware/createBasePathMiddleware.js +20 -0
  80. package/src/middleware/createEnvironmentMiddleware.js +57 -0
  81. package/src/middleware/createNavigationBlockerMiddleware.js +128 -0
  82. package/src/middleware/createTransformLocationMiddleware.js +29 -0
  83. package/src/middleware/navigationActionMiddleware.js +27 -0
  84. package/src/middleware/normalizeInputLocationMiddleware.js +21 -0
  85. package/src/navigationBlockers.js +158 -0
  86. package/src/normalizeInputLocation.js +44 -0
  87. package/src/onlyAllowedOnClientSide.js +5 -0
  88. package/src/parseLocationUrl.js +40 -0
  89. package/src/parseQueryFromSearch.js +12 -0
  90. package/test/.eslintrc.cjs +17 -0
  91. package/test/Action.test.js +72 -0
  92. package/test/ActionTypes.test.js +13 -0
  93. package/test/LocationStateStorage.test.js +75 -0
  94. package/test/basePath.test.js +158 -0
  95. package/test/createMiddlewares.test.js +62 -0
  96. package/test/environment/BrowserEnvironment.test.js +165 -0
  97. package/test/environment/MemoryEnvironment.test.js +218 -0
  98. package/test/environment/ServerEnvironment.test.js +23 -0
  99. package/test/getLocationUrl.test.js +33 -0
  100. package/test/helpers.js +34 -0
  101. package/test/index.js +44 -0
  102. package/test/index.test.js +20 -0
  103. package/test/locationReducer.test.js +42 -0
  104. package/test/middleware/createBasePathMiddleware.test.js +67 -0
  105. package/test/middleware/createNavigationBlockerMiddleware.test.js +472 -0
  106. package/test/middleware/createTransformLocationMiddleware.test.js +44 -0
  107. package/test/middleware/navigationActionMiddleware.test.js +74 -0
  108. package/test/middleware/normalizeInputLocationMiddleware.test.js +62 -0
  109. package/test/normalizeInputLocation.test.js +81 -0
  110. package/test/parseLocationUrl.test.js +30 -0
  111. package/types/.eslintrc.cjs +3 -0
  112. package/types/index.d.ts +301 -0
  113. package/types/tsconfig.json +14 -0
@@ -0,0 +1,472 @@
1
+ import delay from 'delay';
2
+ import pDefer from 'p-defer';
3
+ import { applyMiddleware, createStore } from 'redux';
4
+
5
+ import Actions from '../../src/Actions';
6
+ import addNavigationBlockerOriginal from '../../src/addNavigationBlocker';
7
+ import createMiddlewares from '../../src/createMiddlewares';
8
+ import MemoryEnvironment from '../../src/environment/MemoryEnvironment';
9
+ import locationReducer from '../../src/locationReducer';
10
+ import { shouldWarn } from '../helpers';
11
+
12
+ describe('createNavigationBlockerMiddleware', () => {
13
+ const sandbox = sinon.createSandbox();
14
+
15
+ let environment;
16
+ let store;
17
+
18
+ function addNavigationBlocker(listener) {
19
+ return addNavigationBlockerOriginal(environment, listener);
20
+ }
21
+
22
+ beforeEach(() => {
23
+ environment = new MemoryEnvironment('/foo');
24
+
25
+ store = createStore(
26
+ locationReducer,
27
+ applyMiddleware(...createMiddlewares(environment)),
28
+ );
29
+ store.dispatch(Actions.init());
30
+
31
+ sinon.spy(environment, 'addBeforeDestroyListener');
32
+ sinon.spy(environment, '_removeBeforeDestroyListener');
33
+ });
34
+
35
+ afterEach(() => {
36
+ store.dispatch(Actions.dispose());
37
+
38
+ sandbox.restore();
39
+ });
40
+
41
+ describe('PUSH navigations', () => {
42
+ it('should block navigation when blocker returns `true`', () => {
43
+ const listener = sinon.stub().returns(true);
44
+ addNavigationBlocker(listener);
45
+
46
+ store.dispatch(Actions.push('/bar'));
47
+ expect(store.getState().pathname).to.equal('/foo');
48
+
49
+ expect(listener.firstCall.args[0]).to.include({
50
+ action: 'PUSH',
51
+ pathname: '/bar',
52
+ });
53
+ });
54
+
55
+ it('should allow navigation when blocker returns `undefined`', () => {
56
+ addNavigationBlocker(() => undefined);
57
+
58
+ store.dispatch(Actions.push('/bar'));
59
+ expect(store.getState().pathname).to.equal('/bar');
60
+ });
61
+
62
+ it("should fall through when first blocker doesn't return `true`", () => {
63
+ const listener1 = sinon.stub().returns(undefined);
64
+ const listener2 = sinon.stub().returns(true);
65
+
66
+ addNavigationBlocker(listener1);
67
+ addNavigationBlocker(listener2);
68
+
69
+ store.dispatch(Actions.push('/bar'));
70
+ expect(store.getState().pathname).to.equal('/foo');
71
+
72
+ expect(listener1).to.have.been.calledOnce();
73
+ expect(listener2).to.have.been.calledOnce();
74
+ });
75
+
76
+ it('should not fall through when first blocker returns `true`', () => {
77
+ const listener1 = sinon.stub().returns(true);
78
+ const listener2 = sinon.stub().returns(undefined);
79
+
80
+ addNavigationBlocker(listener1);
81
+ addNavigationBlocker(listener2);
82
+
83
+ store.dispatch(Actions.push('/bar'));
84
+ expect(store.getState().pathname).to.equal('/foo');
85
+
86
+ expect(listener1).to.have.been.calledOnce();
87
+ expect(listener2).not.to.have.been.called();
88
+ });
89
+
90
+ it('should warn on and ignore listeners that throw', () => {
91
+ shouldWarn(
92
+ 'Ignoring navigation blocker `syncListener` that failed with `Error: foo`.',
93
+ );
94
+
95
+ const syncListener = () => {
96
+ throw new Error('foo');
97
+ };
98
+
99
+ addNavigationBlocker(syncListener);
100
+
101
+ store.dispatch(Actions.push('/bar'));
102
+ expect(store.getState().pathname).to.equal('/bar');
103
+ });
104
+
105
+ // it('should show a confirmation dialog and allow navigation on string', () => {
106
+ // sandbox.stub(window, 'confirm').returns(true);
107
+ //
108
+ // addNavigationBlocker(({ pathname }) => pathname);
109
+ //
110
+ // store.dispatch(Actions.push('/bar'));
111
+ // expect(store.getState().pathname).to.equal('/bar');
112
+ //
113
+ // expect(window.confirm)
114
+ // .to.have.been.calledOnce()
115
+ // .and.to.have.been.called.with('/bar');
116
+ // });
117
+
118
+ // it('should show a confirmation dialog and block navigation on string', () => {
119
+ // sandbox.stub(window, 'confirm').returns(false);
120
+ //
121
+ // addNavigationBlocker(({ pathname }) => pathname);
122
+ //
123
+ // store.dispatch(Actions.push('/bar'));
124
+ // expect(store.getState().pathname).to.equal('/foo');
125
+ //
126
+ // expect(window.confirm)
127
+ // .to.have.been.calledOnce()
128
+ // .and.to.have.been.called.with('/bar');
129
+ // });
130
+
131
+ it('should allow navigation when blocker returns `undefined` (async)', async () => {
132
+ const deferred = pDefer();
133
+ addNavigationBlocker(() => deferred.promise);
134
+
135
+ store.dispatch(Actions.push('/bar'));
136
+ expect(store.getState().pathname).to.equal('/foo');
137
+
138
+ deferred.resolve(undefined);
139
+ await delay(10);
140
+
141
+ expect(store.getState().pathname).to.equal('/bar');
142
+ });
143
+
144
+ it('should block navigation when blocker returns `true` (async)', async () => {
145
+ const deferred = pDefer();
146
+ addNavigationBlocker(() => deferred.promise);
147
+
148
+ store.dispatch(Actions.push('/bar'));
149
+ expect(store.getState().pathname).to.equal('/foo');
150
+
151
+ deferred.resolve(true);
152
+ await delay(10);
153
+
154
+ expect(store.getState().pathname).to.equal('/foo');
155
+ });
156
+
157
+ it('should allow chaining async blockers', async () => {
158
+ const deferred1 = pDefer();
159
+ const deferred2 = pDefer();
160
+
161
+ addNavigationBlocker(() => deferred1.promise);
162
+ addNavigationBlocker(() => deferred2.promise);
163
+
164
+ store.dispatch(Actions.push('/bar'));
165
+ expect(store.getState().pathname).to.equal('/foo');
166
+
167
+ deferred1.resolve(undefined);
168
+ await delay(10);
169
+
170
+ expect(store.getState().pathname).to.equal('/foo');
171
+
172
+ deferred2.resolve(undefined);
173
+ await delay(10);
174
+
175
+ expect(store.getState().pathname).to.equal('/bar');
176
+ });
177
+
178
+ it('should warn on and ignore async blockers that throw an error', async () => {
179
+ shouldWarn(
180
+ 'Ignoring navigation blocker `asyncListener` that failed with `Error: foo`.',
181
+ );
182
+
183
+ // eslint-disable-next-line require-await
184
+ const asyncListener = async () => {
185
+ throw new Error('foo');
186
+ };
187
+
188
+ addNavigationBlocker(asyncListener);
189
+
190
+ store.dispatch(Actions.push('/bar'));
191
+ expect(store.getState().pathname).to.equal('/foo');
192
+
193
+ await delay(10);
194
+
195
+ expect(store.getState().pathname).to.equal('/bar');
196
+ });
197
+
198
+ it('should allow removing listeners', () => {
199
+ const removeNavigationListener = addNavigationBlocker(() => true);
200
+
201
+ store.dispatch(Actions.push('/bar'));
202
+ expect(store.getState().pathname).to.equal('/foo');
203
+
204
+ removeNavigationListener();
205
+
206
+ store.dispatch(Actions.push('/bar'));
207
+ expect(store.getState().pathname).to.equal('/bar');
208
+ });
209
+ });
210
+
211
+ describe('POP navigations', () => {
212
+ beforeEach(() => {
213
+ store.dispatch(Actions.push('/bar'));
214
+ });
215
+
216
+ it('should allow navigation when blocker returns `undefined`', () => {
217
+ const listener = sinon.stub().returns(undefined);
218
+ addNavigationBlocker(listener);
219
+
220
+ store.dispatch(Actions.shift(-1));
221
+ expect(store.getState().pathname).to.equal('/foo');
222
+
223
+ expect(listener.firstCall.args[0]).to.include({
224
+ action: 'POP',
225
+ pathname: '/foo',
226
+ delta: -1,
227
+ });
228
+ });
229
+
230
+ it('should block navigation when blocker returns `true`', () => {
231
+ addNavigationBlocker(() => true);
232
+
233
+ store.dispatch(Actions.shift(-1));
234
+ expect(store.getState().pathname).to.equal('/bar');
235
+ });
236
+
237
+ it('should allow navigation when blocker returns `undefined` (async)', async () => {
238
+ const deferred = pDefer();
239
+ addNavigationBlocker(() => deferred.promise);
240
+
241
+ store.dispatch(Actions.shift(-1));
242
+ expect(store.getState().pathname).to.equal('/bar');
243
+
244
+ deferred.resolve(undefined);
245
+ await delay(10);
246
+
247
+ expect(store.getState().pathname).to.equal('/foo');
248
+ });
249
+
250
+ it('should block navigation when blocker returns `true` (async)', async () => {
251
+ const deferred = pDefer();
252
+ addNavigationBlocker(() => deferred.promise);
253
+
254
+ store.dispatch(Actions.shift(-1));
255
+ expect(store.getState().pathname).to.equal('/bar');
256
+
257
+ deferred.resolve(true);
258
+ await delay(10);
259
+
260
+ expect(store.getState().pathname).to.equal('/bar');
261
+ });
262
+
263
+ // it('should show a confirmation dialog and allow navigation on string', () => {
264
+ // sandbox.stub(window, 'confirm').returns(true);
265
+ //
266
+ // addNavigationBlocker(({ pathname }) => pathname);
267
+ //
268
+ // store.dispatch(Actions.shift(-1));
269
+ // expect(store.getState().pathname).to.equal('/foo');
270
+ //
271
+ // expect(window.confirm)
272
+ // .to.have.been.calledOnce()
273
+ // .and.to.have.been.called.with('/bar');
274
+ // });
275
+
276
+ it('should ignore the initial load when blocker returns `true`', () => {
277
+ // Get rid of the old store. We'll replace it with a new one.
278
+ store.dispatch(Actions.dispose());
279
+
280
+ store = createStore(
281
+ locationReducer,
282
+ applyMiddleware(...createMiddlewares(new MemoryEnvironment('/foo'))),
283
+ );
284
+ addNavigationBlocker(() => true);
285
+
286
+ expect(store.getState()).to.be.undefined();
287
+ store.dispatch(Actions.init());
288
+ expect(store.getState().pathname).to.equal('/foo');
289
+ });
290
+
291
+ it('should support async rewinding', async () => {
292
+ // eslint-disable-next-line no-underscore-dangle
293
+ const listener = environment._listener;
294
+
295
+ let environmentDeferred;
296
+
297
+ // eslint-disable-next-line no-underscore-dangle
298
+ environment._listener = async (location) => {
299
+ environmentDeferred = pDefer();
300
+ await environmentDeferred.promise;
301
+
302
+ listener(location);
303
+ };
304
+
305
+ const deferred = pDefer();
306
+ addNavigationBlocker(() => deferred.promise);
307
+
308
+ store.dispatch(Actions.shift(-1));
309
+
310
+ // Environment popped, update to store blocked.
311
+ expect(environment.init().pathname).to.equal('/foo');
312
+ expect(store.getState().pathname).to.equal('/bar');
313
+
314
+ environmentDeferred.resolve();
315
+ await delay(10);
316
+
317
+ // Environment rewinded.
318
+ expect(environment.init().pathname).to.equal('/bar');
319
+ expect(store.getState().pathname).to.equal('/bar');
320
+
321
+ deferred.resolve(undefined);
322
+ await delay(10);
323
+
324
+ environmentDeferred.resolve();
325
+ await delay(10);
326
+
327
+ // Environment re-popped, update to store delayed.
328
+ expect(environment.init().pathname).to.equal('/foo');
329
+ expect(store.getState().pathname).to.equal('/foo');
330
+ });
331
+
332
+ it('should allow navigation without calling any blockers when `location.delta` is `null`', async () => {
333
+ const deferred = pDefer();
334
+ addNavigationBlocker(() => deferred.promise);
335
+
336
+ // Update location with a `POP` action.
337
+ /* eslint-disable no-underscore-dangle */
338
+ environment._index = 0;
339
+ environment._listener(environment.init(null));
340
+ /* eslint-enable no-underscore-dangle */
341
+
342
+ deferred.resolve(undefined);
343
+ await delay(10);
344
+
345
+ // Without delta, we can't rewind on the environment,
346
+ // so navigation is allowed without calling any blockers.
347
+ expect(environment.init().pathname).to.equal('/foo');
348
+ expect(store.getState().pathname).to.equal('/foo');
349
+ });
350
+
351
+ // it('should allow navigation when blocker returns `undefined` and `location.delta` is `null`', async () => {
352
+ // const deferred = pDefer();
353
+ // addNavigationBlocker(() => deferred.promise);
354
+ //
355
+ // // Update location with a `POP` action.
356
+ // /* eslint-disable no-underscore-dangle */
357
+ // environment._index = 0;
358
+ // environment._listener(environment.init(null));
359
+ // /* eslint-enable no-underscore-dangle */
360
+ //
361
+ // // Without delta, we can't rewind on the environment.
362
+ // expect(environment.init().pathname).to.equal('/foo');
363
+ // expect(store.getState().pathname).to.equal('/bar');
364
+ //
365
+ // deferred.resolve(undefined);
366
+ // await delay(10);
367
+ //
368
+ // expect(environment.init().pathname).to.equal('/foo');
369
+ // expect(store.getState().pathname).to.equal('/foo');
370
+ // });
371
+
372
+ // it('should block store update when blocker returns `true` and `location.delta` is `null`', async () => {
373
+ // const deferred = pDefer();
374
+ // addNavigationBlocker(() => deferred.promise);
375
+ //
376
+ // /* eslint-disable no-underscore-dangle */
377
+ // environment._index = 0;
378
+ // environment._listener(environment.init(null));
379
+ // /* eslint-enable no-underscore-dangle */
380
+ //
381
+ // expect(environment.init().pathname).to.equal('/foo');
382
+ // expect(store.getState().pathname).to.equal('/bar');
383
+ //
384
+ // deferred.resolve(true);
385
+ // await delay(10);
386
+ //
387
+ // // These are out-of-sync now, but it's the best we can do.
388
+ // expect(environment.init().pathname).to.equal('/foo');
389
+ // expect(store.getState().pathname).to.equal('/bar');
390
+ // });
391
+ });
392
+
393
+ describe('beforeUnload', () => {
394
+ beforeEach(() => {
395
+ // Get rid of the old store. We'll replace it with a new one.
396
+ store.dispatch(Actions.dispose());
397
+
398
+ sandbox.stub(window, 'addEventListener');
399
+ sandbox.stub(window, 'removeEventListener');
400
+
401
+ store = createStore(
402
+ () => null,
403
+ applyMiddleware(...createMiddlewares(environment)),
404
+ );
405
+
406
+ store.dispatch(Actions.init());
407
+ });
408
+
409
+ it('should manage event listener', () => {
410
+ expect(environment.addBeforeDestroyListener).not.to.have.been.called();
411
+ // expect(window.addEventListener).not.to.have.been.called();
412
+
413
+ const removeNavigationListener1 = addNavigationBlocker(() => null, {
414
+ beforeUnload: true,
415
+ });
416
+ expect(environment.addBeforeDestroyListener).to.have.been.calledOnce();
417
+ // expect(window.addEventListener)
418
+ // .to.have.been.calledOnce()
419
+ // .and.to.have.been.called.with('beforeunload');
420
+
421
+ const removeNavigationListener2 = addNavigationBlocker(() => null, {
422
+ beforeUnload: true,
423
+ });
424
+ expect(environment.addBeforeDestroyListener).to.have.been.calledOnce();
425
+ // expect(window.addEventListener)
426
+ // .to.have.been.calledOnce()
427
+ // .and.to.have.been.called.with('beforeunload');
428
+
429
+ removeNavigationListener1();
430
+ // expect(window.removeEventListener).not.to.have.been.called();
431
+ expect(
432
+ // eslint-disable-next-line no-underscore-dangle
433
+ environment._removeBeforeDestroyListener,
434
+ ).not.to.have.been.called();
435
+
436
+ removeNavigationListener2();
437
+ // expect(window.removeEventListener)
438
+ // .to.have.been.calledOnce()
439
+ // .and.to.have.been.called.with('beforeunload');
440
+ expect(
441
+ // eslint-disable-next-line no-underscore-dangle
442
+ environment._removeBeforeDestroyListener,
443
+ ).to.have.been.calledOnce();
444
+ });
445
+
446
+ it('should remove event listener on dispose', () => {
447
+ addNavigationBlocker(() => null, { beforeUnload: true });
448
+ // expect(window.removeEventListener).not.to.have.been.called();
449
+ expect(
450
+ // eslint-disable-next-line no-underscore-dangle
451
+ environment._removeBeforeDestroyListener,
452
+ ).not.to.have.been.called();
453
+
454
+ store.dispatch(Actions.dispose());
455
+ // expect(window.removeEventListener)
456
+ // .to.have.been.calledOnce()
457
+ // .and.to.have.been.called.with('beforeunload');
458
+ expect(
459
+ // eslint-disable-next-line no-underscore-dangle
460
+ environment._removeBeforeDestroyListener,
461
+ ).to.have.been.calledOnce();
462
+ });
463
+
464
+ it('should not add event listener without beforeUnload', () => {
465
+ const removeNavigationListener = addNavigationBlocker(() => null);
466
+ expect(window.addEventListener).not.to.have.been.called();
467
+
468
+ removeNavigationListener();
469
+ expect(window.removeEventListener).not.to.have.been.called();
470
+ });
471
+ });
472
+ });
@@ -0,0 +1,44 @@
1
+ import ActionTypes from '../../src/ActionTypes';
2
+ import createTransformLocationMiddleware from '../../src/middleware/createTransformLocationMiddleware';
3
+
4
+ describe('createTransformLocationMiddleware', () => {
5
+ const middleware = createTransformLocationMiddleware({
6
+ transformInputLocation: (locationInput) => ({ locationInput }),
7
+ transformEnvironmentLocation: (location) => ({ location }),
8
+ });
9
+
10
+ const dispatch = middleware()((action) => action.payload);
11
+
12
+ it('should handle location descriptors for NAVIGATE', () => {
13
+ expect(
14
+ dispatch({
15
+ type: ActionTypes.NAVIGATE,
16
+ payload: {},
17
+ }),
18
+ ).to.eql({
19
+ locationInput: {},
20
+ });
21
+ });
22
+
23
+ it('should handle locations for UPDATE', () => {
24
+ expect(
25
+ dispatch({
26
+ type: ActionTypes.UPDATE,
27
+ payload: {},
28
+ }),
29
+ ).to.eql({
30
+ location: {},
31
+ });
32
+ });
33
+
34
+ it('should ignore other actions', () => {
35
+ expect(
36
+ dispatch({
37
+ type: 'UNKNOWN',
38
+ payload: { unknown: {} },
39
+ }),
40
+ ).to.eql({
41
+ unknown: {},
42
+ });
43
+ });
44
+ });
@@ -0,0 +1,74 @@
1
+ import ActionTypes from '../../src/ActionTypes';
2
+ import navigationActionMiddleware from '../../src/middleware/navigationActionMiddleware';
3
+
4
+ describe('navigationActionMiddleware', () => {
5
+ let next;
6
+ let dispatch;
7
+ beforeEach(() => {
8
+ next = sinon.spy();
9
+ dispatch = navigationActionMiddleware()(next);
10
+ });
11
+
12
+ it('should change `type` of PUSH action', () => {
13
+ dispatch({
14
+ type: ActionTypes.PUSH,
15
+ payload: {
16
+ pathname: '/foo',
17
+ search: '?bar=baz',
18
+ hash: '#qux',
19
+ },
20
+ });
21
+
22
+ expect(next).to.be.calledWith({
23
+ type: ActionTypes.NAVIGATE,
24
+ payload: {
25
+ action: 'PUSH',
26
+ pathname: '/foo',
27
+ search: '?bar=baz',
28
+ hash: '#qux',
29
+ },
30
+ });
31
+ });
32
+
33
+ it('should change `type` of REPLACE action', () => {
34
+ dispatch({
35
+ type: ActionTypes.REPLACE,
36
+ payload: {
37
+ pathname: '/foo',
38
+ search: '?bar=baz',
39
+ hash: '#qux',
40
+ },
41
+ });
42
+
43
+ expect(next).to.be.calledWith({
44
+ type: ActionTypes.NAVIGATE,
45
+ payload: {
46
+ action: 'REPLACE',
47
+ pathname: '/foo',
48
+ search: '?bar=baz',
49
+ hash: '#qux',
50
+ },
51
+ });
52
+ });
53
+
54
+ it('should not affect other action', () => {
55
+ const UNKNOWN = 'UNKNOWN';
56
+ dispatch({
57
+ type: UNKNOWN,
58
+ payload: {
59
+ pathname: '/foo',
60
+ search: '?bar=baz',
61
+ hash: '#qux',
62
+ },
63
+ });
64
+
65
+ expect(next).to.be.calledWith({
66
+ type: UNKNOWN,
67
+ payload: {
68
+ pathname: '/foo',
69
+ search: '?bar=baz',
70
+ hash: '#qux',
71
+ },
72
+ });
73
+ });
74
+ });
@@ -0,0 +1,62 @@
1
+ import ActionTypes from '../../src/ActionTypes';
2
+ import normalizeInputLocationMiddleware from '../../src/middleware/normalizeInputLocationMiddleware';
3
+
4
+ describe('normalizeInputLocationMiddleware', () => {
5
+ let next;
6
+ let dispatch;
7
+ beforeEach(() => {
8
+ next = sinon.spy();
9
+ dispatch = normalizeInputLocationMiddleware()(next);
10
+ });
11
+
12
+ it('should transform input location of PUSH action (`string` to `object`)', () => {
13
+ dispatch({
14
+ type: ActionTypes.PUSH,
15
+ payload: '/foo?bar=baz#qux',
16
+ });
17
+
18
+ expect(next).to.be.calledWith({
19
+ type: ActionTypes.PUSH,
20
+ payload: {
21
+ pathname: '/foo',
22
+ search: '?bar=baz',
23
+ query: {
24
+ bar: 'baz',
25
+ },
26
+ hash: '#qux',
27
+ },
28
+ });
29
+ });
30
+
31
+ it('should transform input location of REPLACE action (`string` to `object`)', () => {
32
+ dispatch({
33
+ type: ActionTypes.REPLACE,
34
+ payload: '/foo?bar=baz#qux',
35
+ });
36
+
37
+ expect(next).to.be.calledWith({
38
+ type: ActionTypes.REPLACE,
39
+ payload: {
40
+ pathname: '/foo',
41
+ search: '?bar=baz',
42
+ query: {
43
+ bar: 'baz',
44
+ },
45
+ hash: '#qux',
46
+ },
47
+ });
48
+ });
49
+
50
+ it('should not affect other action', () => {
51
+ const UNKNOWN = 'UNKNOWN';
52
+ dispatch({
53
+ type: UNKNOWN,
54
+ payload: '/foo?bar=baz#qux',
55
+ });
56
+
57
+ expect(next).to.be.calledWith({
58
+ type: UNKNOWN,
59
+ payload: '/foo?bar=baz#qux',
60
+ });
61
+ });
62
+ });