navigation-stack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.babelrc.cjs +17 -0
  2. package/.eslintignore +8 -0
  3. package/.eslintrc.cjs +10 -0
  4. package/.github/workflows/main.yml +39 -0
  5. package/.yarn/install-state.gz +0 -0
  6. package/.yarnrc.yml +1 -0
  7. package/CODE_OF_CONDUCT.md +77 -0
  8. package/LICENSE +21 -0
  9. package/README.md +249 -0
  10. package/codecov.yml +1 -0
  11. package/karma.conf.cjs +63 -0
  12. package/lib/cjs/ActionTypes.js +14 -0
  13. package/lib/cjs/Actions.js +27 -0
  14. package/lib/cjs/LocationStateStorage.js +60 -0
  15. package/lib/cjs/addNavigationBlocker.js +7 -0
  16. package/lib/cjs/basePath.js +58 -0
  17. package/lib/cjs/createMiddlewares.js +43 -0
  18. package/lib/cjs/createSearchFromQuery.js +13 -0
  19. package/lib/cjs/environment/BrowserEnvironment.js +111 -0
  20. package/lib/cjs/environment/MemoryEnvironment.js +150 -0
  21. package/lib/cjs/environment/ServerEnvironment.js +53 -0
  22. package/lib/cjs/getLocationUrl.js +20 -0
  23. package/lib/cjs/index.js +30 -0
  24. package/lib/cjs/isPromise.js +9 -0
  25. package/lib/cjs/locationReducer.js +13 -0
  26. package/lib/cjs/middleware/createBasePathMiddleware.js +24 -0
  27. package/lib/cjs/middleware/createEnvironmentMiddleware.js +58 -0
  28. package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +128 -0
  29. package/lib/cjs/middleware/createTransformLocationMiddleware.js +38 -0
  30. package/lib/cjs/middleware/navigationActionMiddleware.js +37 -0
  31. package/lib/cjs/middleware/normalizeInputLocationMiddleware.js +27 -0
  32. package/lib/cjs/navigationBlockers.js +146 -0
  33. package/lib/cjs/normalizeInputLocation.js +46 -0
  34. package/lib/cjs/onlyAllowedOnClientSide.js +10 -0
  35. package/lib/cjs/parseLocationUrl.js +39 -0
  36. package/lib/cjs/parseQueryFromSearch.js +16 -0
  37. package/lib/esm/ActionTypes.js +9 -0
  38. package/lib/esm/Actions.js +21 -0
  39. package/lib/esm/LocationStateStorage.js +53 -0
  40. package/lib/esm/addNavigationBlocker.js +2 -0
  41. package/lib/esm/basePath.js +53 -0
  42. package/lib/esm/createMiddlewares.js +37 -0
  43. package/lib/esm/createSearchFromQuery.js +8 -0
  44. package/lib/esm/environment/BrowserEnvironment.js +104 -0
  45. package/lib/esm/environment/MemoryEnvironment.js +143 -0
  46. package/lib/esm/environment/ServerEnvironment.js +46 -0
  47. package/lib/esm/getLocationUrl.js +15 -0
  48. package/lib/esm/index.js +12 -0
  49. package/lib/esm/isPromise.js +4 -0
  50. package/lib/esm/locationReducer.js +7 -0
  51. package/lib/esm/middleware/createBasePathMiddleware.js +19 -0
  52. package/lib/esm/middleware/createEnvironmentMiddleware.js +52 -0
  53. package/lib/esm/middleware/createNavigationBlockerMiddleware.js +123 -0
  54. package/lib/esm/middleware/createTransformLocationMiddleware.js +33 -0
  55. package/lib/esm/middleware/navigationActionMiddleware.js +32 -0
  56. package/lib/esm/middleware/normalizeInputLocationMiddleware.js +22 -0
  57. package/lib/esm/navigationBlockers.js +138 -0
  58. package/lib/esm/normalizeInputLocation.js +41 -0
  59. package/lib/esm/onlyAllowedOnClientSide.js +5 -0
  60. package/lib/esm/parseLocationUrl.js +33 -0
  61. package/lib/esm/parseQueryFromSearch.js +11 -0
  62. package/lib/index.d.ts +301 -0
  63. package/package.json +100 -0
  64. package/renovate.json +3 -0
  65. package/src/ActionTypes.js +9 -0
  66. package/src/Actions.js +26 -0
  67. package/src/LocationStateStorage.js +59 -0
  68. package/src/addNavigationBlocker.js +2 -0
  69. package/src/basePath.js +65 -0
  70. package/src/createMiddlewares.js +41 -0
  71. package/src/createSearchFromQuery.js +9 -0
  72. package/src/environment/BrowserEnvironment.js +109 -0
  73. package/src/environment/MemoryEnvironment.js +151 -0
  74. package/src/environment/ServerEnvironment.js +54 -0
  75. package/src/getLocationUrl.js +12 -0
  76. package/src/index.js +12 -0
  77. package/src/isPromise.js +8 -0
  78. package/src/locationReducer.js +8 -0
  79. package/src/middleware/createBasePathMiddleware.js +20 -0
  80. package/src/middleware/createEnvironmentMiddleware.js +57 -0
  81. package/src/middleware/createNavigationBlockerMiddleware.js +128 -0
  82. package/src/middleware/createTransformLocationMiddleware.js +29 -0
  83. package/src/middleware/navigationActionMiddleware.js +27 -0
  84. package/src/middleware/normalizeInputLocationMiddleware.js +21 -0
  85. package/src/navigationBlockers.js +158 -0
  86. package/src/normalizeInputLocation.js +44 -0
  87. package/src/onlyAllowedOnClientSide.js +5 -0
  88. package/src/parseLocationUrl.js +40 -0
  89. package/src/parseQueryFromSearch.js +12 -0
  90. package/test/.eslintrc.cjs +17 -0
  91. package/test/Action.test.js +72 -0
  92. package/test/ActionTypes.test.js +13 -0
  93. package/test/LocationStateStorage.test.js +75 -0
  94. package/test/basePath.test.js +158 -0
  95. package/test/createMiddlewares.test.js +62 -0
  96. package/test/environment/BrowserEnvironment.test.js +165 -0
  97. package/test/environment/MemoryEnvironment.test.js +218 -0
  98. package/test/environment/ServerEnvironment.test.js +23 -0
  99. package/test/getLocationUrl.test.js +33 -0
  100. package/test/helpers.js +34 -0
  101. package/test/index.js +44 -0
  102. package/test/index.test.js +20 -0
  103. package/test/locationReducer.test.js +42 -0
  104. package/test/middleware/createBasePathMiddleware.test.js +67 -0
  105. package/test/middleware/createNavigationBlockerMiddleware.test.js +472 -0
  106. package/test/middleware/createTransformLocationMiddleware.test.js +44 -0
  107. package/test/middleware/navigationActionMiddleware.test.js +74 -0
  108. package/test/middleware/normalizeInputLocationMiddleware.test.js +62 -0
  109. package/test/normalizeInputLocation.test.js +81 -0
  110. package/test/parseLocationUrl.test.js +30 -0
  111. package/types/.eslintrc.cjs +3 -0
  112. package/types/index.d.ts +301 -0
  113. package/types/tsconfig.json +14 -0
