navigation-stack 0.3.1 → 0.5.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 (257) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +611 -163
  3. package/data-storage/package.json +6 -0
  4. package/karma.conf.cjs +21 -4
  5. package/lib/cjs/NavigationStack.js +88 -0
  6. package/lib/cjs/data-storage/DataStorage.js +71 -0
  7. package/lib/cjs/data-storage/LocationDataStorage.js +29 -0
  8. package/lib/cjs/data-storage/index.js +9 -0
  9. package/lib/cjs/debug.js +12 -0
  10. package/lib/cjs/environment/InMemoryEnvironment.js +15 -0
  11. package/lib/cjs/environment/WebBrowserEnvironment.js +15 -0
  12. package/lib/cjs/environment/data-storage/InMemoryDataStorage.js +27 -0
  13. package/lib/cjs/environment/data-storage/WebBrowserDataStorage.js +21 -0
  14. package/lib/cjs/environment/scroll-position/InMemoryScrollPosition.js +44 -0
  15. package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +60 -0
  16. package/lib/cjs/getLocationFromInternalLocation.js +14 -0
  17. package/lib/cjs/index.js +20 -16
  18. package/lib/cjs/navigationBlockers.js +28 -23
  19. package/lib/cjs/{normalizeInputLocation.js → parseInputLocation.js} +25 -9
  20. package/lib/cjs/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  21. package/lib/cjs/redux/ActionTypesInternal.js +8 -0
  22. package/lib/cjs/{Actions.js → redux/Actions.js} +5 -4
  23. package/lib/cjs/redux/createMiddlewares.js +60 -0
  24. package/lib/cjs/redux/index.js +13 -0
  25. package/lib/cjs/redux/internalLocationReducer.js +14 -0
  26. package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +32 -0
  27. package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +113 -0
  28. package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  29. package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +30 -0
  30. package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +73 -0
  31. package/lib/cjs/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +11 -8
  32. package/lib/cjs/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +6 -4
  33. package/lib/cjs/redux/middleware/updateLocationMiddleware.js +34 -0
  34. package/lib/cjs/scroll-position/PageScrollPositionSetter.js +97 -0
  35. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +141 -0
  36. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +407 -0
  37. package/lib/cjs/scroll-position/ScrollPositionSaver.js +87 -0
  38. package/lib/cjs/scroll-position/ScrollPositionSetter.js +16 -0
  39. package/lib/cjs/scroll-position/constants.js +5 -0
  40. package/lib/cjs/scroll-position/index.js +7 -0
  41. package/lib/cjs/scroll-position/scheduleNextTick.js +11 -0
  42. package/lib/cjs/session/InMemorySession.js +22 -0
  43. package/lib/cjs/session/ServerSideRenderSession.js +17 -0
  44. package/lib/cjs/session/Session.js +202 -0
  45. package/lib/cjs/session/WebBrowserSession.js +20 -0
  46. package/lib/cjs/session/key/createSessionKey.js +23 -0
  47. package/lib/cjs/session/lifecycle/InMemorySessionLifecycle.js +19 -0
  48. package/lib/cjs/session/lifecycle/WebBrowserSessionLifecycle.js +128 -0
  49. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycle.js +269 -0
  50. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +8 -0
  51. package/lib/cjs/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +33 -0
  52. package/lib/cjs/session/navigation/InMemoryNavigation.js +104 -0
  53. package/lib/cjs/session/navigation/ServerSideNavigation.js +61 -0
  54. package/lib/cjs/session/navigation/WebBrowserNavigation.js +221 -0
  55. package/lib/cjs/session/navigation/error/NavigationOutOfBoundsError.js +12 -0
  56. package/lib/cjs/session/navigation/error/ServerSideNavigationError.js +21 -0
  57. package/lib/cjs/session/navigation/operation/operations.js +11 -0
  58. package/lib/cjs/session/subscription/Subscription.js +81 -0
  59. package/lib/data-storage/index.d.ts +35 -0
  60. package/lib/esm/NavigationStack.js +81 -0
  61. package/lib/esm/data-storage/DataStorage.js +65 -0
  62. package/lib/esm/data-storage/LocationDataStorage.js +22 -0
  63. package/lib/esm/data-storage/index.js +2 -0
  64. package/lib/esm/debug.js +7 -0
  65. package/lib/esm/environment/InMemoryEnvironment.js +8 -0
  66. package/lib/esm/environment/WebBrowserEnvironment.js +8 -0
  67. package/lib/esm/environment/data-storage/InMemoryDataStorage.js +21 -0
  68. package/lib/esm/environment/data-storage/WebBrowserDataStorage.js +15 -0
  69. package/lib/esm/environment/scroll-position/InMemoryScrollPosition.js +38 -0
  70. package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +54 -0
  71. package/lib/esm/getLocationFromInternalLocation.js +9 -0
  72. package/lib/esm/index.js +10 -8
  73. package/lib/esm/navigationBlockers.js +28 -23
  74. package/lib/esm/{normalizeInputLocation.js → parseInputLocation.js} +24 -8
  75. package/lib/esm/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  76. package/lib/esm/redux/ActionTypesInternal.js +3 -0
  77. package/lib/esm/{Actions.js → redux/Actions.js} +5 -4
  78. package/lib/esm/redux/createMiddlewares.js +54 -0
  79. package/lib/esm/redux/index.js +4 -0
  80. package/lib/esm/redux/internalLocationReducer.js +8 -0
  81. package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  82. package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +108 -0
  83. package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +88 -0
  84. package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +25 -0
  85. package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +68 -0
  86. package/lib/esm/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -7
  87. package/lib/esm/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  88. package/lib/esm/redux/middleware/updateLocationMiddleware.js +28 -0
  89. package/lib/esm/scroll-position/PageScrollPositionSetter.js +91 -0
  90. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +134 -0
  91. package/lib/esm/scroll-position/ScrollPositionRestoration.js +400 -0
  92. package/lib/esm/scroll-position/ScrollPositionSaver.js +80 -0
  93. package/lib/esm/scroll-position/ScrollPositionSetter.js +10 -0
  94. package/lib/esm/scroll-position/constants.js +1 -0
  95. package/lib/esm/scroll-position/index.js +1 -0
  96. package/lib/esm/scroll-position/scheduleNextTick.js +6 -0
  97. package/lib/esm/session/InMemorySession.js +15 -0
  98. package/lib/esm/session/ServerSideRenderSession.js +11 -0
  99. package/lib/esm/session/Session.js +195 -0
  100. package/lib/esm/session/WebBrowserSession.js +13 -0
  101. package/lib/esm/session/key/createSessionKey.js +18 -0
  102. package/lib/esm/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  103. package/lib/esm/session/lifecycle/WebBrowserSessionLifecycle.js +120 -0
  104. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycle.js +263 -0
  105. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +2 -0
  106. package/lib/esm/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +30 -0
  107. package/lib/esm/session/navigation/InMemoryNavigation.js +97 -0
  108. package/lib/esm/session/navigation/ServerSideNavigation.js +54 -0
  109. package/lib/esm/session/navigation/WebBrowserNavigation.js +213 -0
  110. package/lib/esm/session/navigation/error/NavigationOutOfBoundsError.js +6 -0
  111. package/lib/esm/session/navigation/error/ServerSideNavigationError.js +14 -0
  112. package/lib/esm/session/navigation/operation/operations.js +6 -0
  113. package/lib/esm/session/subscription/Subscription.js +75 -0
  114. package/lib/index.d.ts +179 -157
  115. package/lib/redux/index.d.ts +90 -0
  116. package/lib/scroll-position/index.d.ts +107 -0
  117. package/package.json +9 -5
  118. package/redux/package.json +6 -0
  119. package/scroll-position/package.json +6 -0
  120. package/src/NavigationStack.js +100 -0
  121. package/src/data-storage/DataStorage.js +69 -0
  122. package/src/data-storage/LocationDataStorage.js +23 -0
  123. package/src/data-storage/index.js +2 -0
  124. package/src/debug.js +8 -0
  125. package/src/environment/InMemoryEnvironment.js +9 -0
  126. package/src/environment/WebBrowserEnvironment.js +9 -0
  127. package/src/environment/data-storage/InMemoryDataStorage.js +23 -0
  128. package/src/environment/data-storage/WebBrowserDataStorage.js +17 -0
  129. package/src/environment/scroll-position/InMemoryScrollPosition.js +45 -0
  130. package/src/environment/scroll-position/WebBrowserScrollPosition.js +72 -0
  131. package/src/getLocationFromInternalLocation.js +7 -0
  132. package/src/index.js +10 -8
  133. package/src/navigationBlockers.js +31 -27
  134. package/src/{normalizeInputLocation.js → parseInputLocation.js} +23 -8
  135. package/src/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  136. package/src/redux/ActionTypesInternal.js +3 -0
  137. package/src/{Actions.js → redux/Actions.js} +4 -3
  138. package/src/redux/createMiddlewares.js +65 -0
  139. package/src/redux/index.js +4 -0
  140. package/src/redux/internalLocationReducer.js +9 -0
  141. package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  142. package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +119 -0
  143. package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  144. package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +26 -0
  145. package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +72 -0
  146. package/src/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -3
  147. package/src/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  148. package/src/redux/middleware/updateLocationMiddleware.js +28 -0
  149. package/src/scroll-position/PageScrollPositionSetter.js +110 -0
  150. package/src/scroll-position/ScrollPositionAutoSaver.js +168 -0
  151. package/src/scroll-position/ScrollPositionRestoration.js +551 -0
  152. package/src/scroll-position/ScrollPositionSaver.js +120 -0
  153. package/src/scroll-position/ScrollPositionSetter.js +16 -0
  154. package/src/scroll-position/constants.js +1 -0
  155. package/src/scroll-position/index.js +1 -0
  156. package/src/scroll-position/scheduleNextTick.js +6 -0
  157. package/src/session/InMemorySession.js +13 -0
  158. package/src/session/ServerSideRenderSession.js +9 -0
  159. package/src/session/Session.js +238 -0
  160. package/src/session/WebBrowserSession.js +13 -0
  161. package/src/session/key/createSessionKey.js +18 -0
  162. package/src/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  163. package/src/session/lifecycle/WebBrowserSessionLifecycle.js +126 -0
  164. package/src/session/lifecycle/page-lifecycle/PageLifecycle.js +291 -0
  165. package/src/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +3 -0
  166. package/src/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +32 -0
  167. package/src/session/navigation/InMemoryNavigation.js +78 -0
  168. package/src/session/navigation/ServerSideNavigation.js +43 -0
  169. package/src/session/navigation/WebBrowserNavigation.js +224 -0
  170. package/src/session/navigation/error/NavigationOutOfBoundsError.js +7 -0
  171. package/src/session/navigation/error/ServerSideNavigationError.js +18 -0
  172. package/src/session/navigation/operation/operations.js +6 -0
  173. package/src/session/subscription/Subscription.js +76 -0
  174. package/test/NavigationStack.test.js +296 -0
  175. package/test/{LocationDataStorage.test.js → data-storage/LocationDataStorage.test.js} +3 -3
  176. package/test/data-storage/index.test.js +8 -0
  177. package/test/index.js +12 -0
  178. package/test/index.test.js +8 -7
  179. package/test/{helpers.js → middlewareTestUtil.js} +9 -12
  180. package/test/{normalizeInputLocation.test.js → parseInputLocationMiddleware.test.js} +9 -9
  181. package/test/{Action.test.js → redux/Action.test.js} +7 -6
  182. package/test/{ActionTypes.test.js → redux/ActionTypes.test.js} +2 -2
  183. package/test/redux/createMiddlewares.test.js +96 -0
  184. package/test/redux/index.test.js +10 -0
  185. package/test/{locationReducer.test.js → redux/locationReducer.test.js} +4 -7
  186. package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +40 -0
  187. package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +264 -0
  188. package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +312 -0
  189. package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +51 -0
  190. package/test/{middleware/navigationActionMiddleware.test.js → redux/middleware/navigationOperationMiddleware.test.js} +16 -12
  191. package/test/{middleware/normalizeInputLocationMiddleware.test.js → redux/middleware/parseInputLocationMiddleware.test.js} +4 -4
  192. package/test/scroll-position/ScrollPositionRestoration.test.js +435 -0
  193. package/test/scroll-position/addScrollableContainer.js +39 -0
  194. package/test/scroll-position/addScrollableContainerWithAnchors.js +56 -0
  195. package/test/scroll-position/createApp.js +132 -0
  196. package/test/scroll-position/delay.js +9 -0
  197. package/test/scroll-position/mockPageLifecycle.js +17 -0
  198. package/test/scroll-position/runApp.js +24 -0
  199. package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +72 -0
  200. package/test/session/InMemorySession.test.js +348 -0
  201. package/test/session/ServerSession.test.js +17 -9
  202. package/test/session/WebBrowserSession.test.js +265 -0
  203. package/test/testUtil.js +3 -0
  204. package/types/data-storage/index.d.ts +35 -0
  205. package/types/index.d.ts +179 -157
  206. package/types/redux/index.d.ts +90 -0
  207. package/types/scroll-position/index.d.ts +107 -0
  208. package/types/tsconfig.json +1 -1
  209. package/lib/cjs/LocationDataStorage.js +0 -61
  210. package/lib/cjs/addBeforeLocationChangeListener.js +0 -7
  211. package/lib/cjs/beforeLocationChangeListeners.js +0 -51
  212. package/lib/cjs/createMiddlewares.js +0 -47
  213. package/lib/cjs/middleware/createBasePathMiddleware.js +0 -24
  214. package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -39
  215. package/lib/cjs/middleware/createLocationMiddleware.js +0 -56
  216. package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +0 -161
  217. package/lib/cjs/middleware/createTransformLocationMiddleware.js +0 -38
  218. package/lib/cjs/onlyAllowedOnClientSide.js +0 -10
  219. package/lib/cjs/session/BrowserSession.js +0 -235
  220. package/lib/cjs/session/MemorySession.js +0 -223
  221. package/lib/cjs/session/ServerSession.js +0 -65
  222. package/lib/esm/LocationDataStorage.js +0 -54
  223. package/lib/esm/addBeforeLocationChangeListener.js +0 -2
  224. package/lib/esm/beforeLocationChangeListeners.js +0 -44
  225. package/lib/esm/createMiddlewares.js +0 -41
  226. package/lib/esm/middleware/createBasePathMiddleware.js +0 -19
  227. package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -34
  228. package/lib/esm/middleware/createLocationMiddleware.js +0 -50
  229. package/lib/esm/middleware/createNavigationBlockerMiddleware.js +0 -156
  230. package/lib/esm/middleware/createTransformLocationMiddleware.js +0 -33
  231. package/lib/esm/onlyAllowedOnClientSide.js +0 -5
  232. package/lib/esm/session/BrowserSession.js +0 -229
  233. package/lib/esm/session/MemorySession.js +0 -217
  234. package/lib/esm/session/ServerSession.js +0 -58
  235. package/src/LocationDataStorage.js +0 -60
  236. package/src/addBeforeLocationChangeListener.js +0 -2
  237. package/src/beforeLocationChangeListeners.js +0 -54
  238. package/src/createMiddlewares.js +0 -45
  239. package/src/middleware/createBasePathMiddleware.js +0 -20
  240. package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -40
  241. package/src/middleware/createLocationMiddleware.js +0 -55
  242. package/src/middleware/createNavigationBlockerMiddleware.js +0 -168
  243. package/src/middleware/createTransformLocationMiddleware.js +0 -29
  244. package/src/onlyAllowedOnClientSide.js +0 -5
  245. package/src/session/BrowserSession.js +0 -235
  246. package/src/session/MemorySession.js +0 -219
  247. package/src/session/ServerSession.js +0 -67
  248. package/test/createMiddlewares.test.js +0 -62
  249. package/test/middleware/createBasePathMiddleware.test.js +0 -67
  250. package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +0 -141
  251. package/test/middleware/createNavigationBlockerMiddleware.test.js +0 -471
  252. package/test/middleware/createTransformLocationMiddleware.test.js +0 -44
  253. package/test/session/BrowserSession.test.js +0 -182
  254. package/test/session/MemorySession.test.js +0 -244
  255. /package/lib/cjs/{locationReducer.js → redux/locationReducer.js} +0 -0
  256. /package/lib/esm/{locationReducer.js → redux/locationReducer.js} +0 -0
  257. /package/src/{locationReducer.js → redux/locationReducer.js} +0 -0
