navigation-stack 0.5.3 → 0.6.1

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 (210) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +144 -282
  3. package/karma.conf.cjs +1 -1
  4. package/lib/cjs/NavigationStack.js +138 -49
  5. package/lib/cjs/data-storage/DataStorage.js +7 -6
  6. package/lib/cjs/environment/InMemoryEnvironment.js +6 -0
  7. package/lib/cjs/{session/ServerSideRenderSession.js → environment/ServerSideRenderEnvironment.js} +5 -6
  8. package/lib/cjs/environment/WebBrowserEnvironment.js +6 -0
  9. package/lib/cjs/environment/log/InMemoryLog.js +23 -0
  10. package/lib/cjs/environment/log/WebBrowserLog.js +22 -0
  11. package/lib/cjs/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  12. package/lib/cjs/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  13. package/lib/cjs/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  14. package/lib/cjs/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +2 -2
  15. package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  16. package/lib/cjs/getLocationBaseFromLocation.js +14 -0
  17. package/lib/cjs/getLocationUrl.js +3 -5
  18. package/lib/cjs/index.js +10 -16
  19. package/lib/cjs/navigationBlockers.js +34 -32
  20. package/lib/cjs/navigationBlockersEvaluation.js +150 -0
  21. package/lib/cjs/parseInputLocation.js +2 -2
  22. package/lib/cjs/parseQueryFromSearch.js +3 -6
  23. package/lib/cjs/parseQueryString.js +77 -0
  24. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +7 -6
  25. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +31 -27
  26. package/lib/cjs/scroll-position/ScrollPositionSaver.js +6 -4
  27. package/lib/cjs/session/Session.js +61 -26
  28. package/lib/cjs/session/subscription/Subscription.js +36 -18
  29. package/lib/cjs/stringifyQuery.js +66 -0
  30. package/lib/cjs/stringifyQueryAsSearch.js +14 -0
  31. package/lib/esm/NavigationStack.js +138 -49
  32. package/lib/esm/data-storage/DataStorage.js +7 -6
  33. package/lib/esm/environment/InMemoryEnvironment.js +6 -0
  34. package/lib/esm/environment/ServerSideRenderEnvironment.js +10 -0
  35. package/lib/esm/environment/WebBrowserEnvironment.js +6 -0
  36. package/lib/esm/environment/log/InMemoryLog.js +17 -0
  37. package/lib/esm/environment/log/WebBrowserLog.js +16 -0
  38. package/lib/esm/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  39. package/lib/esm/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  40. package/lib/esm/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  41. package/lib/esm/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
  42. package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  43. package/lib/esm/getLocationBaseFromLocation.js +9 -0
  44. package/lib/esm/getLocationUrl.js +2 -5
  45. package/lib/esm/index.js +5 -8
  46. package/lib/esm/navigationBlockers.js +34 -32
  47. package/lib/esm/navigationBlockersEvaluation.js +145 -0
  48. package/lib/esm/parseInputLocation.js +2 -2
  49. package/lib/esm/parseQueryFromSearch.js +2 -6
  50. package/lib/esm/parseQueryString.js +72 -0
  51. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +7 -6
  52. package/lib/esm/scroll-position/ScrollPositionRestoration.js +31 -27
  53. package/lib/esm/scroll-position/ScrollPositionSaver.js +6 -4
  54. package/lib/esm/session/Session.js +61 -26
  55. package/lib/esm/session/subscription/Subscription.js +36 -18
  56. package/lib/esm/stringifyQuery.js +61 -0
  57. package/lib/esm/stringifyQueryAsSearch.js +8 -0
  58. package/lib/index.d.ts +180 -34
  59. package/package.json +4 -7
  60. package/src/NavigationStack.js +166 -56
  61. package/src/data-storage/DataStorage.js +9 -6
  62. package/src/environment/InMemoryEnvironment.js +6 -0
  63. package/src/environment/ServerSideRenderEnvironment.js +10 -0
  64. package/src/environment/WebBrowserEnvironment.js +6 -0
  65. package/src/environment/log/InMemoryLog.js +20 -0
  66. package/src/environment/log/WebBrowserLog.js +18 -0
  67. package/src/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  68. package/src/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  69. package/src/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  70. package/src/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
  71. package/src/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  72. package/src/getLocationBaseFromLocation.js +7 -0
  73. package/src/getLocationUrl.js +2 -5
  74. package/src/index.js +10 -13
  75. package/src/navigationBlockers.js +55 -34
  76. package/src/navigationBlockersEvaluation.js +161 -0
  77. package/src/parseInputLocation.js +2 -2
  78. package/src/parseQueryFromSearch.js +2 -6
  79. package/src/parseQueryString.js +81 -0
  80. package/src/scroll-position/ScrollPositionAutoSaver.js +10 -6
  81. package/src/scroll-position/ScrollPositionRestoration.js +36 -30
  82. package/src/scroll-position/ScrollPositionSaver.js +6 -4
  83. package/src/scroll-position/index.js +1 -1
  84. package/src/session/Session.js +68 -24
  85. package/src/session/subscription/Subscription.js +36 -11
  86. package/src/stringifyQuery.js +71 -0
  87. package/src/stringifyQueryAsSearch.js +9 -0
  88. package/test/NavigationStack.addBasePath.test.js +50 -0
  89. package/test/{redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockNonProgrammaticNavigationIfRequired.test.js} +51 -63
  90. package/test/{redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockProgrammaticNavigationIfRequired.test.js} +98 -78
  91. package/test/NavigationStack.general.test.js +68 -0
  92. package/test/NavigationStack.parseInputLocation.test.js +52 -0
  93. package/test/NavigationStack.removeBasePath.test.js +69 -0
  94. package/test/NavigationStack.test.js +97 -29
  95. package/test/data-storage/LocationDataStorage.test.js +3 -2
  96. package/test/index.js +7 -31
  97. package/test/index.test.js +4 -5
  98. package/test/parseQueryFromSearch.test.js +19 -0
  99. package/test/parseQueryString.test.js +18 -0
  100. package/test/scroll-position/ScrollPositionRestoration.test.js +34 -13
  101. package/test/scroll-position/createApp.js +8 -8
  102. package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +4 -4
  103. package/test/session/{InMemorySession.test.js → Session.InMemoryEnvironment.test.js} +10 -9
  104. package/test/session/{ServerSession.test.js → Session.ServerSideRenderEnvironment.test.js} +5 -4
  105. package/test/session/{WebBrowserSession.test.js → Session.WebBrowserEnvironment.test.js} +63 -13
  106. package/test/shouldWarn.js +44 -0
  107. package/test/stringifyQuery.test.js +65 -0
  108. package/types/index.d.ts +180 -34
  109. package/types/tsconfig.json +0 -1
  110. package/data-storage/package.json +0 -7
  111. package/lib/cjs/createSearchFromQuery.js +0 -13
  112. package/lib/cjs/debug.js +0 -12
  113. package/lib/cjs/redux/ActionTypes.js +0 -14
  114. package/lib/cjs/redux/ActionTypesInternal.js +0 -8
  115. package/lib/cjs/redux/Actions.js +0 -28
  116. package/lib/cjs/redux/createMiddlewares.js +0 -60
  117. package/lib/cjs/redux/index.js +0 -13
  118. package/lib/cjs/redux/internalLocationReducer.js +0 -14
  119. package/lib/cjs/redux/locationReducer.js +0 -13
  120. package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -32
  121. package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -113
  122. package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
  123. package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -30
  124. package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -73
  125. package/lib/cjs/redux/middleware/navigationOperationMiddleware.js +0 -40
  126. package/lib/cjs/redux/middleware/parseInputLocationMiddleware.js +0 -29
  127. package/lib/cjs/redux/middleware/updateLocationMiddleware.js +0 -34
  128. package/lib/cjs/session/InMemorySession.js +0 -22
  129. package/lib/cjs/session/WebBrowserSession.js +0 -20
  130. package/lib/data-storage/index.d.ts +0 -35
  131. package/lib/esm/createSearchFromQuery.js +0 -8
  132. package/lib/esm/debug.js +0 -7
  133. package/lib/esm/redux/ActionTypes.js +0 -9
  134. package/lib/esm/redux/ActionTypesInternal.js +0 -3
  135. package/lib/esm/redux/Actions.js +0 -22
  136. package/lib/esm/redux/createMiddlewares.js +0 -54
  137. package/lib/esm/redux/index.js +0 -4
  138. package/lib/esm/redux/internalLocationReducer.js +0 -8
  139. package/lib/esm/redux/locationReducer.js +0 -7
  140. package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
  141. package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -108
  142. package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -88
  143. package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -25
  144. package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -68
  145. package/lib/esm/redux/middleware/navigationOperationMiddleware.js +0 -35
  146. package/lib/esm/redux/middleware/parseInputLocationMiddleware.js +0 -24
  147. package/lib/esm/redux/middleware/updateLocationMiddleware.js +0 -28
  148. package/lib/esm/session/InMemorySession.js +0 -15
  149. package/lib/esm/session/ServerSideRenderSession.js +0 -11
  150. package/lib/esm/session/WebBrowserSession.js +0 -13
  151. package/lib/redux/index.d.ts +0 -90
  152. package/lib/scroll-position/index.d.ts +0 -107
  153. package/redux/package.json +0 -7
  154. package/scroll-position/package.json +0 -7
  155. package/src/createSearchFromQuery.js +0 -9
  156. package/src/debug.js +0 -8
  157. package/src/redux/ActionTypes.js +0 -9
  158. package/src/redux/ActionTypesInternal.js +0 -3
  159. package/src/redux/Actions.js +0 -27
  160. package/src/redux/createMiddlewares.js +0 -65
  161. package/src/redux/index.js +0 -4
  162. package/src/redux/internalLocationReducer.js +0 -9
  163. package/src/redux/locationReducer.js +0 -8
  164. package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
  165. package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -119
  166. package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
  167. package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -26
  168. package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -72
  169. package/src/redux/middleware/navigationOperationMiddleware.js +0 -34
  170. package/src/redux/middleware/parseInputLocationMiddleware.js +0 -23
  171. package/src/redux/middleware/updateLocationMiddleware.js +0 -28
  172. package/src/session/InMemorySession.js +0 -13
  173. package/src/session/ServerSideRenderSession.js +0 -9
  174. package/src/session/WebBrowserSession.js +0 -13
  175. package/test/middlewareTestUtil.js +0 -31
  176. package/test/redux/Action.test.js +0 -73
  177. package/test/redux/ActionTypes.test.js +0 -13
  178. package/test/redux/createMiddlewares.test.js +0 -96
  179. package/test/redux/index.test.js +0 -10
  180. package/test/redux/locationReducer.test.js +0 -39
  181. package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +0 -40
  182. package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +0 -51
  183. package/test/redux/middleware/navigationOperationMiddleware.test.js +0 -78
  184. package/test/redux/middleware/parseInputLocationMiddleware.test.js +0 -62
  185. package/test/testUtil.js +0 -3
  186. package/types/data-storage/index.d.ts +0 -35
  187. package/types/redux/index.d.ts +0 -90
  188. package/types/scroll-position/index.d.ts +0 -107
  189. /package/lib/cjs/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  190. /package/lib/cjs/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  191. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  192. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  193. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  194. /package/lib/cjs/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  195. /package/lib/cjs/{session → environment}/navigation/operation/operations.js +0 -0
  196. /package/lib/esm/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  197. /package/lib/esm/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  198. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  199. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  200. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  201. /package/lib/esm/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  202. /package/lib/esm/{session → environment}/navigation/operation/operations.js +0 -0
  203. /package/src/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  204. /package/src/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  205. /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  206. /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  207. /package/src/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  208. /package/src/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  209. /package/src/{session → environment}/navigation/operation/operations.js +0 -0
  210. /package/test/{parseInputLocationMiddleware.test.js → parseInputLocation.test.js} +0 -0
