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,435 @@
1
+ import { offset, scrollLeft, scrollTop } from 'dom-helpers';
2
+ import sinon from 'sinon';
3
+
4
+ import addScrollableContainer from './addScrollableContainer';
5
+ import addScrollableContainerWithAnchors from './addScrollableContainerWithAnchors';
6
+ import createApp from './createApp';
7
+ import delay from './delay';
8
+ import { setEventListener, triggerEvent } from './mockPageLifecycle';
9
+ import runApp from './runApp';
10
+ import withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration from './withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration';
11
+ import PageLifecycle from '../../src/session/lifecycle/page-lifecycle/PageLifecycleInstance';
12
+
13
+ describe('ScrollPositionRestoration', () => {
14
+ let unlisten;
15
+
16
+ beforeEach(() => {
17
+ window.history.scrollRestoration = 'auto';
18
+ });
19
+
20
+ afterEach(() => {
21
+ if (unlisten) {
22
+ unlisten();
23
+ }
24
+ sinon.restore();
25
+ setEventListener();
26
+ });
27
+
28
+ it('sets/restores/resets `window.history.scrollRestoration` on freeze/resume', (done) => {
29
+ sinon.replace(PageLifecycle, 'addEventListener', setEventListener);
30
+ const app = createApp();
31
+ expect(window.history.scrollRestoration).to.equal('auto');
32
+ unlisten = runApp(app, [
33
+ () => {
34
+ expect(window.history.scrollRestoration).to.equal('manual');
35
+ triggerEvent('frozen', 'hidden');
36
+ expect(window.history.scrollRestoration).to.equal('manual');
37
+ triggerEvent('hidden', 'frozen');
38
+ expect(window.history.scrollRestoration).to.equal('auto');
39
+ triggerEvent('frozen', 'hidden');
40
+ expect(window.history.scrollRestoration).to.equal('manual');
41
+ done();
42
+ },
43
+ ]);
44
+ });
45
+
46
+ it('sets/restores `window.history.scrollRestoration` on termination', (done) => {
47
+ sinon.replace(PageLifecycle, 'addEventListener', setEventListener);
48
+ expect(window.history.scrollRestoration).to.equal('auto');
49
+ const app = createApp();
50
+ unlisten = runApp(app, [
51
+ () => {
52
+ expect(window.history.scrollRestoration).to.equal('manual');
53
+ triggerEvent('hidden', 'terminated');
54
+ expect(window.history.scrollRestoration).to.equal('auto');
55
+ done();
56
+ },
57
+ ]);
58
+ });
59
+
60
+ describe('default behavior', () => {
61
+ it('should emulate browser scroll behavior', (done) => {
62
+ const app = addScrollableContainerWithAnchors(createApp());
63
+ const child1 = document.getElementById('child1');
64
+ const child2 = document.getElementById('child2-id');
65
+
66
+ unlisten = runApp(app, [
67
+ () => {
68
+ // This scroll will be ignored (overwritten by a subsequent scroll),
69
+ // but it will test the "throttle scroll events" code.
70
+ scrollTop(window, 10000);
71
+
72
+ setTimeout(() => {
73
+ scrollTop(window, 15000);
74
+ delay(() => {
75
+ app.goTo('/detail');
76
+ });
77
+ });
78
+ },
79
+ () => {
80
+ expect(scrollTop(window)).to.equal(0);
81
+ scrollTop(window, 5000);
82
+ delay(app.goBack);
83
+ },
84
+ (location) => {
85
+ expect(location.state).to.not.exist();
86
+ // Here, it said "expected 14999.8330078125 to equal 15000".
87
+ // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
88
+ expect(scrollTop(window)).to.be.closeTo(15000, 0.5);
89
+ app.goTo('/detail#child2');
90
+ },
91
+ () => {
92
+ expect(scrollTop(window)).to.be.closeTo(offset(child2).top, 2);
93
+ app.goTo('/detail#child1');
94
+ },
95
+ () => {
96
+ // Here, it said "expected 7.800000190734863 to equal 7.999997138977051".
97
+ // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
98
+ expect(scrollTop(window)).to.be.closeTo(offset(child1).top, 0.5);
99
+ app.goTo('/detail#unknown-fragment');
100
+ },
101
+ () => {
102
+ expect(scrollTop(window)).to.equal(0);
103
+ done();
104
+ },
105
+ ]);
106
+ });
107
+
108
+ it('should not crash when `window.history` is not available', (done) => {
109
+ Object.defineProperty(window.history, 'scrollRestoration', {
110
+ value: 'auto',
111
+ // See https://github.com/taion/scroll-behavior/issues/126
112
+ writable: false,
113
+ enumerable: true,
114
+ configurable: true,
115
+ });
116
+
117
+ const app = addScrollableContainerWithAnchors(createApp());
118
+
119
+ unlisten = runApp(app, [
120
+ () => {
121
+ expect(scrollTop(window)).to.equal(0);
122
+
123
+ delete window.history.scrollRestoration;
124
+ window.history.scrollRestoration = 'auto';
125
+
126
+ done();
127
+ },
128
+ ]);
129
+ });
130
+ });
131
+
132
+ describe('custom behavior', () => {
133
+ it('should allow scroll suppression', (done) => {
134
+ const app = addScrollableContainerWithAnchors(
135
+ createApp({
136
+ shouldUpdatePageScrollPositionForLocation: (
137
+ location,
138
+ prevLocation,
139
+ ) => {
140
+ return (
141
+ !prevLocation || prevLocation.pathname !== location.pathname
142
+ );
143
+ },
144
+ }),
145
+ );
146
+
147
+ unlisten = runApp(app, [
148
+ () => {
149
+ app.goTo('/detail');
150
+ },
151
+ () => {
152
+ scrollTop(window, 5000);
153
+ delay(() => {
154
+ app.goTo('/detail?key=value');
155
+ });
156
+ },
157
+ () => {
158
+ // Here, it said "expected 4999.7998046875 to equal 5000".
159
+ // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
160
+ expect(scrollTop(window)).to.be.closeTo(5000, 0.5);
161
+ app.goTo('/');
162
+ },
163
+ () => {
164
+ expect(scrollTop(window)).to.equal(0);
165
+ done();
166
+ },
167
+ ]);
168
+ });
169
+
170
+ it('should ignore scroll events when `disableSavingScrollPosition()` is used', (done) => {
171
+ const app = addScrollableContainerWithAnchors(createApp());
172
+
173
+ unlisten = runApp(app, [
174
+ () => {
175
+ app.disableSavingScrollPosition();
176
+ scrollTop(window, 5000);
177
+ delay(() => {
178
+ app.goTo('/detail');
179
+ });
180
+ },
181
+ () => {
182
+ delay(() => {
183
+ app.goBack();
184
+ });
185
+ },
186
+ () => {
187
+ expect(scrollTop(window)).to.equal(0);
188
+ app.enableSavingScrollPosition();
189
+ scrollTop(window, 2000);
190
+ delay(() => {
191
+ app.goTo('/detail');
192
+ });
193
+ },
194
+ () => {
195
+ delay(() => {
196
+ app.goBack();
197
+ });
198
+ },
199
+ () => {
200
+ // Here, it said "expected 1999.8333740234375 to equal 2000".
201
+ // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
202
+ expect(scrollTop(window)).to.be.closeTo(2000, 0.5);
203
+ done();
204
+ },
205
+ ]);
206
+ });
207
+
208
+ it('should allow custom position', (done) => {
209
+ const app = addScrollableContainerWithAnchors(
210
+ createApp({
211
+ getPageScrollPositionForLocation: () => [10, 20],
212
+ }),
213
+ );
214
+
215
+ unlisten = runApp(app, [
216
+ () => {
217
+ app.goTo('/detail');
218
+ },
219
+ () => {
220
+ app.goTo('/');
221
+ },
222
+ () => {
223
+ // Here, it said "expected 9.966666221618652 to equal 10".
224
+ // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
225
+ expect(scrollLeft(window)).to.be.closeTo(10, 0.5);
226
+ // Here, it said "19.933332443237305 to equal 20".
227
+ // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
228
+ expect(scrollTop(window)).to.be.closeTo(20, 0.5);
229
+ done();
230
+ },
231
+ ]);
232
+ });
233
+
234
+ // This test case was disabled because `ScrollPositionRestoration` doesn't save
235
+ // scroll position on page load. It only saves scroll position on actual scroll events.
236
+ // If there were no scroll events, the scroll position doesn't get saved.
237
+ // The rationale is that when there were no scroll events, the scroll position
238
+ // is gonna be either a default one or a custom one specified by passing a custom
239
+ // `getPageScrollPositionForLocation()` function. In the latter case, the custom
240
+ // `getPageScrollPositionForLocation()` function is responsible to return a correct scroll position
241
+ // every time it gets called rather than just return a correct scroll position once,
242
+ // save it immediately and then restore it when returning to the page.
243
+ //
244
+ // it('should save scroll position even if no scroll events are dispatched', (done) => {
245
+ // let customInitialPageScrollPosition;
246
+ //
247
+ // const app = addScrollableContainerWithAnchors(
248
+ // createApp({
249
+ // // eslint-disable-next-line no-unused-vars
250
+ // getPageScrollPositionForLocation(location, prevLocation) {
251
+ // // Only when navigated via `.goTo()`. Ignore `.goBack()` navigation.
252
+ // if (prevLocation && location.index > prevLocation.index) {
253
+ // return [10, 20];
254
+ // }
255
+ // return undefined;
256
+ // },
257
+ // }),
258
+ // );
259
+ //
260
+ // unlisten = runApp(app, [
261
+ // () => {
262
+ // app.goTo('/detail');
263
+ // },
264
+ // () => {
265
+ // app.goTo('/');
266
+ // },
267
+ // () => {
268
+ // app.goTo('/detail');
269
+ // },
270
+ // () => {
271
+ // if (customInitialPageScrollPosition === 123) {
272
+ // customInitialPageScrollPosition = 456;
273
+ // }
274
+ // app.goBack();
275
+ // },
276
+ // () => {
277
+ // // Here, it said "expected 9.966666221618652 to equal 10".
278
+ // // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
279
+ // expect(scrollLeft(window)).to.be.closeTo(10, 0.5);
280
+ // // Here, it said "expected 19.933332443237305 to equal 20".
281
+ // // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
282
+ // expect(scrollTop(window)).to.be.closeTo(20, 0.5);
283
+ // done();
284
+ // },
285
+ // ]);
286
+ // });
287
+ });
288
+
289
+ describe('scrollable container', () => {
290
+ it('should follow browser scroll behavior', (done) => {
291
+ const { container, ...app } = addScrollableContainer(
292
+ createApp({
293
+ shouldUpdatePageScrollPositionForLocation: () => false,
294
+ }),
295
+ );
296
+
297
+ unlisten = runApp(app, [
298
+ () => {
299
+ scrollTop(container, 10000);
300
+ delay(() => {
301
+ app.goTo('/other');
302
+ });
303
+ },
304
+ () => {
305
+ expect(scrollTop(container)).to.equal(0);
306
+ scrollTop(container, 5000);
307
+ delay(() => {
308
+ app.goBack();
309
+ });
310
+ },
311
+ () => {
312
+ // Here, it said "expected 10000.033203125 to equal 10000".
313
+ // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
314
+ expect(scrollTop(container)).to.be.closeTo(10000, 0.5);
315
+ app.goTo('/other');
316
+ },
317
+ () => {
318
+ expect(scrollTop(container)).to.equal(0);
319
+ done();
320
+ },
321
+ ]);
322
+ });
323
+
324
+ it('should automatically restore a previously-saved scroll position when adding a scrollable container', (done) => {
325
+ const { container, ...app } =
326
+ withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration(
327
+ createApp({
328
+ shouldUpdatePageScrollPositionForLocation: () => false,
329
+ }),
330
+ );
331
+
332
+ unlisten = runApp(app, [
333
+ () => {
334
+ scrollTop(container, 10000);
335
+ delay(() => {
336
+ app.goTo('/other');
337
+ });
338
+ },
339
+ () => {
340
+ expect(container.scrollHeight).to.equal(100);
341
+ expect(scrollTop(container)).to.equal(0);
342
+ // This `scrollTop()` won't trigger a "scroll" event
343
+ // because the container height is only `100` so it's not scrollable.
344
+ scrollTop(container, 5000);
345
+ delay(() => {
346
+ app.goBack();
347
+ });
348
+ },
349
+ () => {
350
+ expect(container.scrollHeight).to.equal(20000);
351
+ // Here, it said "expected 10000.033203125 to equal 10000".
352
+ // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
353
+ expect(scrollTop(container)).to.be.closeTo(10000, 0.5);
354
+ done();
355
+ },
356
+ ]);
357
+ });
358
+
359
+ it('should save element scroll position on scroll event, i.e. before navigation is even attempted', (done) => {
360
+ const app1 = addScrollableContainer(
361
+ createApp({
362
+ shouldUpdatePageScrollPositionForLocation: () => false,
363
+ }),
364
+ );
365
+
366
+ const unlisten1 = runApp(app1, [
367
+ () => {
368
+ expect(scrollTop(app1.container)).to.equal(0);
369
+ scrollTop(app1.container, 5000);
370
+
371
+ delay(() => {
372
+ unlisten1();
373
+
374
+ const app2 = addScrollableContainer(
375
+ createApp({
376
+ // Restore the data of the session of `app1`.
377
+ sessionKey: app1.getSessionKey(),
378
+ shouldUpdatePageScrollPositionForLocation: () => false,
379
+ }),
380
+ );
381
+
382
+ unlisten = app2.listen(() => {
383
+ delay(() => {
384
+ // Here, it said "expected 4999.7998046875 to equal 5000".
385
+ // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
386
+ expect(scrollTop(app2.container)).to.be.closeTo(5000, 0.5);
387
+ done();
388
+ });
389
+ });
390
+ });
391
+ },
392
+ ]);
393
+ });
394
+
395
+ it('should ignore scroll events when `disableSavingScrollPosition` is used', (done) => {
396
+ const app = addScrollableContainer(
397
+ addScrollableContainerWithAnchors(createApp()),
398
+ );
399
+
400
+ unlisten = runApp(app, [
401
+ () => {
402
+ app.disableSavingScrollPosition();
403
+ scrollTop(app.container, 5432);
404
+ delay(() => {
405
+ app.goTo('/detail');
406
+ });
407
+ },
408
+ () => {
409
+ delay(() => {
410
+ app.goBack();
411
+ });
412
+ },
413
+ () => {
414
+ expect(scrollTop(app.container)).to.equal(0);
415
+ app.enableSavingScrollPosition();
416
+ scrollTop(app.container, 2000);
417
+ delay(() => {
418
+ app.goTo('/detail');
419
+ });
420
+ },
421
+ () => {
422
+ delay(() => {
423
+ app.goBack();
424
+ });
425
+ },
426
+ () => {
427
+ // Here, it said "expected 1999.8333740234375 to equal 2000".
428
+ // Using `.to.be.closeTo()` here instead of `.to.equal()` to work around this browser issue.
429
+ expect(scrollTop(app.container)).to.be.closeTo(2000, 0.5);
430
+ done();
431
+ },
432
+ ]);
433
+ });
434
+ });
435
+ });
@@ -0,0 +1,39 @@
1
+ // Adds a 100x100 scrollable container on the website, with a large amount of content
2
+ // inside it (20000x20000 to be specific), making the container scrollable.
3
+ export default function addScrollableContainer(app) {
4
+ // Create a scrollable container.
5
+ const container = document.createElement('div');
6
+ container.style.height = '100px';
7
+ container.style.width = '100px';
8
+ container.style.overflow = 'hidden';
9
+
10
+ // Put a large amount of content in the container, making it scrollable.
11
+ const element = document.createElement('div');
12
+ element.style.height = '20000px';
13
+ element.style.width = '20000px';
14
+ container.appendChild(element);
15
+
16
+ // Add the scrollable container to the website.
17
+ document.body.appendChild(container);
18
+
19
+ function listen(listener) {
20
+ const unlisten = app.listen(listener);
21
+
22
+ const unregisterScrollableContainer = app.registerScrollableContainer(
23
+ 'container',
24
+ container,
25
+ );
26
+
27
+ return () => {
28
+ unlisten();
29
+ unregisterScrollableContainer();
30
+ document.body.removeChild(container);
31
+ };
32
+ }
33
+
34
+ return {
35
+ ...app,
36
+ container,
37
+ listen,
38
+ };
39
+ }
@@ -0,0 +1,56 @@
1
+ // Creates a scrollable container on the website.
2
+ //
3
+ // Puts a couple of child elements inside it:
4
+ // * One is just a simple child element, 100px in height.
5
+ // * Another is a clickable hyperlink to an unspecified location, 100px in height.
6
+ //
7
+ // * When the current location is "/", the scrollable container size is 20000x20000.
8
+ // * At any other location, the scrollable container size is 10000x10000.
9
+ //
10
+ export default function addScrollableContainerWithAnchors(app) {
11
+ const container = document.createElement('div');
12
+ document.body.appendChild(container);
13
+
14
+ const child1 = document.createElement('div');
15
+ // In HTML, an "anchor" is matched either by `id` attribute value
16
+ // or by `name` attribute value.
17
+ child1.id = 'child1';
18
+ child1.style.height = '100px';
19
+ container.appendChild(child1);
20
+
21
+ const child2 = document.createElement('a');
22
+ child2.id = 'child2-id';
23
+ // In HTML, an "anchor" is matched either by `id` attribute value
24
+ // or by `name` attribute value.
25
+ // Here, it tests the correctness of scrolling to an anchor called "child2".
26
+ // The `id` attribute value is different so that it doesn't interfere.
27
+ child2.name = 'child2';
28
+ child2.style.height = '100px';
29
+ child2.appendChild(document.createTextNode('link'));
30
+ container.appendChild(child2);
31
+
32
+ function listen(listener) {
33
+ const unlisten = app.listen((location) => {
34
+ listener(location);
35
+
36
+ // Scrollable container has different height on different pages.
37
+ if (location.pathname === '/') {
38
+ container.style.height = '20000px';
39
+ container.style.width = '20000px';
40
+ } else {
41
+ container.style.height = '10000px';
42
+ container.style.width = '10000px';
43
+ }
44
+ });
45
+
46
+ return () => {
47
+ unlisten();
48
+ document.body.removeChild(container);
49
+ };
50
+ }
51
+
52
+ return {
53
+ ...app,
54
+ listen,
55
+ };
56
+ }
@@ -0,0 +1,132 @@
1
+ import NavigationStack from '../../src/NavigationStack';
2
+ import ScrollPositionRestoration from '../../src/scroll-position/ScrollPositionRestoration';
3
+ import WebBrowserSession from '../../src/session/WebBrowserSession';
4
+
5
+ // Creates a website with `ScrollPositionRestoration`.
6
+ export default function createApp({
7
+ sessionKey,
8
+ shouldUpdatePageScrollPositionForLocation,
9
+ getPageScrollPositionForLocation,
10
+ } = {}) {
11
+ let currentLocation = null;
12
+
13
+ let locationRenderedListeners = [];
14
+ let listeners = [];
15
+ let scrollPositionRestoration = null;
16
+ let navigationStack = null;
17
+ let session = null;
18
+
19
+ function onLocationDidChange(location) {
20
+ // const prevLocation = currentLocation;
21
+ currentLocation = location;
22
+
23
+ listeners.forEach((listener) => {
24
+ listener(location);
25
+ });
26
+
27
+ scrollPositionRestoration.locationRendered(location);
28
+
29
+ for (const locationRenderedListener of locationRenderedListeners) {
30
+ locationRenderedListener(location);
31
+ }
32
+ }
33
+
34
+ // Adds a "location rendered" listener.
35
+ function whenRenderedLocation(listener) {
36
+ locationRenderedListeners.push(listener);
37
+
38
+ // Returns a "remove listener" function.
39
+ return () => {
40
+ locationRenderedListeners = locationRenderedListeners.filter(
41
+ (_) => _ !== listener,
42
+ );
43
+ };
44
+ }
45
+
46
+ // Removes a "location change" event listener.
47
+ let unlisten = null;
48
+
49
+ // Adds a "location change" event listener.
50
+ // A "location change" event happens at every navigation: `goTo()` or `goBack()`,
51
+ // An initial "location change" event happens at startup.
52
+ function listen(listener) {
53
+ // Create a `WebBrowserSession`, `NavigationStack` and `ScrollPositionRestoration`
54
+ // at the initial call of the `app.listen()` function.
55
+ if (listeners.length > 0) {
56
+ throw new Error('Only one `listener` is allowed in tests');
57
+ }
58
+
59
+ session = new WebBrowserSession();
60
+ // There's this one test that restores data of a session of a previous app
61
+ // and for that the new session just has to have the same key in order to read
62
+ // the previous app's session data from the environment storage.
63
+ if (sessionKey) {
64
+ session.key = sessionKey;
65
+ }
66
+ navigationStack = new NavigationStack(session);
67
+ scrollPositionRestoration = new ScrollPositionRestoration(session, {
68
+ _shouldUpdatePageScrollPositionForLocation:
69
+ shouldUpdatePageScrollPositionForLocation,
70
+ _getPageScrollPositionForLocation: getPageScrollPositionForLocation,
71
+ });
72
+
73
+ unlisten = navigationStack.subscribe(onLocationDidChange);
74
+
75
+ listeners.push(listener);
76
+
77
+ navigationStack.init(currentLocation);
78
+
79
+ return () => {
80
+ listeners = listeners.filter((item) => item !== listener);
81
+
82
+ if (listeners.length === 0) {
83
+ // `navigationStack.stop()` automatically calls `session.stop()`
84
+ // so there's no requirement to call `session.stop()` explicitly here.
85
+ navigationStack.stop();
86
+ scrollPositionRestoration.stop();
87
+ unlisten();
88
+ }
89
+ };
90
+ }
91
+
92
+ // Registers a scrollable container on a page.
93
+ function registerScrollableContainer(key, element, options) {
94
+ return scrollPositionRestoration.addScrollableContainer(key, element, {
95
+ _shouldUpdateScrollPositionForLocation:
96
+ options && options.shouldUpdateScrollPositionForLocation,
97
+ _getScrollPositionForLocation:
98
+ options && options.getScrollPositionForLocation,
99
+ });
100
+ }
101
+
102
+ function disableSavingScrollPosition() {
103
+ // eslint-disable-next-line no-underscore-dangle
104
+ scrollPositionRestoration._disableSavingScrollPosition();
105
+ }
106
+
107
+ function enableSavingScrollPosition() {
108
+ // eslint-disable-next-line no-underscore-dangle
109
+ scrollPositionRestoration._enableSavingScrollPosition();
110
+ }
111
+
112
+ // Navigates to a new location.
113
+ const goTo = (location) => {
114
+ navigationStack.push(location);
115
+ };
116
+
117
+ // Navigates to a previous location.
118
+ const goBack = () => {
119
+ navigationStack.shift(-1);
120
+ };
121
+
122
+ return {
123
+ goTo,
124
+ goBack,
125
+ listen,
126
+ registerScrollableContainer,
127
+ disableSavingScrollPosition,
128
+ enableSavingScrollPosition,
129
+ getSessionKey: () => session.key,
130
+ whenRenderedLocation,
131
+ };
132
+ }
@@ -0,0 +1,9 @@
1
+ // Runs a `callback` after a few "ticks".
2
+ export default function delay(callback) {
3
+ // Give throttled scroll listeners time to settle down.
4
+ requestAnimationFrame(() => {
5
+ requestAnimationFrame(() => {
6
+ requestAnimationFrame(callback);
7
+ });
8
+ });
9
+ }
@@ -0,0 +1,17 @@
1
+ // Simulates `PageLifecycle` events.
2
+
3
+ let listener;
4
+
5
+ // eslint-disable-next-line no-unused-vars
6
+ export const setEventListener = (eventType, callback) => {
7
+ listener = callback;
8
+ };
9
+
10
+ export const triggerEvent = (oldState, newState) => {
11
+ if (listener) {
12
+ const event = new Event('statechange');
13
+ event.newState = newState;
14
+ event.oldState = oldState;
15
+ listener(event);
16
+ }
17
+ };