@@ -0,0 +1,18 @@
1
+ import getLocationUrl from '../../../getLocationUrl';
2
+
3
+ export default class ServerSideNavigationError extends Error {
4
+ constructor(location) {
5
+ super(
6
+ location
7
+ ? `Navigate to ${getLocationUrl(location)}`
8
+ : 'Navigate to previous or next location',
9
+ );
10
+
11
+ if (location) {
12
+ // Remove `operation` property from `location`.
13
+ // eslint-disable-next-line no-unused-vars
14
+ const { operation, ...locationBase } = location;
15
+ this.location = locationBase;
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ INIT: 'init',
3
+ PUSH: 'push',
4
+ REPLACE: 'replace',
5
+ SHIFT: 'shift',
6
+ };
@@ -0,0 +1,76 @@
1
+ export default class Subscription {
2
+ constructor({ activateSubscription } = {}) {
3
+ this._activateSubscription = activateSubscription;
4
+
5
+ // This property is accessed in tests.
6
+ this._listeners = [];
7
+ }
8
+
9
+ notifySubscribers = (argument) => {
10
+ // `._latest` is only used in tests.
11
+ this._latest = argument;
12
+ for (const { listener } of this._listeners) {
13
+ listener(argument);
14
+ }
15
+ };
16
+
17
+ subscribe(listener) {
18
+ // If subscriptions are stopped, i.e. no new subscriptions are to be added,
19
+ // then don't add any listeners and return a "do nothing" function.
20
+ if (this._stopped) {
21
+ return () => {};
22
+ }
23
+
24
+ // Creating a `listenerEntry` object ensures that the `.filter()` function
25
+ // during "unsubscribe" step doesn't accidentally remove another listeners
26
+ // having the same `listener` function.
27
+ // I.e. it's not illegal to call `.subscribe(listener)` multiple times
28
+ // with the same argument, and those would be considered different subscriptions.
29
+ const listenerEntry = { listener };
30
+
31
+ // If it's the first listener, activate subscription.
32
+ if (this._listeners.length === 0) {
33
+ this._deactivateSubscription = this._activateSubscription(
34
+ this.notifySubscribers,
35
+ );
36
+ }
37
+
38
+ // Add the `listener` to the list.
39
+ this._listeners.push(listenerEntry);
40
+
41
+ // The returned `unsubscribe()` function is "idempotent", i.e. it can be called multiple times.
42
+ return () => {
43
+ // Remove the listener, if not already removed.
44
+ this._removeListener(listenerEntry);
45
+ };
46
+ }
47
+
48
+ stop() {
49
+ if (this._stopped) {
50
+ throw new Error('Already stopped');
51
+ }
52
+
53
+ this._stopped = true;
54
+
55
+ // Clear any remaining listeners.
56
+ for (const listener of this._listeners.slice()) {
57
+ this._removeListener(listener);
58
+ }
59
+ }
60
+
61
+ _removeListener(listenerEntry) {
62
+ // If no listeners are left, no need to do anything.
63
+ if (this._listeners.length === 0) {
64
+ return;
65
+ }
66
+
67
+ // Remove the `listener` from the list.
68
+ this._listeners = this._listeners.filter((_) => _ !== listenerEntry);
69
+
70
+ // If it was the last listener, deactivate subscription.
71
+ if (this._listeners.length === 0) {
72
+ this._deactivateSubscription();
73
+ this._deactivateSubscription = undefined;
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,296 @@
1
+ import delay from 'delay';
2
+
3
+ import NavigationStack from '../src/NavigationStack';
4
+ import InMemorySession from '../src/session/InMemorySession';
5
+ import WebBrowserSession from '../src/session/WebBrowserSession';
6
+
7
+ describe('NavigationStack', () => {
8
+ let navigationStack;
9
+
10
+ beforeEach(() => {
11
+ navigationStack = new NavigationStack(new InMemorySession());
12
+ navigationStack.init('/initial');
13
+ });
14
+
15
+ afterEach(() => {
16
+ // Even if a test errors, the `NavigationStack` should still be stopped
17
+ // in order to remove the potential "popstate" listener so that it doesn't
18
+ // interfere with other tests.
19
+ navigationStack.stop();
20
+ });
21
+
22
+ it('should support `push` and `shift` navigation operations', () => {
23
+ navigationStack.push('/new');
24
+ expect(navigationStack.current()).to.include({
25
+ pathname: '/new',
26
+ index: 1,
27
+ });
28
+
29
+ navigationStack.shift(-1);
30
+ expect(navigationStack.current()).to.include({
31
+ pathname: '/initial',
32
+ index: 0,
33
+ });
34
+
35
+ navigationStack.shift(+1);
36
+ expect(navigationStack.current()).to.include({
37
+ pathname: '/new',
38
+ index: 1,
39
+ });
40
+ });
41
+
42
+ it('should support `replace` navigation operation', () => {
43
+ navigationStack.replace('/new');
44
+ expect(navigationStack.current()).to.include({
45
+ pathname: '/new',
46
+ index: 0,
47
+ });
48
+ });
49
+ });
50
+
51
+ describe('NavigationStack (WebBrowserSession)', () => {
52
+ let navigationStack;
53
+
54
+ beforeEach(() => {
55
+ window.history.replaceState(null, null, '/initial');
56
+
57
+ navigationStack = new NavigationStack(new WebBrowserSession());
58
+
59
+ navigationStack.init();
60
+ });
61
+
62
+ afterEach(() => {
63
+ // Even if a test errors, the `NavigationStack` should still be stopped
64
+ // in order to remove the potential "popstate" listener so that it doesn't
65
+ // interfere with other tests.
66
+ navigationStack.stop();
67
+ });
68
+
69
+ it('should allow calling `init()` without an argument, and then support `push` and `shift` navigation operations', async () => {
70
+ navigationStack.push('/new');
71
+ await delay(20);
72
+ expect(navigationStack.current()).to.include({
73
+ pathname: '/new',
74
+ index: 1,
75
+ });
76
+
77
+ navigationStack.shift(-1);
78
+ await delay(20);
79
+ expect(navigationStack.current()).to.include({
80
+ pathname: '/initial',
81
+ index: 0,
82
+ });
83
+
84
+ navigationStack.shift(+1);
85
+ await delay(20);
86
+ expect(navigationStack.current()).to.include({
87
+ pathname: '/new',
88
+ index: 1,
89
+ });
90
+ });
91
+
92
+ it('should allow calling `init()` without an argument, and then support `replace` navigation operation', async () => {
93
+ navigationStack.replace('/new');
94
+ await delay(20);
95
+ expect(navigationStack.current()).to.include({
96
+ pathname: '/new',
97
+ index: 0,
98
+ });
99
+ });
100
+ });
101
+
102
+ describe('NavigationStack.subscribe', () => {
103
+ let navigationStack;
104
+
105
+ afterEach(() => {
106
+ // Even if a test errors, the `NavigationStack` should still be stopped
107
+ // in order to remove the potential "popstate" listener so that it doesn't
108
+ // interfere with other tests.
109
+ navigationStack.stop();
110
+ });
111
+
112
+ it('should subscribe to location changes', () => {
113
+ navigationStack = new NavigationStack(new InMemorySession());
114
+
115
+ const listener = sinon.spy();
116
+
117
+ const unsubscribe = navigationStack.subscribe(listener);
118
+
119
+ navigationStack.init('/initial');
120
+
121
+ // `.init()` calls subscription listeners.
122
+ expect(listener).to.have.been.calledOnce();
123
+ expect(listener.lastCall.args[0]).to.include({
124
+ // operation: 'init',
125
+ pathname: '/initial',
126
+ });
127
+ listener.resetHistory();
128
+
129
+ navigationStack.push('/new');
130
+
131
+ expect(listener).to.have.been.calledOnce();
132
+ expect(listener.lastCall.args[0]).to.include({
133
+ // operation: 'push',
134
+ pathname: '/new',
135
+ });
136
+ listener.resetHistory();
137
+
138
+ navigationStack.replace('/new-2');
139
+
140
+ expect(listener).to.have.been.calledOnce();
141
+ expect(listener.lastCall.args[0]).to.include({
142
+ // operation: 'replace',
143
+ pathname: '/new-2',
144
+ });
145
+ listener.resetHistory();
146
+
147
+ navigationStack.shift(-1);
148
+
149
+ expect(listener).to.have.been.calledOnce();
150
+ expect(listener.lastCall.args[0]).to.include({
151
+ // operation: 'shift',
152
+ // delta: -1,
153
+ pathname: '/initial',
154
+ });
155
+ listener.resetHistory();
156
+
157
+ unsubscribe();
158
+
159
+ navigationStack.push('/new-3');
160
+
161
+ // Unsubscribed, so the listener doesn't get called.
162
+ expect(listener).to.not.have.been.called();
163
+ });
164
+
165
+ it('should subscribe to location changes (WebBrowserSession)', () => {
166
+ window.history.replaceState(null, null, '/initial');
167
+
168
+ navigationStack = new NavigationStack(new WebBrowserSession());
169
+
170
+ const listener = sinon.spy();
171
+
172
+ const unsubscribe = navigationStack.subscribe(listener);
173
+
174
+ navigationStack.init();
175
+
176
+ // `.init()` calls subscription listeners.
177
+ expect(listener).to.have.been.calledOnce();
178
+ expect(listener.lastCall.args[0]).to.include({
179
+ // operation: 'init',
180
+ pathname: '/initial',
181
+ });
182
+ listener.resetHistory();
183
+
184
+ unsubscribe();
185
+ });
186
+ });
187
+
188
+ describe('NavigationStack.stop()', () => {
189
+ const sandbox = sinon.createSandbox();
190
+
191
+ afterEach(() => {
192
+ // Even if a test errors, the spies should be removed, otherwise it'll say:
193
+ // "Attempted to wrap addEventListener which is already wrapped".
194
+ sandbox.restore();
195
+ });
196
+
197
+ it('should remove "popstate" listener on stop', () => {
198
+ sandbox.spy(window, 'addEventListener');
199
+ sandbox.spy(window, 'removeEventListener');
200
+
201
+ const session = new WebBrowserSession();
202
+
203
+ const navigationStack = new NavigationStack(session);
204
+
205
+ // This subscription won't be "unsubscribed" by the code.
206
+ // It is expected to be "unsubscribed" automatically on `navigationStack.stop()`
207
+ // and not hold off the clearing of `popstate` listener.
208
+ navigationStack.subscribe(() => {});
209
+
210
+ navigationStack.init();
211
+
212
+ navigationStack.push('/new');
213
+
214
+ navigationStack.stop();
215
+
216
+ expect(window.addEventListener)
217
+ .to.have.been.calledOnce()
218
+ .and.to.have.been.called.with('popstate');
219
+
220
+ expect(window.removeEventListener)
221
+ .to.have.been.calledOnce()
222
+ .and.to.have.been.called.with('popstate');
223
+ });
224
+ });
225
+
226
+ describe('NavigationStack', () => {
227
+ let navigationStack;
228
+
229
+ afterEach(() => {
230
+ // Even if a test errors, the `NavigationStack` should still be stopped
231
+ // in order to remove the potential "popstate" listener so that it doesn't
232
+ // interfere with other tests.
233
+ //
234
+ // `navigationStack.stop()` method is "idempotent", i.e. it can be called multiple times.
235
+ //
236
+ navigationStack.stop();
237
+ });
238
+
239
+ it('should support `basePath`', () => {
240
+ const session = new InMemorySession();
241
+
242
+ navigationStack = new NavigationStack(session, {
243
+ basePath: '/base',
244
+ });
245
+
246
+ navigationStack.init('/initial');
247
+
248
+ navigationStack.push('/new');
249
+
250
+ // eslint-disable-next-line no-underscore-dangle
251
+ expect(session._subscription._latest.pathname).to.equal('/base/new');
252
+
253
+ expect(navigationStack.current()).to.include({
254
+ pathname: '/new',
255
+ index: 1,
256
+ });
257
+ });
258
+
259
+ it('should support `maintainScrollPosition: true` option', async () => {
260
+ navigationStack = new NavigationStack(new WebBrowserSession(), {
261
+ maintainScrollPosition: true,
262
+ });
263
+
264
+ navigationStack.init();
265
+
266
+ navigationStack.locationRendered();
267
+
268
+ navigationStack.push('/new');
269
+
270
+ navigationStack.locationRendered();
271
+
272
+ const scrollableContainer = document.createElement('div');
273
+ document.body.appendChild(scrollableContainer);
274
+
275
+ const removeScrollableContainer = navigationStack.addScrollableContainer(
276
+ 'container',
277
+ scrollableContainer,
278
+ );
279
+
280
+ // `PageScrollPositionSetter` works in an asynchronous fashion,
281
+ // so this delay lets it finish setting page scroll position
282
+ // before proceeding to next location.
283
+ await delay(20);
284
+
285
+ removeScrollableContainer();
286
+
287
+ navigationStack.shift(-1);
288
+
289
+ navigationStack.locationRendered();
290
+
291
+ // `PageScrollPositionSetter` works in an asynchronous fashion,
292
+ // so this delay lets it finish setting page scroll position
293
+ // before proceeding to next location.
294
+ await delay(20);
295
+ });
296
+ });
@@ -1,5 +1,5 @@
1
- import LocationDataStorage from '../src/LocationDataStorage';
2
- import MemorySession from '../src/session/MemorySession';
1
+ import LocationDataStorage from '../../src/data-storage/LocationDataStorage';
2
+ import InMemorySession from '../../src/session/InMemorySession';
3
3
 
4
4
  describe('LocationDataStorage', () => {
5
5
  let session;
@@ -12,7 +12,7 @@ describe('LocationDataStorage', () => {
12
12
  beforeEach(() => {
13
13
  window.sessionStorage.clear();
14
14
 
15
- session = new MemorySession('/initial-location');
15
+ session = new InMemorySession();
16
16
  stateStorage = new LocationDataStorage(session, {
17
17
  namespace: 'test',
18
18
  });
@@ -0,0 +1,8 @@
1
+ import * as exports from '../../src/data-storage';
2
+
3
+ describe('index', () => {
4
+ it('should export top level correctly', () => {
5
+ expect(exports.DataStorage).to.exist();
6
+ expect(exports.LocationDataStorage).to.exist();
7
+ });
8
+ });
package/test/index.js CHANGED
@@ -1,7 +1,19 @@
1
1
  import dirtyChai from 'dirty-chai';
2
2
 
3
+ // `dirty-chai` package is used to create functions like `.to.be.true()`
4
+ // from `chai` `expect` properties like `.to.be.true`.
5
+ //
6
+ // The rationale is that the latter form — i.e. when not using `dirty-chai` —
7
+ // is prone to typos like `.to.be.ture` which wouldn't throw any error and the test would still pass.
8
+ // Contrary to that, `.to.be.ture()` notation would catch the typo and would throw an error.
9
+ //
10
+ // https://stackoverflow.com/questions/54332284/what-exactly-does-dirty-chai-js-do
3
11
  global.chai.use(dirtyChai);
4
12
 
13
+ // Ensure all files in src folder are loaded for proper code coverage analysis.
14
+ const srcContext = require.context('../src', true, /.*\.js$/);
15
+ srcContext.keys().forEach(srcContext);
16
+
5
17
  // const testsContext = import.meta.webpackContext('.', true, /\.test\.js$/);
6
18
  const testsContext = require.context('.', true, /\.test\.js$/);
7
19
  testsContext.keys().forEach(testsContext);
@@ -2,8 +2,6 @@ import * as exports from '../src';
2
2
 
3
3
  describe('index', () => {
4
4
  it('should export top level correctly', () => {
5
- expect(exports.Actions).to.exist();
6
- expect(exports.ActionTypes).to.exist();
7
5
  expect(exports.addBasePath).to.exist();
8
6
  expect(exports.removeBasePath).to.exist();
9
7
  expect(exports.getLocationUrl).to.exist();
@@ -11,10 +9,13 @@ describe('index', () => {
11
9
  expect(exports.addNavigationBlocker).to.exist();
12
10
  expect(exports.getLocationUrl).to.exist();
13
11
  expect(exports.parseLocationUrl).to.exist();
14
- expect(exports.createMiddlewares).to.exist();
15
- expect(exports.locationReducer).to.exist();
16
- expect(exports.BrowserSession).to.exist();
17
- expect(exports.MemorySession).to.exist();
18
- expect(exports.ServerSession).to.exist();
12
+ expect(exports.parseInputLocation).to.exist();
13
+ expect(exports.NavigationStack).to.exist();
14
+ expect(exports.Session).to.exist();
15
+ expect(exports.InMemorySession).to.exist();
16
+ expect(exports.WebBrowserSession).to.exist();
17
+ expect(exports.ServerSideRenderSession).to.exist();
18
+ expect(exports.ServerSideNavigationError).to.exist();
19
+ expect(exports.NavigationOutOfBoundsError).to.exist();
19
20
  });
20
21
  });
@@ -1,8 +1,5 @@
1
- import ActionTypes from '../src/ActionTypes';
2
-
3
- export function shouldWarn(about) {
4
- console.warn.expected.push(about); // eslint-disable-line no-console
5
- }
1
+ import ActionTypes from '../src/redux/ActionTypes';
2
+ import ActionTypesInternal from '../src/redux/ActionTypesInternal';
6
3
 
7
4
  export function invokeLocationMiddleware(middleware, action) {
8
5
  let result;
@@ -19,16 +16,16 @@ export function invokeLocationMiddleware(middleware, action) {
19
16
  export function transformInputLocationUsingMiddleware(middleware, location) {
20
17
  return invokeLocationMiddleware(middleware, {
21
18
  type: ActionTypes.NAVIGATE,
22
- payload: location,
23
- }).payload;
19
+ payload: {
20
+ operation: 'push',
21
+ location,
22
+ },
23
+ }).payload.location;
24
24
  }
25
25
 
26
- export function transformSubscriptionLocationUsingMiddleware(
27
- middleware,
28
- location,
29
- ) {
26
+ export function transformOutputLocationUsingMiddleware(middleware, location) {
30
27
  return invokeLocationMiddleware(middleware, {
31
- type: ActionTypes.UPDATE,
28
+ type: ActionTypesInternal.INTERNAL_LOCATION_UPDATE,
32
29
  payload: location,
33
30
  }).payload;
34
31
  }
@@ -1,9 +1,9 @@
1
- import normalizeInputLocation from '../src/normalizeInputLocation';
1
+ import parseInputLocation from '../src/parseInputLocation';
2
2
 
3
- describe('normalizeInputLocation', () => {
3
+ describe('parseInputLocation', () => {
4
4
  it('should create `query` from `search`', () => {
5
5
  expect(
6
- normalizeInputLocation({
6
+ parseInputLocation({
7
7
  pathname: '/foo',
8
8
  search: '?bar=baz',
9
9
  hash: '#qux',
@@ -20,7 +20,7 @@ describe('normalizeInputLocation', () => {
20
20
 
21
21
  it('should add default `search` and `hash`', () => {
22
22
  expect(
23
- normalizeInputLocation({
23
+ parseInputLocation({
24
24
  pathname: '/new/pathname',
25
25
  }),
26
26
  ).to.eql({
@@ -32,14 +32,14 @@ describe('normalizeInputLocation', () => {
32
32
  });
33
33
 
34
34
  it('should parse location URL', () => {
35
- expect(normalizeInputLocation('/foo')).to.eql({
35
+ expect(parseInputLocation('/foo')).to.eql({
36
36
  pathname: '/foo',
37
37
  search: '',
38
38
  query: {},
39
39
  hash: '',
40
40
  });
41
41
 
42
- expect(normalizeInputLocation('/foo?bar=baz')).to.eql({
42
+ expect(parseInputLocation('/foo?bar=baz')).to.eql({
43
43
  pathname: '/foo',
44
44
  search: '?bar=baz',
45
45
  query: {
@@ -48,14 +48,14 @@ describe('normalizeInputLocation', () => {
48
48
  hash: '',
49
49
  });
50
50
 
51
- expect(normalizeInputLocation('/foo#qux')).to.eql({
51
+ expect(parseInputLocation('/foo#qux')).to.eql({
52
52
  pathname: '/foo',
53
53
  search: '',
54
54
  query: {},
55
55
  hash: '#qux',
56
56
  });
57
57
 
58
- expect(normalizeInputLocation('/foo?bar=baz#qux')).to.eql({
58
+ expect(parseInputLocation('/foo?bar=baz#qux')).to.eql({
59
59
  pathname: '/foo',
60
60
  search: '?bar=baz',
61
61
  query: {
@@ -67,7 +67,7 @@ describe('normalizeInputLocation', () => {
67
67
 
68
68
  it('should create `search` from `query` when `search` is not present', () => {
69
69
  expect(
70
- normalizeInputLocation({
70
+ parseInputLocation({
71
71
  pathname: '/foo',
72
72
  query: { bar: 'baz' },
73
73
  hash: '#qux',
@@ -1,10 +1,11 @@
1
- import ActionTypes from '../src/ActionTypes';
2
- import Actions from '../src/Actions';
1
+ import ActionTypes from '../../src/redux/ActionTypes';
2
+ import Actions from '../../src/redux/Actions';
3
3
 
4
4
  describe('Actions', () => {
5
5
  it('#init should create an INIT action', () => {
6
- expect(Actions.init()).to.eql({
6
+ expect(Actions.init('/foo?bar=baz#qux')).to.eql({
7
7
  type: ActionTypes.INIT,
8
+ payload: '/foo?bar=baz#qux',
8
9
  });
9
10
  });
10
11
 
@@ -64,9 +65,9 @@ describe('Actions', () => {
64
65
  });
65
66
  });
66
67
 
67
- it('#dispose should create a DISPOSE action', () => {
68
- expect(Actions.dispose()).to.eql({
69
- type: ActionTypes.DISPOSE,
68
+ it('#stop should create a STOP action', () => {
69
+ expect(Actions.stop()).to.eql({
70
+ type: ActionTypes.STOP,
70
71
  });
71
72
  });
72
73
  });
@@ -1,4 +1,4 @@
1
- import ActionTypes from '../src/ActionTypes';
1
+ import ActionTypes from '../../src/redux/ActionTypes';
2
2
 
3
3
  describe('ActionTypes', () => {
4
4
  it('should have the correct exports', () => {
@@ -8,6 +8,6 @@ describe('ActionTypes', () => {
8
8
  expect(ActionTypes.NAVIGATE).to.exist();
9
9
  expect(ActionTypes.SHIFT).to.exist();
10
10
  expect(ActionTypes.UPDATE).to.exist();
11
- expect(ActionTypes.DISPOSE).to.exist();
11
+ expect(ActionTypes.STOP).to.exist();
12
12
  });
13
13
  });