navigation-stack 0.3.1 → 0.4.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.
- package/README.md +603 -163
- package/data-storage/package.json +6 -0
- package/karma.conf.cjs +21 -4
- package/lib/cjs/NavigationStack.js +73 -0
- package/lib/cjs/data-storage/DataStorage.js +71 -0
- package/lib/cjs/data-storage/LocationDataStorage.js +29 -0
- package/lib/cjs/data-storage/index.js +9 -0
- package/lib/cjs/environment/InMemoryEnvironment.js +15 -0
- package/lib/cjs/environment/WebBrowserEnvironment.js +15 -0
- package/lib/cjs/environment/data-storage/InMemoryDataStorage.js +27 -0
- package/lib/cjs/environment/data-storage/WebBrowserDataStorage.js +21 -0
- package/lib/cjs/environment/scroll-position/InMemoryScrollPosition.js +44 -0
- package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +60 -0
- package/lib/cjs/getLocationFromInternalLocation.js +14 -0
- package/lib/cjs/index.js +20 -16
- package/lib/cjs/navigationBlockers.js +25 -23
- package/lib/cjs/{normalizeInputLocation.js → parseInputLocation.js} +25 -9
- package/lib/cjs/{ActionTypes.js → redux/ActionTypes.js} +1 -1
- package/lib/cjs/redux/ActionTypesInternal.js +8 -0
- package/lib/cjs/{Actions.js → redux/Actions.js} +5 -4
- package/lib/cjs/redux/createMiddlewares.js +60 -0
- package/lib/cjs/redux/index.js +13 -0
- package/lib/cjs/redux/internalLocationReducer.js +14 -0
- package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +32 -0
- package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +113 -0
- package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
- package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +30 -0
- package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +73 -0
- package/lib/cjs/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +11 -8
- package/lib/cjs/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +6 -4
- package/lib/cjs/redux/middleware/updateLocationMiddleware.js +34 -0
- package/lib/cjs/scroll-position/PageScrollPositionSetter.js +97 -0
- package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +130 -0
- package/lib/cjs/scroll-position/ScrollPositionRestoration.js +383 -0
- package/lib/cjs/scroll-position/ScrollPositionSaver.js +81 -0
- package/lib/cjs/scroll-position/ScrollPositionSetter.js +16 -0
- package/lib/cjs/scroll-position/constants.js +5 -0
- package/lib/cjs/scroll-position/index.js +7 -0
- package/lib/cjs/scroll-position/scheduleNextTick.js +11 -0
- package/lib/cjs/session/InMemorySession.js +22 -0
- package/lib/cjs/session/ServerSideRenderSession.js +17 -0
- package/lib/cjs/session/Session.js +196 -0
- package/lib/cjs/session/WebBrowserSession.js +20 -0
- package/lib/cjs/session/key/createSessionKey.js +23 -0
- package/lib/cjs/session/lifecycle/InMemorySessionLifecycle.js +19 -0
- package/lib/cjs/session/lifecycle/WebBrowserSessionLifecycle.js +128 -0
- package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycle.js +269 -0
- package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +8 -0
- package/lib/cjs/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +33 -0
- package/lib/cjs/session/navigation/InMemoryNavigation.js +104 -0
- package/lib/cjs/session/navigation/ServerSideNavigation.js +61 -0
- package/lib/cjs/session/navigation/WebBrowserNavigation.js +221 -0
- package/lib/cjs/session/navigation/error/NavigationOutOfBoundsError.js +12 -0
- package/lib/cjs/session/navigation/error/ServerSideNavigationError.js +21 -0
- package/lib/cjs/session/navigation/operation/operations.js +11 -0
- package/lib/cjs/session/subscription/Subscription.js +81 -0
- package/lib/data-storage/index.d.ts +35 -0
- package/lib/esm/NavigationStack.js +66 -0
- package/lib/esm/data-storage/DataStorage.js +65 -0
- package/lib/esm/data-storage/LocationDataStorage.js +22 -0
- package/lib/esm/data-storage/index.js +2 -0
- package/lib/esm/environment/InMemoryEnvironment.js +8 -0
- package/lib/esm/environment/WebBrowserEnvironment.js +8 -0
- package/lib/esm/environment/data-storage/InMemoryDataStorage.js +21 -0
- package/lib/esm/environment/data-storage/WebBrowserDataStorage.js +15 -0
- package/lib/esm/environment/scroll-position/InMemoryScrollPosition.js +38 -0
- package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +54 -0
- package/lib/esm/getLocationFromInternalLocation.js +9 -0
- package/lib/esm/index.js +10 -8
- package/lib/esm/navigationBlockers.js +25 -23
- package/lib/esm/{normalizeInputLocation.js → parseInputLocation.js} +24 -8
- package/lib/esm/{ActionTypes.js → redux/ActionTypes.js} +1 -1
- package/lib/esm/redux/ActionTypesInternal.js +3 -0
- package/lib/esm/{Actions.js → redux/Actions.js} +5 -4
- package/lib/esm/redux/createMiddlewares.js +54 -0
- package/lib/esm/redux/index.js +4 -0
- package/lib/esm/redux/internalLocationReducer.js +8 -0
- package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
- package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +108 -0
- package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +88 -0
- package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +25 -0
- package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +68 -0
- package/lib/esm/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -7
- package/lib/esm/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
- package/lib/esm/redux/middleware/updateLocationMiddleware.js +28 -0
- package/lib/esm/scroll-position/PageScrollPositionSetter.js +91 -0
- package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +123 -0
- package/lib/esm/scroll-position/ScrollPositionRestoration.js +376 -0
- package/lib/esm/scroll-position/ScrollPositionSaver.js +74 -0
- package/lib/esm/scroll-position/ScrollPositionSetter.js +10 -0
- package/lib/esm/scroll-position/constants.js +1 -0
- package/lib/esm/scroll-position/index.js +1 -0
- package/lib/esm/scroll-position/scheduleNextTick.js +6 -0
- package/lib/esm/session/InMemorySession.js +15 -0
- package/lib/esm/session/ServerSideRenderSession.js +11 -0
- package/lib/esm/session/Session.js +189 -0
- package/lib/esm/session/WebBrowserSession.js +13 -0
- package/lib/esm/session/key/createSessionKey.js +18 -0
- package/lib/esm/session/lifecycle/InMemorySessionLifecycle.js +13 -0
- package/lib/esm/session/lifecycle/WebBrowserSessionLifecycle.js +120 -0
- package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycle.js +263 -0
- package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +2 -0
- package/lib/esm/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +30 -0
- package/lib/esm/session/navigation/InMemoryNavigation.js +97 -0
- package/lib/esm/session/navigation/ServerSideNavigation.js +54 -0
- package/lib/esm/session/navigation/WebBrowserNavigation.js +213 -0
- package/lib/esm/session/navigation/error/NavigationOutOfBoundsError.js +6 -0
- package/lib/esm/session/navigation/error/ServerSideNavigationError.js +14 -0
- package/lib/esm/session/navigation/operation/operations.js +6 -0
- package/lib/esm/session/subscription/Subscription.js +75 -0
- package/lib/index.d.ts +178 -157
- package/lib/redux/index.d.ts +90 -0
- package/lib/scroll-position/index.d.ts +107 -0
- package/package.json +9 -5
- package/redux/package.json +6 -0
- package/scroll-position/package.json +6 -0
- package/src/NavigationStack.js +84 -0
- package/src/data-storage/DataStorage.js +69 -0
- package/src/data-storage/LocationDataStorage.js +23 -0
- package/src/data-storage/index.js +2 -0
- package/src/environment/InMemoryEnvironment.js +9 -0
- package/src/environment/WebBrowserEnvironment.js +9 -0
- package/src/environment/data-storage/InMemoryDataStorage.js +23 -0
- package/src/environment/data-storage/WebBrowserDataStorage.js +17 -0
- package/src/environment/scroll-position/InMemoryScrollPosition.js +45 -0
- package/src/environment/scroll-position/WebBrowserScrollPosition.js +72 -0
- package/src/getLocationFromInternalLocation.js +7 -0
- package/src/index.js +10 -8
- package/src/navigationBlockers.js +28 -27
- package/src/{normalizeInputLocation.js → parseInputLocation.js} +23 -8
- package/src/{ActionTypes.js → redux/ActionTypes.js} +1 -1
- package/src/redux/ActionTypesInternal.js +3 -0
- package/src/{Actions.js → redux/Actions.js} +4 -3
- package/src/redux/createMiddlewares.js +65 -0
- package/src/redux/index.js +4 -0
- package/src/redux/internalLocationReducer.js +9 -0
- package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
- package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +119 -0
- package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
- package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +26 -0
- package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +72 -0
- package/src/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -3
- package/src/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
- package/src/redux/middleware/updateLocationMiddleware.js +28 -0
- package/src/scroll-position/PageScrollPositionSetter.js +110 -0
- package/src/scroll-position/ScrollPositionAutoSaver.js +151 -0
- package/src/scroll-position/ScrollPositionRestoration.js +506 -0
- package/src/scroll-position/ScrollPositionSaver.js +100 -0
- package/src/scroll-position/ScrollPositionSetter.js +16 -0
- package/src/scroll-position/constants.js +1 -0
- package/src/scroll-position/index.js +1 -0
- package/src/scroll-position/scheduleNextTick.js +6 -0
- package/src/session/InMemorySession.js +13 -0
- package/src/session/ServerSideRenderSession.js +9 -0
- package/src/session/Session.js +216 -0
- package/src/session/WebBrowserSession.js +13 -0
- package/src/session/key/createSessionKey.js +18 -0
- package/src/session/lifecycle/InMemorySessionLifecycle.js +13 -0
- package/src/session/lifecycle/WebBrowserSessionLifecycle.js +126 -0
- package/src/session/lifecycle/page-lifecycle/PageLifecycle.js +291 -0
- package/src/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +3 -0
- package/src/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +32 -0
- package/src/session/navigation/InMemoryNavigation.js +78 -0
- package/src/session/navigation/ServerSideNavigation.js +43 -0
- package/src/session/navigation/WebBrowserNavigation.js +224 -0
- package/src/session/navigation/error/NavigationOutOfBoundsError.js +7 -0
- package/src/session/navigation/error/ServerSideNavigationError.js +18 -0
- package/src/session/navigation/operation/operations.js +6 -0
- package/src/session/subscription/Subscription.js +76 -0
- package/test/NavigationStack.test.js +296 -0
- package/test/{LocationDataStorage.test.js → data-storage/LocationDataStorage.test.js} +3 -3
- package/test/data-storage/index.test.js +8 -0
- package/test/index.js +12 -0
- package/test/index.test.js +8 -7
- package/test/{helpers.js → middlewareTestUtil.js} +9 -12
- package/test/{normalizeInputLocation.test.js → parseInputLocationMiddleware.test.js} +9 -9
- package/test/{Action.test.js → redux/Action.test.js} +7 -6
- package/test/{ActionTypes.test.js → redux/ActionTypes.test.js} +2 -2
- package/test/redux/createMiddlewares.test.js +96 -0
- package/test/redux/index.test.js +10 -0
- package/test/{locationReducer.test.js → redux/locationReducer.test.js} +4 -7
- package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +40 -0
- package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +264 -0
- package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +312 -0
- package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +51 -0
- package/test/{middleware/navigationActionMiddleware.test.js → redux/middleware/navigationOperationMiddleware.test.js} +16 -12
- package/test/{middleware/normalizeInputLocationMiddleware.test.js → redux/middleware/parseInputLocationMiddleware.test.js} +4 -4
- package/test/scroll-position/ScrollPositionRestoration.test.js +418 -0
- package/test/scroll-position/addScrollableContainer.js +36 -0
- package/test/scroll-position/addScrollableContainerWithHyperlink.js +50 -0
- package/test/scroll-position/createApp.js +112 -0
- package/test/scroll-position/delay.js +9 -0
- package/test/scroll-position/mockPageLifecycle.js +17 -0
- package/test/scroll-position/runApp.js +24 -0
- package/test/scroll-position/withScrollableContainerAtIndexPage.js +62 -0
- package/test/session/InMemorySession.test.js +348 -0
- package/test/session/ServerSession.test.js +17 -9
- package/test/session/WebBrowserSession.test.js +265 -0
- package/test/testUtil.js +3 -0
- package/types/data-storage/index.d.ts +35 -0
- package/types/index.d.ts +178 -157
- package/types/redux/index.d.ts +90 -0
- package/types/scroll-position/index.d.ts +107 -0
- package/types/tsconfig.json +1 -1
- package/lib/cjs/LocationDataStorage.js +0 -61
- package/lib/cjs/addBeforeLocationChangeListener.js +0 -7
- package/lib/cjs/beforeLocationChangeListeners.js +0 -51
- package/lib/cjs/createMiddlewares.js +0 -47
- package/lib/cjs/middleware/createBasePathMiddleware.js +0 -24
- package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -39
- package/lib/cjs/middleware/createLocationMiddleware.js +0 -56
- package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +0 -161
- package/lib/cjs/middleware/createTransformLocationMiddleware.js +0 -38
- package/lib/cjs/onlyAllowedOnClientSide.js +0 -10
- package/lib/cjs/session/BrowserSession.js +0 -235
- package/lib/cjs/session/MemorySession.js +0 -223
- package/lib/cjs/session/ServerSession.js +0 -65
- package/lib/esm/LocationDataStorage.js +0 -54
- package/lib/esm/addBeforeLocationChangeListener.js +0 -2
- package/lib/esm/beforeLocationChangeListeners.js +0 -44
- package/lib/esm/createMiddlewares.js +0 -41
- package/lib/esm/middleware/createBasePathMiddleware.js +0 -19
- package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -34
- package/lib/esm/middleware/createLocationMiddleware.js +0 -50
- package/lib/esm/middleware/createNavigationBlockerMiddleware.js +0 -156
- package/lib/esm/middleware/createTransformLocationMiddleware.js +0 -33
- package/lib/esm/onlyAllowedOnClientSide.js +0 -5
- package/lib/esm/session/BrowserSession.js +0 -229
- package/lib/esm/session/MemorySession.js +0 -217
- package/lib/esm/session/ServerSession.js +0 -58
- package/src/LocationDataStorage.js +0 -60
- package/src/addBeforeLocationChangeListener.js +0 -2
- package/src/beforeLocationChangeListeners.js +0 -54
- package/src/createMiddlewares.js +0 -45
- package/src/middleware/createBasePathMiddleware.js +0 -20
- package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -40
- package/src/middleware/createLocationMiddleware.js +0 -55
- package/src/middleware/createNavigationBlockerMiddleware.js +0 -168
- package/src/middleware/createTransformLocationMiddleware.js +0 -29
- package/src/onlyAllowedOnClientSide.js +0 -5
- package/src/session/BrowserSession.js +0 -235
- package/src/session/MemorySession.js +0 -219
- package/src/session/ServerSession.js +0 -67
- package/test/createMiddlewares.test.js +0 -62
- package/test/middleware/createBasePathMiddleware.test.js +0 -67
- package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +0 -141
- package/test/middleware/createNavigationBlockerMiddleware.test.js +0 -471
- package/test/middleware/createTransformLocationMiddleware.test.js +0 -44
- package/test/session/BrowserSession.test.js +0 -182
- package/test/session/MemorySession.test.js +0 -244
- /package/lib/cjs/{locationReducer.js → redux/locationReducer.js} +0 -0
- /package/lib/esm/{locationReducer.js → redux/locationReducer.js} +0 -0
- /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,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 '
|
|
2
|
-
import
|
|
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
|
|
15
|
+
session = new InMemorySession();
|
|
16
16
|
stateStorage = new LocationDataStorage(session, {
|
|
17
17
|
namespace: 'test',
|
|
18
18
|
});
|
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);
|
package/test/index.test.js
CHANGED
|
@@ -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.
|
|
15
|
-
expect(exports.
|
|
16
|
-
expect(exports.
|
|
17
|
-
expect(exports.
|
|
18
|
-
expect(exports.
|
|
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:
|
|
23
|
-
|
|
19
|
+
payload: {
|
|
20
|
+
operation: 'PUSH',
|
|
21
|
+
location,
|
|
22
|
+
},
|
|
23
|
+
}).payload.location;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export function
|
|
27
|
-
middleware,
|
|
28
|
-
location,
|
|
29
|
-
) {
|
|
26
|
+
export function transformOutputLocationUsingMiddleware(middleware, location) {
|
|
30
27
|
return invokeLocationMiddleware(middleware, {
|
|
31
|
-
type:
|
|
28
|
+
type: ActionTypesInternal.INTERNAL_LOCATION_UPDATE,
|
|
32
29
|
payload: location,
|
|
33
30
|
}).payload;
|
|
34
31
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import parseInputLocation from '../src/parseInputLocation';
|
|
2
2
|
|
|
3
|
-
describe('
|
|
3
|
+
describe('parseInputLocation', () => {
|
|
4
4
|
it('should create `query` from `search`', () => {
|
|
5
5
|
expect(
|
|
6
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
70
|
+
parseInputLocation({
|
|
71
71
|
pathname: '/foo',
|
|
72
72
|
query: { bar: 'baz' },
|
|
73
73
|
hash: '#qux',
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import ActionTypes from '
|
|
2
|
-
import Actions from '
|
|
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('#
|
|
68
|
-
expect(Actions.
|
|
69
|
-
type: ActionTypes.
|
|
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 '
|
|
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.
|
|
11
|
+
expect(ActionTypes.STOP).to.exist();
|
|
12
12
|
});
|
|
13
13
|
});
|