@@ -1,14 +1,14 @@
1
1
  import delay from 'delay';
2
2
 
3
3
  import NavigationStack from '../src/NavigationStack';
4
- import InMemorySession from '../src/session/InMemorySession';
5
- import WebBrowserSession from '../src/session/WebBrowserSession';
4
+ import InMemoryEnvironment from '../src/environment/InMemoryEnvironment';
5
+ import WebBrowserEnvironment from '../src/environment/WebBrowserEnvironment';
6
6
 
7
7
  describe('NavigationStack', () => {
8
8
  let navigationStack;
9
9
 
10
10
  beforeEach(() => {
11
- navigationStack = new NavigationStack(new InMemorySession());
11
+ navigationStack = new NavigationStack(InMemoryEnvironment);
12
12
  navigationStack.init('/initial');
13
13
  });
14
14
 
@@ -54,7 +54,7 @@ describe('NavigationStack (WebBrowserSession)', () => {
54
54
  beforeEach(() => {
55
55
  window.history.replaceState(null, null, '/initial');
56
56
 
57
- navigationStack = new NavigationStack(new WebBrowserSession());
57
+ navigationStack = new NavigationStack(WebBrowserEnvironment);
58
58
 
59
59
  navigationStack.init();
60
60
  });
@@ -68,21 +68,21 @@ describe('NavigationStack (WebBrowserSession)', () => {
68
68
 
69
69
  it('should allow calling `init()` without an argument, and then support `push` and `shift` navigation operations', async () => {
70
70
  navigationStack.push('/new');
71
- await delay(20);
71
+ await delay(100);
72
72
  expect(navigationStack.current()).to.include({
73
73
  pathname: '/new',
74
74
  index: 1,
75
75
  });
76
76
 
77
77
  navigationStack.shift(-1);
78
- await delay(20);
78
+ await delay(100);
79
79
  expect(navigationStack.current()).to.include({
80
80
  pathname: '/initial',
81
81
  index: 0,
82
82
  });
83
83
 
84
84
  navigationStack.shift(+1);
85
- await delay(20);
85
+ await delay(100);
86
86
  expect(navigationStack.current()).to.include({
87
87
  pathname: '/new',
88
88
  index: 1,
@@ -91,12 +91,81 @@ describe('NavigationStack (WebBrowserSession)', () => {
91
91
 
92
92
  it('should allow calling `init()` without an argument, and then support `replace` navigation operation', async () => {
93
93
  navigationStack.replace('/new');
94
- await delay(20);
94
+ await delay(100);
95
95
  expect(navigationStack.current()).to.include({
96
96
  pathname: '/new',
97
97
  index: 0,
98
98
  });
99
99
  });
100
+
101
+ it('should not allow calling `init()` multiple times', () => {
102
+ expect(() => {
103
+ navigationStack.init('/new');
104
+ }).to.throw('Already initialized');
105
+ });
106
+
107
+ it('should restart a previously-started session', async () => {
108
+ navigationStack.push('/new');
109
+ navigationStack.stop();
110
+
111
+ // Simulate the user refreshing the page in a web browser.
112
+ // In such case, `window.history` will be retained
113
+ // but every javascript variable will be recreated from scratch.
114
+ navigationStack = new NavigationStack(WebBrowserEnvironment);
115
+
116
+ navigationStack.init();
117
+
118
+ // A publicly-exposed `location` object doesn't have an `operation` property.
119
+ // expect(navigationStack.current().operation).to.equal('init');
120
+
121
+ expect(navigationStack.current().pathname).to.equal('/new');
122
+ expect(navigationStack.current().index).to.equal(1);
123
+
124
+ navigationStack.shift(-1);
125
+
126
+ await delay(100);
127
+
128
+ expect(navigationStack.current().pathname).to.equal('/initial');
129
+ expect(navigationStack.current().index).to.equal(0);
130
+
131
+ navigationStack.shift(1);
132
+
133
+ await delay(100);
134
+
135
+ expect(navigationStack.current().pathname).to.equal('/new');
136
+ expect(navigationStack.current().index).to.equal(1);
137
+ });
138
+ });
139
+
140
+ describe('NavigationStack.addNavigationBlocker', () => {
141
+ let navigationStack;
142
+
143
+ afterEach(() => {
144
+ // Even if a test errors, the `NavigationStack` should still be stopped
145
+ // in order to remove the potential "popstate" listener so that it doesn't
146
+ // interfere with other tests.
147
+ navigationStack.stop();
148
+ });
149
+
150
+ it('should add/remove navigation blocker', () => {
151
+ navigationStack = new NavigationStack(InMemoryEnvironment);
152
+
153
+ navigationStack.init('/initial');
154
+
155
+ expect(navigationStack.current().pathname).to.equal('/initial');
156
+
157
+ const removeNavigationBlocker = navigationStack.addNavigationBlocker(
158
+ () => true,
159
+ );
160
+
161
+ navigationStack.push('/new');
162
+ expect(navigationStack.current().pathname).to.equal('/initial');
163
+
164
+ removeNavigationBlocker();
165
+
166
+ navigationStack.push('/new');
167
+ expect(navigationStack.current().pathname).to.equal('/new');
168
+ });
100
169
  });
101
170
 
102
171
  describe('NavigationStack.subscribe', () => {
@@ -110,7 +179,7 @@ describe('NavigationStack.subscribe', () => {
110
179
  });
111
180
 
112
181
  it('should subscribe to location changes', () => {
113
- navigationStack = new NavigationStack(new InMemorySession());
182
+ navigationStack = new NavigationStack(InMemoryEnvironment);
114
183
 
115
184
  const listener = sinon.spy();
116
185
 
@@ -165,7 +234,7 @@ describe('NavigationStack.subscribe', () => {
165
234
  it('should subscribe to location changes (WebBrowserSession)', () => {
166
235
  window.history.replaceState(null, null, '/initial');
167
236
 
168
- navigationStack = new NavigationStack(new WebBrowserSession());
237
+ navigationStack = new NavigationStack(WebBrowserEnvironment);
169
238
 
170
239
  const listener = sinon.spy();
171
240
 
@@ -198,9 +267,7 @@ describe('NavigationStack.stop()', () => {
198
267
  sandbox.spy(window, 'addEventListener');
199
268
  sandbox.spy(window, 'removeEventListener');
200
269
 
201
- const session = new WebBrowserSession();
202
-
203
- const navigationStack = new NavigationStack(session);
270
+ const navigationStack = new NavigationStack(WebBrowserEnvironment);
204
271
 
205
272
  // This subscription won't be "unsubscribed" by the code.
206
273
  // It is expected to be "unsubscribed" automatically on `navigationStack.stop()`
@@ -237,12 +304,13 @@ describe('NavigationStack', () => {
237
304
  });
238
305
 
239
306
  it('should support `basePath`', () => {
240
- const session = new InMemorySession();
241
-
242
- navigationStack = new NavigationStack(session, {
307
+ navigationStack = new NavigationStack(InMemoryEnvironment, {
243
308
  basePath: '/base',
244
309
  });
245
310
 
311
+ // eslint-disable-next-line no-underscore-dangle
312
+ const session = navigationStack._session;
313
+
246
314
  navigationStack.init('/initial');
247
315
 
248
316
  navigationStack.push('/new');
@@ -257,7 +325,7 @@ describe('NavigationStack', () => {
257
325
  });
258
326
  });
259
327
 
260
- describe('NavigationStack (maintainScrollPosition: true)', () => {
328
+ describe('NavigationStack (manageScrollPosition: true)', () => {
261
329
  let navigationStack;
262
330
 
263
331
  afterEach(() => {
@@ -270,9 +338,9 @@ describe('NavigationStack (maintainScrollPosition: true)', () => {
270
338
  navigationStack.stop();
271
339
  });
272
340
 
273
- it('should support `maintainScrollPosition: true` option', async () => {
274
- navigationStack = new NavigationStack(new WebBrowserSession(), {
275
- maintainScrollPosition: true,
341
+ it('should support `manageScrollPosition: true` option', async () => {
342
+ navigationStack = new NavigationStack(WebBrowserEnvironment, {
343
+ manageScrollPosition: true,
276
344
  });
277
345
 
278
346
  // Start with the "/initial" page.
@@ -283,7 +351,7 @@ describe('NavigationStack (maintainScrollPosition: true)', () => {
283
351
 
284
352
  // "/initial" page rendered.
285
353
  // Restore scroll position (no saved scroll position to restore).
286
- await navigationStack.locationRendered();
354
+ await navigationStack.locationRendered(navigationStack.current());
287
355
 
288
356
  // Create a content <div/> that "overflows" the window so that it becomes scrollable.
289
357
  const content = document.createElement('div');
@@ -301,7 +369,7 @@ describe('NavigationStack (maintainScrollPosition: true)', () => {
301
369
  // Wait a bit for `ScrollPositionRestoration` to save the scroll position
302
370
  // because it does that "asynchronously", i.e. in an "immediate" timeout
303
371
  // as a way of "throttling" scroll events.
304
- await delay(20);
372
+ await delay(100);
305
373
 
306
374
  // Go to "/new" page.
307
375
  navigationStack.push('/new');
@@ -319,7 +387,7 @@ describe('NavigationStack (maintainScrollPosition: true)', () => {
319
387
 
320
388
  // "/new" page rendered.
321
389
  // Restore scroll position (no saved scroll position to restore).
322
- await navigationStack.locationRendered();
390
+ await navigationStack.locationRendered(navigationStack.current());
323
391
 
324
392
  // It should've reset page scroll position.
325
393
  expect(window.pageYOffset).to.equal(0);
@@ -341,7 +409,7 @@ describe('NavigationStack (maintainScrollPosition: true)', () => {
341
409
  // Wait a bit for `ScrollPositionRestoration` to save the scroll position
342
410
  // because it does that "asynchronously", i.e. in an "immediate" timeout
343
411
  // as a way of "throttling" scroll events.
344
- await delay(20);
412
+ await delay(100);
345
413
 
346
414
  // Register the scrollable container on the "/new" page.
347
415
  const untrackScrollableContainer = navigationStack.addScrollableContainer(
@@ -354,7 +422,7 @@ describe('NavigationStack (maintainScrollPosition: true)', () => {
354
422
 
355
423
  // "/new-2" page rendered.
356
424
  // Restore scroll position (no saved scroll position to restore).
357
- await navigationStack.locationRendered();
425
+ await navigationStack.locationRendered(navigationStack.current());
358
426
 
359
427
  // Check that it has reset the scroll position inside the scrollable container.
360
428
  expect(scrollableContainer.scrollTop).to.equal(0);
@@ -366,11 +434,11 @@ describe('NavigationStack (maintainScrollPosition: true)', () => {
366
434
  navigationStack.shift(-1);
367
435
 
368
436
  // Wait for the web browser to emit a "popstate" event from `window.history.go(-1)` navigation.
369
- await delay(20);
437
+ await delay(100);
370
438
 
371
439
  // "/new" page rendered.
372
440
  // Restore scroll position.
373
- await navigationStack.locationRendered();
441
+ await navigationStack.locationRendered(navigationStack.current());
374
442
 
375
443
  // Check that it has restored the scroll position inside the scrollable container.
376
444
  expect(scrollableContainer.scrollTop).to.be.closeTo(1000, 0.5);
@@ -382,7 +450,7 @@ describe('NavigationStack (maintainScrollPosition: true)', () => {
382
450
  navigationStack.shift(-1);
383
451
 
384
452
  // Wait for the web browser to emit a "popstate" event from `window.history.go(-1)` navigation.
385
- await delay(20);
453
+ await delay(100);
386
454
 
387
455
  // The scrollable container is only present at the "/new" or "/new-2" pages so remove it now.
388
456
  document.body.removeChild(scrollableContainer);
@@ -391,7 +459,7 @@ describe('NavigationStack (maintainScrollPosition: true)', () => {
391
459
 
392
460
  // "/initial" page rendered.
393
461
  // Restore scroll position.
394
- await navigationStack.locationRendered();
462
+ await navigationStack.locationRendered(navigationStack.current());
395
463
 
396
464
  // Check that it has restored page scroll position.
397
465
  expect(window.pageYOffset).to.be.closeTo(1000, 0.5);
@@ -1,5 +1,6 @@
1
1
  import LocationDataStorage from '../../src/data-storage/LocationDataStorage';
2
- import InMemorySession from '../../src/session/InMemorySession';
2
+ import InMemoryEnvironment from '../../src/environment/InMemoryEnvironment';
3
+ import Session from '../../src/session/Session';
3
4
 
4
5
  describe('LocationDataStorage', () => {
5
6
  let session;
@@ -12,7 +13,7 @@ describe('LocationDataStorage', () => {
12
13
  beforeEach(() => {
13
14
  window.sessionStorage.clear();
14
15
 
15
- session = new InMemorySession();
16
+ session = new Session(InMemoryEnvironment);
16
17
  stateStorage = new LocationDataStorage(session, {
17
18
  namespace: 'test',
18
19
  });
package/test/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import dirtyChai from 'dirty-chai';
2
2
 
3
+ import { stubWarn } from './shouldWarn';
4
+
3
5
  // `dirty-chai` package is used to create functions like `.to.be.true()`
4
6
  // from `chai` `expect` properties like `.to.be.true`.
5
7
  //
@@ -18,39 +20,13 @@ srcContext.keys().forEach(srcContext);
18
20
  const testsContext = require.context('.', true, /\.test\.js$/);
19
21
  testsContext.keys().forEach(testsContext);
20
22
 
21
- beforeEach(() => {
22
- /* eslint-disable no-console */
23
- sinon.stub(console, 'warn').callsFake((message) => {
24
- let expected = false;
25
-
26
- console.warn.expected.forEach((about) => {
27
- if (message.includes(about)) {
28
- console.warn.warned[about] = true;
29
- expected = true;
30
- }
31
- });
32
-
33
- if (expected) {
34
- return;
35
- }
23
+ let unstubWarn;
36
24
 
37
- console.warn.threw = true;
38
- throw new Error(message);
39
- });
40
-
41
- console.warn.expected = [];
42
- console.warn.warned = Object.create(null);
43
- console.warn.threw = false;
44
- /* eslint-enable no-console */
25
+ beforeEach(() => {
26
+ unstubWarn = stubWarn();
45
27
  });
46
28
 
47
29
  afterEach(() => {
48
- /* eslint-disable no-console */
49
- const { expected, warned, threw } = console.warn;
50
- console.warn.restore();
51
-
52
- if (!threw && expected.length) {
53
- expect(warned).to.have.keys(expected);
54
- }
55
- /* eslint-enable no-console */
30
+ unstubWarn();
31
+ unstubWarn = undefined;
56
32
  });
@@ -11,11 +11,10 @@ describe('index', () => {
11
11
  expect(exports.parseLocationUrl).to.exist();
12
12
  expect(exports.parseInputLocation).to.exist();
13
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();
14
+ expect(exports.InMemoryEnvironment).to.exist();
15
+ expect(exports.WebBrowserEnvironment).to.exist();
16
+ expect(exports.ServerSideRenderEnvironment).to.exist();
17
+ expect(exports.ServerSideRedirectError).to.exist();
19
18
  expect(exports.NavigationOutOfBoundsError).to.exist();
20
19
  });
21
20
  });
@@ -0,0 +1,19 @@
1
+ import parseQueryFromSearch from '../src/parseQueryFromSearch';
2
+
3
+ describe('parseQueryFromSearch', () => {
4
+ it('should parse query object from `location.search', () => {
5
+ expect(parseQueryFromSearch('')).to.deep.equal({});
6
+ expect(parseQueryFromSearch('?')).to.deep.equal({});
7
+ expect(parseQueryFromSearch('?a')).to.deep.equal({ a: null });
8
+ expect(parseQueryFromSearch('?=a')).to.deep.equal({});
9
+ expect(parseQueryFromSearch('?a=b')).to.deep.equal({ a: 'b' });
10
+ expect(parseQueryFromSearch('?a=b&c=d')).to.deep.equal({ a: 'b', c: 'd' });
11
+ expect(parseQueryFromSearch('?a=b+c%20d')).to.deep.equal({ a: 'b c d' });
12
+ expect(parseQueryFromSearch('?a=b+c%20d')).to.deep.equal({ a: 'b c d' });
13
+ // Ivalid percent-encoding is ignored.
14
+ expect(parseQueryFromSearch('?a=b%0&c=d')).to.deep.equal({
15
+ a: 'b%0',
16
+ c: 'd',
17
+ });
18
+ });
19
+ });
@@ -0,0 +1,18 @@
1
+ import parseQueryString from '../src/parseQueryString';
2
+
3
+ describe('parseQueryString', () => {
4
+ it('should parse query object from a query string', () => {
5
+ expect(parseQueryString('')).to.deep.equal({});
6
+ expect(parseQueryString('a')).to.deep.equal({ a: null });
7
+ expect(parseQueryString('=a')).to.deep.equal({});
8
+ expect(parseQueryString('a=b')).to.deep.equal({ a: 'b' });
9
+ expect(parseQueryString('a=b&c=d')).to.deep.equal({ a: 'b', c: 'd' });
10
+ expect(parseQueryString('a=b+c%20d')).to.deep.equal({ a: 'b c d' });
11
+ expect(parseQueryString('a=b+c%20d')).to.deep.equal({ a: 'b c d' });
12
+ // Ivalid percent-encoding is ignored.
13
+ expect(parseQueryString('a=b%0&c=d')).to.deep.equal({
14
+ a: 'b%0',
15
+ c: 'd',
16
+ });
17
+ });
18
+ });
@@ -8,7 +8,7 @@ import delay from './delay';
8
8
  import { setEventListener, triggerEvent } from './mockPageLifecycle';
9
9
  import runApp from './runApp';
10
10
  import withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration from './withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration';
11
- import PageLifecycle from '../../src/session/lifecycle/page-lifecycle/PageLifecycleInstance';
11
+ import PageLifecycle from '../../src/environment/lifecycle/page-lifecycle/PageLifecycleInstance';
12
12
 
13
13
  describe('ScrollPositionRestoration', () => {
14
14
  let unlisten;
@@ -130,16 +130,22 @@ describe('ScrollPositionRestoration', () => {
130
130
  });
131
131
 
132
132
  describe('custom behavior', () => {
133
- it('should allow scroll suppression', (done) => {
133
+ it('should allow disabling scroll position reset when just the URL query parameters change', (done) => {
134
134
  const app = addScrollableContainerWithAnchors(
135
135
  createApp({
136
- shouldSetPageScrollPositionOnLocationChange: (
137
- location,
136
+ shouldChangePageScrollPositionOnLocationChange: (
138
137
  prevLocation,
138
+ location,
139
139
  ) => {
140
- return (
141
- !prevLocation || prevLocation.pathname !== location.pathname
142
- );
140
+ // Is allowed to set the initial scroll position.
141
+ if (!prevLocation) {
142
+ return true;
143
+ }
144
+ // Can set scroll position if the new location has a different `pathname`.
145
+ // If it has the same `pathame`, the scroll position won't be set
146
+ // and the previous one will be retained, hereby avoiding scroll position "jumping"
147
+ // when simply updating URL query parameters.
148
+ return prevLocation.pathname !== location.pathname;
143
149
  },
144
150
  }),
145
151
  );
@@ -151,23 +157,30 @@ describe('ScrollPositionRestoration', () => {
151
157
  () => {
152
158
  scrollTop(window, 5000);
153
159
  delay(() => {
160
+ // Add URL query parameters to the current URL.
161
+ // This shouldn't result in a scroll position reset.
154
162
  app.goTo('/detail?key=value');
155
163
  });
156
164
  },
157
165
  () => {
166
+ // Check that the scroll position hasn't been reset.
167
+ //
158
168
  // Here, it said "expected 4999.7998046875 to equal 5000".
159
169
  // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
160
170
  expect(scrollTop(window)).to.be.closeTo(5000, 0.5);
171
+ // Navigate to some other completely unrelated URL.
161
172
  app.goTo('/');
162
173
  },
163
174
  () => {
175
+ // Check that the scroll position has been reset because it's now a completely
176
+ // different URL and not just the old one with new query parameters.
164
177
  expect(scrollTop(window)).to.equal(0);
165
178
  done();
166
179
  },
167
180
  ]);
168
181
  });
169
182
 
170
- it('should ignore scroll events when `disableSavingScrollPosition()` is used', (done) => {
183
+ it('should stop saving scroll position when `disableSavingScrollPosition()` is called, and should resume saving scroll position when `enableSavingScrollPosition()` is called', (done) => {
171
184
  const app = addScrollableContainerWithAnchors(createApp());
172
185
 
173
186
  unlisten = runApp(app, [
@@ -205,27 +218,34 @@ describe('ScrollPositionRestoration', () => {
205
218
  ]);
206
219
  });
207
220
 
208
- it('should allow custom position', (done) => {
221
+ it('should allow overriding the default source for reading a previously-saved scroll position', (done) => {
209
222
  const app = addScrollableContainerWithAnchors(
210
223
  createApp({
224
+ // Read a previously-saved scroll position not from the default "data storage"
225
+ // but from this "mock-up" which always returns the same scroll position.
211
226
  getSavedPageScrollPositionOnLocationChange: () => [10, 20],
212
227
  }),
213
228
  );
214
229
 
215
230
  unlisten = runApp(app, [
216
231
  () => {
232
+ // Go to some random page.
217
233
  app.goTo('/detail');
218
234
  },
219
235
  () => {
236
+ // Go to some random page again, for no reason.
220
237
  app.goTo('/');
221
238
  },
222
239
  () => {
240
+ // Check that the "mock-up" scroll position has been restored.
241
+ //
223
242
  // Here, it said "expected 9.966666221618652 to equal 10".
224
243
  // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
225
244
  expect(scrollLeft(window)).to.be.closeTo(10, 0.5);
226
245
  // Here, it said "19.933332443237305 to equal 20".
227
246
  // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
228
247
  expect(scrollTop(window)).to.be.closeTo(20, 0.5);
248
+ // Test finished.
229
249
  done();
230
250
  },
231
251
  ]);
@@ -290,7 +310,7 @@ describe('ScrollPositionRestoration', () => {
290
310
  it('should follow browser scroll behavior', (done) => {
291
311
  const { container, ...app } = addScrollableContainer(
292
312
  createApp({
293
- shouldSetPageScrollPositionOnLocationChange: () => false,
313
+ shouldChangePageScrollPositionOnLocationChange: () => false,
294
314
  }),
295
315
  );
296
316
 
@@ -325,7 +345,7 @@ describe('ScrollPositionRestoration', () => {
325
345
  const { container, ...app } =
326
346
  withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration(
327
347
  createApp({
328
- shouldSetPageScrollPositionOnLocationChange: () => false,
348
+ shouldChangePageScrollPositionOnLocationChange: () => false,
329
349
  }),
330
350
  );
331
351
 
@@ -359,7 +379,7 @@ describe('ScrollPositionRestoration', () => {
359
379
  it('should save element scroll position on scroll event, i.e. before navigation is even attempted', (done) => {
360
380
  const app1 = addScrollableContainer(
361
381
  createApp({
362
- shouldSetPageScrollPositionOnLocationChange: () => false,
382
+ shouldChangePageScrollPositionOnLocationChange: () => false,
363
383
  }),
364
384
  );
365
385
 
@@ -374,8 +394,9 @@ describe('ScrollPositionRestoration', () => {
374
394
  const app2 = addScrollableContainer(
375
395
  createApp({
376
396
  // Restore the data of the session of `app1`.
397
+ // That data includes the scroll position.
377
398
  sessionKey: app1.getSessionKey(),
378
- shouldSetPageScrollPositionOnLocationChange: () => false,
399
+ shouldChangePageScrollPositionOnLocationChange: () => false,
379
400
  }),
380
401
  );
381
402
 
@@ -1,11 +1,11 @@
1
1
  import NavigationStack from '../../src/NavigationStack';
2
+ import WebBrowserEnvironment from '../../src/environment/WebBrowserEnvironment';
2
3
  import ScrollPositionRestoration from '../../src/scroll-position/ScrollPositionRestoration';
3
- import WebBrowserSession from '../../src/session/WebBrowserSession';
4
4
 
5
5
  // Creates a website with `ScrollPositionRestoration`.
6
6
  export default function createApp({
7
7
  sessionKey,
8
- shouldSetPageScrollPositionOnLocationChange,
8
+ shouldChangePageScrollPositionOnLocationChange,
9
9
  getSavedPageScrollPositionOnLocationChange,
10
10
  } = {}) {
11
11
  let currentLocation = null;
@@ -56,17 +56,17 @@ export default function createApp({
56
56
  throw new Error('Only one `listener` is allowed in tests');
57
57
  }
58
58
 
59
- session = new WebBrowserSession();
59
+ navigationStack = new NavigationStack(WebBrowserEnvironment);
60
+ // eslint-disable-next-line no-underscore-dangle
61
+ session = navigationStack._session;
60
62
  // There's this one test that restores data of a session of a previous app
61
63
  // and for that the new session just has to have the same key in order to read
62
64
  // the previous app's session data from the environment storage.
63
65
  if (sessionKey) {
64
66
  session.key = sessionKey;
65
67
  }
66
- navigationStack = new NavigationStack(session);
67
68
  scrollPositionRestoration = new ScrollPositionRestoration(session, {
68
- _shouldSetPageScrollPositionOnLocationChange:
69
- shouldSetPageScrollPositionOnLocationChange,
69
+ shouldChangePageScrollPositionOnLocationChange,
70
70
  _getSavedPageScrollPositionOnLocationChange:
71
71
  getSavedPageScrollPositionOnLocationChange,
72
72
  });
@@ -93,8 +93,8 @@ export default function createApp({
93
93
  // Registers a scrollable container on a page.
94
94
  function registerScrollableContainer(key, element, options) {
95
95
  return scrollPositionRestoration.addScrollableContainer(key, element, {
96
- _shouldSetScrollPositionOnLocationChange:
97
- options && options.shouldSetScrollPositionOnLocationChange,
96
+ shouldChangeScrollPositionOnLocationChange:
97
+ options && options.shouldChangeScrollPositionOnLocationChange,
98
98
  _getSavedScrollPositionOnLocationChange:
99
99
  options && options.getSavedScrollPositionOnLocationChange,
100
100
  });
@@ -18,9 +18,9 @@ export default function withScrollableContainerAtIndexPageWithDisabledAutomaticS
18
18
  let unregisterScrollableContainer;
19
19
 
20
20
  function listen(listener) {
21
- function shouldUpdateScrollableContainerScrollPositionForLocation(
22
- location,
21
+ function shouldChangeScrollableContainerScrollPositionOnLocationChange(
23
22
  prevLocation,
23
+ location,
24
24
  ) {
25
25
  // Disable the automatic scroll position restoration on "back" navigation
26
26
  // to check that it automatically restores scroll position when calling
@@ -37,8 +37,8 @@ export default function withScrollableContainerAtIndexPageWithDisabledAutomaticS
37
37
  'container',
38
38
  container,
39
39
  {
40
- shouldSetScrollPositionOnLocationChange:
41
- shouldUpdateScrollableContainerScrollPositionForLocation,
40
+ shouldChangeScrollPositionOnLocationChange:
41
+ shouldChangeScrollableContainerScrollPositionOnLocationChange,
42
42
  },
43
43
  );
44
44
  }