@@ -0,0 +1,21 @@
1
+ import ActionTypes from '../ActionTypes';
2
+ import normalizeInputLocation from '../normalizeInputLocation';
3
+
4
+ // This "middleware" transforms input location argument into a proper `NormalizedInputLocation`.
5
+ export default function normalizeInputLocationMiddleware() {
6
+ return (next) => (action) => {
7
+ const { type, payload } = action;
8
+
9
+ switch (type) {
10
+ case ActionTypes.PUSH:
11
+ case ActionTypes.REPLACE:
12
+ return next({
13
+ type,
14
+ payload: normalizeInputLocation(payload),
15
+ });
16
+
17
+ default:
18
+ return next(action);
19
+ }
20
+ };
21
+ }
@@ -0,0 +1,158 @@
1
+ import isPromise from './isPromise';
2
+ import onlyAllowedOnClientSide from './onlyAllowedOnClientSide';
3
+
4
+ let navigationBlockersList = [];
5
+
6
+ let removeBeforeDestroyListener;
7
+
8
+ export function getNavigationBlockers() {
9
+ return navigationBlockersList;
10
+ }
11
+
12
+ function addNavigationBlockerToTheList(blocker) {
13
+ onlyAllowedOnClientSide();
14
+ navigationBlockersList.push(blocker);
15
+ }
16
+
17
+ function removeNavigationBlockerFromTheList(blocker) {
18
+ onlyAllowedOnClientSide();
19
+ navigationBlockersList = navigationBlockersList.filter((_) => _ !== blocker);
20
+ }
21
+
22
+ export function removeAllNavigationBlockers() {
23
+ onlyAllowedOnClientSide();
24
+ if (getNavigationBlockers().some((blocker) => blocker.beforeDestroy)) {
25
+ removeBeforeDestroyListener();
26
+ removeBeforeDestroyListener = undefined;
27
+ }
28
+ navigationBlockersList = [];
29
+ }
30
+
31
+ // Runs the `listener` while ignoring any errors that might be thrown by it.
32
+ function runNavigationBlocker({ listener }, location) {
33
+ let result;
34
+ try {
35
+ result = listener(location);
36
+ } catch (error) {
37
+ // eslint-disable-next-line no-console
38
+ console.warn(
39
+ `Ignoring navigation blocker \`${listener.name}\` that failed with \`${error}\`.`,
40
+ );
41
+ // eslint-disable-next-line no-console
42
+ console.error(error);
43
+ }
44
+
45
+ // If the listener returned a `Promise`, await for that `Promise`
46
+ // and then return the result.
47
+ if (isPromise(result)) {
48
+ return result.catch((error) => {
49
+ // eslint-disable-next-line no-console
50
+ console.warn(
51
+ `Ignoring navigation blocker \`${listener.name}\` that failed with \`${error}\`.`,
52
+ );
53
+ // eslint-disable-next-line no-console
54
+ console.error(error);
55
+ });
56
+ }
57
+ // The listener didn't return a `Promise`.
58
+ // Return the "synchronous" result.
59
+ return result;
60
+ }
61
+
62
+ // Runs all listeners in order.
63
+ // If any listener returns a non-`null` result, it stops and returns the result.
64
+ // If there's no such listener, returns `true`.
65
+ export function runNavigationBlockers(navigationBlockers, toLocation) {
66
+ if (navigationBlockers.length === 0) {
67
+ return undefined;
68
+ }
69
+
70
+ // Call the first blocker in the list.
71
+ const result = runNavigationBlocker(navigationBlockers[0], toLocation);
72
+
73
+ const next = () => {
74
+ // Proceed to the next blocker.
75
+ return runNavigationBlockers(navigationBlockers.slice(1), toLocation);
76
+ };
77
+
78
+ if (isPromise(result)) {
79
+ return result.then((resultValue) => {
80
+ if (resultValue) {
81
+ return resultValue;
82
+ }
83
+ return next();
84
+ });
85
+ }
86
+
87
+ if (result) {
88
+ return result;
89
+ }
90
+ return next();
91
+ }
92
+
93
+ /* istanbul ignore next: not testable with Karma */
94
+ function onBeforeDestroy() {
95
+ const result = runNavigationBlockers(getNavigationBlockers(), null);
96
+
97
+ // If no listener returned anything, don't prevent the "unload" event.
98
+ if (!result) {
99
+ return undefined;
100
+ }
101
+
102
+ // Web browsers don't allow displaying a custom modal in "beforeunload" phase.
103
+ // They only allow displaying a standard one, with the default text.
104
+ // Hence, "asynchronous" listeners should be ignored.
105
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
106
+ if (isPromise(result)) {
107
+ return undefined;
108
+ }
109
+
110
+ // Prevent the "unload" event.
111
+ return true;
112
+ }
113
+
114
+ export function addNavigationBlocker(environment, listener) {
115
+ onlyAllowedOnClientSide();
116
+
117
+ // All navigation blockers also run on `beforeDestroy` event.
118
+ // If required, this could be a parameter of this function.
119
+ // The rationale could be that adding `beforeunload` a listener
120
+ // disables web page caching in some browsers like Firefox.
121
+ const beforeDestroy = true;
122
+
123
+ // If it's the first "beforeDestroy" listener, add the global `onBeforeDestroy` listener.
124
+ //
125
+ // Sidenote: Add the "beforeunload" event listener only as needed, as its presence
126
+ // prevents the page from being added to the page navigation cache:
127
+ //
128
+ // "In Firefox, beforeunload is not compatible with the back/forward cache (bfcache):
129
+ // that is, Firefox will not place pages in the bfcache if they have beforeunload listeners,
130
+ // and this is bad for performance."
131
+ //
132
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
133
+ if (
134
+ beforeDestroy &&
135
+ !getNavigationBlockers().some((blocker) => blocker.beforeDestroy)
136
+ ) {
137
+ removeBeforeDestroyListener =
138
+ environment.addBeforeDestroyListener(onBeforeDestroy);
139
+ }
140
+
141
+ const blocker = { listener, beforeDestroy };
142
+ addNavigationBlockerToTheList(blocker);
143
+
144
+ return () => {
145
+ removeNavigationBlockerFromTheList(blocker);
146
+
147
+ // If it was the last "beforeDestroy" listener, remove the global `onBeforeDestroy` listener.
148
+ if (
149
+ beforeDestroy &&
150
+ !getNavigationBlockers().some(
151
+ (navigationBlocker) => navigationBlocker.beforeDestroy,
152
+ )
153
+ ) {
154
+ removeBeforeDestroyListener();
155
+ removeBeforeDestroyListener = undefined;
156
+ }
157
+ };
158
+ }
@@ -0,0 +1,44 @@
1
+ import createSearchFromQuery from './createSearchFromQuery';
2
+ import parseLocationUrl from './parseLocationUrl';
3
+ import parseQueryFromSearch from './parseQueryFromSearch';
4
+
5
+ // * If `location` is a string, it parses it into a `NormalizedInputLocation`.
6
+ // * If `location` is an object, it ensures that `search` and `hash` properties aren't `undefined`,
7
+ // i.e. it "ensures" that the `location` object can be used as a `NormalizedInputLocation`.
8
+ export default function normalizeInputLocation(location) {
9
+ if (typeof location === 'string') {
10
+ return parseLocationUrl(location);
11
+ }
12
+
13
+ // Convert `query` property values to strings.
14
+ if (location.query) {
15
+ for (const key of Object.keys(location.query)) {
16
+ location.query[key] = String(location.query[key]);
17
+ }
18
+ }
19
+
20
+ // Create `query` from `search`.
21
+ if (location.search && !location.query) {
22
+ location = {
23
+ ...location,
24
+ query: parseQueryFromSearch(location.search),
25
+ };
26
+ }
27
+
28
+ // Convert `query` object into a `search` string
29
+ // if `query` is present but `search` is not.
30
+ if (location.query && !location.search) {
31
+ location = {
32
+ ...location,
33
+ search: createSearchFromQuery(location.query),
34
+ };
35
+ }
36
+
37
+ // Set default values on `search` and `hash`
38
+ // if those properties are not present.
39
+ return {
40
+ ...location,
41
+ search: location.search || '',
42
+ hash: location.hash || '',
43
+ };
44
+ }
@@ -0,0 +1,5 @@
1
+ export default function onlyAllowedOnClientSide() {
2
+ if (typeof window === 'undefined') {
3
+ throw new Error('This function can only be called on client side');
4
+ }
5
+ }
@@ -0,0 +1,40 @@
1
+ import parseQueryFromSearch from './parseQueryFromSearch';
2
+
3
+ export default function parseLocationUrl(url) {
4
+ if (url[0] !== '/') {
5
+ throw new Error('Expected URL to start with a slash');
6
+ }
7
+
8
+ let remainingPath = url;
9
+
10
+ const hashIndex = remainingPath.indexOf('#');
11
+ let hash;
12
+ if (hashIndex !== -1) {
13
+ hash = remainingPath.slice(hashIndex);
14
+ remainingPath = remainingPath.slice(0, hashIndex);
15
+ } else {
16
+ hash = '';
17
+ }
18
+
19
+ const searchIndex = remainingPath.indexOf('?');
20
+ let search;
21
+ if (searchIndex !== -1) {
22
+ search = remainingPath.slice(searchIndex);
23
+ remainingPath = remainingPath.slice(0, searchIndex);
24
+ } else {
25
+ search = '';
26
+ }
27
+
28
+ const location = {
29
+ pathname: remainingPath,
30
+ search,
31
+ hash,
32
+ };
33
+
34
+ const query = parseQueryFromSearch(search);
35
+ if (query) {
36
+ location.query = query;
37
+ }
38
+
39
+ return location;
40
+ }
@@ -0,0 +1,12 @@
1
+ import { parse as parseQuery } from 'query-string';
2
+
3
+ export default function parseQueryFromSearch(search) {
4
+ if (search.length > '?'.length) {
5
+ try {
6
+ return parseQuery(search.slice(1));
7
+ } catch (error) {
8
+ // Ignore any query parsing errors.
9
+ }
10
+ }
11
+ return undefined;
12
+ }
@@ -0,0 +1,17 @@
1
+ module.exports = {
2
+ env: {
3
+ mocha: true,
4
+ },
5
+ globals: {
6
+ expect: false,
7
+ sinon: false,
8
+ },
9
+ rules: {
10
+ 'import/no-extraneous-dependencies': [
11
+ 'error',
12
+ {
13
+ devDependencies: true,
14
+ },
15
+ ],
16
+ },
17
+ };
@@ -0,0 +1,72 @@
1
+ import ActionTypes from '../src/ActionTypes';
2
+ import Actions from '../src/Actions';
3
+
4
+ describe('Actions', () => {
5
+ it('#init should create an INIT action', () => {
6
+ expect(Actions.init()).to.eql({
7
+ type: ActionTypes.INIT,
8
+ });
9
+ });
10
+
11
+ it('#push should create a PUSH action with location', () => {
12
+ expect(Actions.push('/foo?bar=baz#qux')).to.eql({
13
+ type: ActionTypes.PUSH,
14
+ payload: '/foo?bar=baz#qux',
15
+ });
16
+
17
+ expect(
18
+ Actions.push({
19
+ pathname: '/foo',
20
+ search: '?bar=baz',
21
+ hash: '#qux',
22
+ }),
23
+ ).to.eql({
24
+ type: ActionTypes.PUSH,
25
+ payload: {
26
+ pathname: '/foo',
27
+ search: '?bar=baz',
28
+ hash: '#qux',
29
+ },
30
+ });
31
+ });
32
+
33
+ it('#replace should create a REPLACE action with location', () => {
34
+ expect(Actions.replace('/foo?bar=baz#qux')).to.eql({
35
+ type: ActionTypes.REPLACE,
36
+ payload: '/foo?bar=baz#qux',
37
+ });
38
+
39
+ expect(
40
+ Actions.replace({
41
+ pathname: '/foo',
42
+ search: '?bar=baz',
43
+ hash: '#qux',
44
+ }),
45
+ ).to.eql({
46
+ type: ActionTypes.REPLACE,
47
+ payload: {
48
+ pathname: '/foo',
49
+ search: '?bar=baz',
50
+ hash: '#qux',
51
+ },
52
+ });
53
+ });
54
+
55
+ it('#go should create a SHIFT action with delta', () => {
56
+ expect(Actions.shift(1)).to.eql({
57
+ type: ActionTypes.SHIFT,
58
+ payload: 1,
59
+ });
60
+
61
+ expect(Actions.shift(-1)).to.eql({
62
+ type: ActionTypes.SHIFT,
63
+ payload: -1,
64
+ });
65
+ });
66
+
67
+ it('#dispose should create a DISPOSE action', () => {
68
+ expect(Actions.dispose()).to.eql({
69
+ type: ActionTypes.DISPOSE,
70
+ });
71
+ });
72
+ });
@@ -0,0 +1,13 @@
1
+ import ActionTypes from '../src/ActionTypes';
2
+
3
+ describe('ActionTypes', () => {
4
+ it('should have the correct exports', () => {
5
+ expect(ActionTypes.INIT).to.exist();
6
+ expect(ActionTypes.PUSH).to.exist();
7
+ expect(ActionTypes.REPLACE).to.exist();
8
+ expect(ActionTypes.NAVIGATE).to.exist();
9
+ expect(ActionTypes.SHIFT).to.exist();
10
+ expect(ActionTypes.UPDATE).to.exist();
11
+ expect(ActionTypes.DISPOSE).to.exist();
12
+ });
13
+ });
@@ -0,0 +1,75 @@
1
+ import LocationStateStorage from '../src/LocationStateStorage';
2
+ import MemoryEnvironment from '../src/environment/MemoryEnvironment';
3
+
4
+ describe('LocationStateStorage', () => {
5
+ let environment;
6
+ let stateStorage;
7
+
8
+ const location = {
9
+ key: 'location:0',
10
+ };
11
+
12
+ beforeEach(() => {
13
+ window.sessionStorage.clear();
14
+
15
+ environment = new MemoryEnvironment('/initial-location');
16
+ stateStorage = new LocationStateStorage(environment, {
17
+ namespace: 'test',
18
+ });
19
+ });
20
+
21
+ it('should read saved value for default key', () => {
22
+ stateStorage.set(location, '', 1);
23
+
24
+ expect(stateStorage.get(location, '')).to.equal(1);
25
+ expect(stateStorage.get(location, 'foo')).to.be.undefined();
26
+ });
27
+
28
+ it('should read saved value for explicit key', () => {
29
+ stateStorage.set(location, 'foo', [2, 3]);
30
+
31
+ expect(stateStorage.get(location, 'foo')).to.eql([2, 3]);
32
+ expect(stateStorage.get(location, '')).to.be.undefined();
33
+ });
34
+
35
+ it('should read undefined when value is missing', () => {
36
+ expect(stateStorage.get(location, '')).to.be.undefined();
37
+ expect(stateStorage.get(location, 'foo')).to.be.undefined();
38
+ });
39
+
40
+ it('should work with arbitrary types', () => {
41
+ stateStorage.set(location, 'number', 1);
42
+ stateStorage.set(location, 'boolean', true);
43
+ stateStorage.set(location, 'string', 'state');
44
+ stateStorage.set(location, 'array', [2, 3]);
45
+ stateStorage.set(location, 'object', { a: 1 });
46
+ stateStorage.set(location, 'null', null);
47
+
48
+ expect(stateStorage.get(location, 'number')).to.equal(1);
49
+ expect(stateStorage.get(location, 'boolean')).to.equal(true);
50
+ expect(stateStorage.get(location, 'string')).to.equal('state');
51
+ expect(stateStorage.get(location, 'array')).to.eql([2, 3]);
52
+ expect(stateStorage.get(location, 'object')).to.eql({ a: 1 });
53
+ expect(stateStorage.get(location, 'null')).to.be.null();
54
+ });
55
+
56
+ it('should support deleting values', () => {
57
+ stateStorage.set(location, '', 1);
58
+ expect(stateStorage.get(location, '')).to.equal(1);
59
+
60
+ stateStorage.set(location, '', undefined);
61
+ expect(stateStorage.get(location, '')).to.be.undefined();
62
+ });
63
+
64
+ it('should read undefined for invalid JSON', () => {
65
+ window.sessionStorage.setItem('test|location:0', '[}');
66
+
67
+ expect(stateStorage.get(location, '')).to.be.undefined();
68
+ });
69
+
70
+ it('should support fallback key', () => {
71
+ stateStorage.set({}, '', 1);
72
+
73
+ expect(stateStorage.get({}, '')).to.equal(1);
74
+ });
75
+ });
@@ -0,0 +1,158 @@
1
+ import { addBasePath, removeBasePath } from '../src/basePath';
2
+
3
+ describe('addBasePath', () => {
4
+ it('should add `basePath` to location object', () => {
5
+ expect(
6
+ addBasePath(
7
+ {
8
+ pathname: '/foo',
9
+ search: '?bar=baz',
10
+ hash: '#qux',
11
+ },
12
+ '/base-path',
13
+ ),
14
+ ).to.deep.equal({
15
+ pathname: '/base-path/foo',
16
+ search: '?bar=baz',
17
+ hash: '#qux',
18
+ });
19
+ });
20
+
21
+ it('should add `basePath` to location object (`basePath` ends with a slash)', () => {
22
+ expect(
23
+ addBasePath(
24
+ {
25
+ pathname: '/foo',
26
+ search: '?bar=baz',
27
+ hash: '#qux',
28
+ },
29
+ '/base-path/',
30
+ ),
31
+ ).to.deep.equal({
32
+ pathname: '/base-path/foo',
33
+ search: '?bar=baz',
34
+ hash: '#qux',
35
+ });
36
+ });
37
+
38
+ it('should add `basePath` to location object (no `basePath` option)', () => {
39
+ expect(
40
+ addBasePath({
41
+ pathname: '/foo',
42
+ search: '?bar=baz',
43
+ hash: '#qux',
44
+ }),
45
+ ).to.deep.equal({
46
+ pathname: '/foo',
47
+ search: '?bar=baz',
48
+ hash: '#qux',
49
+ });
50
+ });
51
+
52
+ it('should add `basePath` to location URL', () => {
53
+ expect(addBasePath('/foo?bar=baz#qux', '/base-path')).to.equal(
54
+ '/base-path/foo?bar=baz#qux',
55
+ );
56
+ });
57
+
58
+ it('should add `basePath` to location URL (`basePath` ends with a slash)', () => {
59
+ expect(addBasePath('/foo?bar=baz#qux', '/base-path/')).to.equal(
60
+ '/base-path/foo?bar=baz#qux',
61
+ );
62
+ });
63
+
64
+ it('should add `basePath` to location URL (no `basePath` option)', () => {
65
+ expect(addBasePath('/foo?bar=baz#qux')).to.equal('/foo?bar=baz#qux');
66
+ });
67
+ });
68
+
69
+ describe('removeBasePath', () => {
70
+ it('should remove `basePath` from location object', () => {
71
+ expect(
72
+ removeBasePath(
73
+ {
74
+ pathname: '/base-path/foo',
75
+ search: '?bar=baz',
76
+ hash: '#qux',
77
+ },
78
+ '/base-path',
79
+ ),
80
+ ).to.deep.equal({
81
+ pathname: '/foo',
82
+ search: '?bar=baz',
83
+ hash: '#qux',
84
+ });
85
+ });
86
+
87
+ it('should remove `basePath` from location object (does not contain `basePath`)', () => {
88
+ expect(
89
+ removeBasePath(
90
+ {
91
+ pathname: '/foo',
92
+ search: '?bar=baz',
93
+ hash: '#qux',
94
+ },
95
+ '/base-path',
96
+ ),
97
+ ).to.deep.equal({
98
+ pathname: '/foo',
99
+ search: '?bar=baz',
100
+ hash: '#qux',
101
+ });
102
+ });
103
+
104
+ it('should remove `basePath` from location object (`basePath` ends with a slash)', () => {
105
+ expect(
106
+ removeBasePath(
107
+ {
108
+ pathname: '/base-path/foo',
109
+ search: '?bar=baz',
110
+ hash: '#qux',
111
+ },
112
+ '/base-path/',
113
+ ),
114
+ ).to.deep.equal({
115
+ pathname: '/foo',
116
+ search: '?bar=baz',
117
+ hash: '#qux',
118
+ });
119
+ });
120
+
121
+ it('should remove `basePath` from location object (no `basePath` option)', () => {
122
+ expect(
123
+ removeBasePath({
124
+ pathname: '/base-path/foo',
125
+ search: '?bar=baz',
126
+ hash: '#qux',
127
+ }),
128
+ ).to.deep.equal({
129
+ pathname: '/base-path/foo',
130
+ search: '?bar=baz',
131
+ hash: '#qux',
132
+ });
133
+ });
134
+
135
+ it('should remove `basePath` from location URL', () => {
136
+ expect(
137
+ removeBasePath('/base-path/foo?bar=baz#qux', '/base-path'),
138
+ ).to.equal('/foo?bar=baz#qux');
139
+ });
140
+
141
+ it('should remove `basePath` from location URL (does not contain `basePath`)', () => {
142
+ expect(removeBasePath('/foo?bar=baz#qux', '/base-path')).to.equal(
143
+ '/foo?bar=baz#qux',
144
+ );
145
+ });
146
+
147
+ it('should remove `basePath` from location URL (`basePath` ends with a slash)', () => {
148
+ expect(
149
+ removeBasePath('/base-path/foo?bar=baz#qux', '/base-path/'),
150
+ ).to.equal('/foo?bar=baz#qux');
151
+ });
152
+
153
+ it('should remove `basePath` from location URL (no `basePath` option)', () => {
154
+ expect(removeBasePath('/base-path/foo?bar=baz#qux')).to.equal(
155
+ '/base-path/foo?bar=baz#qux',
156
+ );
157
+ });
158
+ });
@@ -0,0 +1,62 @@
1
+ import { applyMiddleware, createStore } from 'redux';
2
+
3
+ import Actions from '../src/Actions';
4
+ import createMiddlewares from '../src/createMiddlewares';
5
+ import MemoryEnvironment from '../src/environment/MemoryEnvironment';
6
+ import locationReducer from '../src/locationReducer';
7
+
8
+ describe('createMiddlewares', () => {
9
+ let store;
10
+
11
+ beforeEach(() => {
12
+ store = createStore(
13
+ locationReducer,
14
+ applyMiddleware(...createMiddlewares(new MemoryEnvironment('/foo'))),
15
+ );
16
+ store.dispatch(Actions.init());
17
+ });
18
+
19
+ afterEach(() => {
20
+ store.dispatch(Actions.dispose());
21
+ });
22
+
23
+ it('should support push and go', () => {
24
+ store.dispatch(Actions.push('/bar'));
25
+ expect(store.getState()).to.include({
26
+ pathname: '/bar',
27
+ index: 1,
28
+ });
29
+
30
+ store.dispatch(Actions.shift(-1));
31
+ expect(store.getState()).to.include({
32
+ pathname: '/foo',
33
+ index: 0,
34
+ });
35
+
36
+ store.dispatch(Actions.shift(+1));
37
+ expect(store.getState()).to.include({
38
+ pathname: '/bar',
39
+ index: 1,
40
+ });
41
+ });
42
+
43
+ it('should support replace', () => {
44
+ store.dispatch(Actions.replace('/bar'));
45
+ expect(store.getState()).to.include({
46
+ pathname: '/bar',
47
+ index: 0,
48
+ });
49
+ });
50
+
51
+ it('should ignore other actions', () => {
52
+ expect(
53
+ store.dispatch({
54
+ type: 'UNKNOWN',
55
+ payload: { unknown: {} },
56
+ }),
57
+ ).to.eql({
58
+ type: 'UNKNOWN',
59
+ payload: { unknown: {} },
60
+ });
61
+ });
62
+ });