@tramvai/test-mocks 7.20.1 → 7.20.3

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 CHANGED
@@ -115,7 +115,13 @@ it('test', async () => {
115
115
 
116
116
  ### Router
117
117
 
118
- Creates mock instance for `@tinkoff/router`
118
+ Creates mock instance for `@tinkoff/router` with full history support.
119
+
120
+ **Features:**
121
+
122
+ - Full history stack with `back()`, `forward()`, `go()` navigation
123
+ - Proper redirect handling (redirect is part of single navigation)
124
+ - Query and hash change support
119
125
 
120
126
  ```ts
121
127
  import { createMockRouter } from '@tramvai/test-mocks';
@@ -134,5 +140,28 @@ describe('test', () => {
134
140
  expect(router.getCurrentRoute()).toMatchObject({ path: '/page/test/' });
135
141
  expect(router.getCurrentUrl()).toMatchObject({ path: '/page/test/' });
136
142
  });
143
+
144
+ it('should support history navigation', async () => {
145
+ const router = createMockRouter();
146
+
147
+ await router.navigate('/page1');
148
+ await router.navigate('/page2');
149
+
150
+ await router.back();
151
+ expect(router.getCurrentRoute()).toMatchObject({ path: '/page1' });
152
+
153
+ await router.forward();
154
+ expect(router.getCurrentRoute()).toMatchObject({ path: '/page2' });
155
+ });
156
+
157
+ it('should handle static redirect configuration', async () => {
158
+ const router = createMockRouter({
159
+ currentRoute: { name: 'redirected', path: '/new-page', redirect: '/new-page' } as any,
160
+ });
161
+
162
+ await router.navigate('/old-page');
163
+
164
+ expect(router.getCurrentRoute()).toMatchObject({ path: '/new-page' });
165
+ });
137
166
  });
138
167
  ```
package/lib/router.es.js CHANGED
@@ -1,10 +1,73 @@
1
- import { AbstractRouter } from '@tinkoff/router';
1
+ import { AbstractRouter, History } from '@tinkoff/router';
2
2
  import { parse } from '@tinkoff/url';
3
3
 
4
4
  const isNavigationRoute = (route) => {
5
5
  return (route.actualPath !== undefined &&
6
6
  route.params !== undefined);
7
7
  };
8
+ class MockHistory extends History {
9
+ historyStack = [];
10
+ currentIndex = -1;
11
+ keyCounter = 0;
12
+ getNextKey() {
13
+ return ++this.keyCounter;
14
+ }
15
+ save(navigation) {
16
+ if (!navigation.replace && this.currentIndex < this.historyStack.length - 1) {
17
+ this.historyStack = this.historyStack.slice(0, this.currentIndex + 1);
18
+ }
19
+ if (navigation.replace && this.currentIndex >= 0) {
20
+ this.historyStack[this.currentIndex] = navigation;
21
+ }
22
+ else {
23
+ this.historyStack.push(navigation);
24
+ this.currentIndex = this.historyStack.length - 1;
25
+ }
26
+ }
27
+ async go(to) {
28
+ const targetIndex = this.currentIndex + to;
29
+ if (targetIndex < 0 || targetIndex >= this.historyStack.length) {
30
+ return;
31
+ }
32
+ this.currentIndex = targetIndex;
33
+ await this.notifyListener(this.historyStack[targetIndex], to);
34
+ }
35
+ async notifyListener(navigation, to = 0) {
36
+ if (this.listener && navigation.url) {
37
+ await this.listener({
38
+ url: navigation.url.path,
39
+ type: navigation.type,
40
+ navigateState: navigation.navigateState,
41
+ replace: navigation.replace,
42
+ history: true,
43
+ isBack: to < 0,
44
+ hasUAVisualTransition: navigation.hasUAVisualTransition,
45
+ viewTransition: navigation.viewTransition,
46
+ viewTransitionTypes: navigation.viewTransitionTypes,
47
+ });
48
+ }
49
+ }
50
+ getCurrentState() {
51
+ if (this.currentIndex < 0 || this.currentIndex >= this.historyStack.length) {
52
+ return undefined;
53
+ }
54
+ const navigation = this.historyStack[this.currentIndex];
55
+ return {
56
+ key: String(navigation.key ?? this.currentIndex),
57
+ type: navigation.type,
58
+ navigateState: navigation.navigateState,
59
+ index: this.currentIndex,
60
+ viewTransition: navigation.viewTransition,
61
+ viewTransitionTypes: navigation.viewTransitionTypes,
62
+ };
63
+ }
64
+ getNavigation(index) {
65
+ return this.historyStack[index];
66
+ }
67
+ getCurrentIndex() {
68
+ return this.currentIndex;
69
+ }
70
+ }
8
71
  const createMockRouter = ({ currentRoute = { name: 'root', path: '/' }, currentUrl = parse(currentRoute.path), } = {}) => {
9
72
  let route = isNavigationRoute(currentRoute)
10
73
  ? currentRoute
@@ -14,16 +77,42 @@ const createMockRouter = ({ currentRoute = { name: 'root', path: '/' }, currentU
14
77
  ...currentRoute,
15
78
  };
16
79
  let url = currentUrl;
17
- let blocked = false;
18
- return new (class extends AbstractRouter {
80
+ let redirectTarget = null;
81
+ const router = new (class extends AbstractRouter {
19
82
  onRedirect = async (navigation) => {
20
- this.commitNavigation(navigation);
21
- blocked = true;
83
+ redirectTarget = navigation;
22
84
  };
85
+ async navigate(navigateOptions) {
86
+ const options = typeof navigateOptions === 'string' ? { url: navigateOptions } : navigateOptions;
87
+ return super.navigate(options);
88
+ }
89
+ async updateCurrentRoute(updateRouteOptions) {
90
+ return super.updateCurrentRoute(updateRouteOptions);
91
+ }
23
92
  getCurrentRoute() {
93
+ const history = this.history;
94
+ const state = history.getCurrentState();
95
+ if (state && state.index >= 0) {
96
+ const navigation = history.getNavigation(state.index);
97
+ if (navigation?.url) {
98
+ return {
99
+ ...route,
100
+ path: navigation.url.path,
101
+ actualPath: navigation.url.path,
102
+ };
103
+ }
104
+ }
24
105
  return route;
25
106
  }
26
107
  getCurrentUrl() {
108
+ const history = this.history;
109
+ const state = history.getCurrentState();
110
+ if (state && state.index >= 0) {
111
+ const navigation = history.getNavigation(state.index);
112
+ if (navigation?.url) {
113
+ return navigation.url;
114
+ }
115
+ }
27
116
  return url;
28
117
  }
29
118
  getLastRoute() {
@@ -32,22 +121,70 @@ const createMockRouter = ({ currentRoute = { name: 'root', path: '/' }, currentU
32
121
  getLastUrl() {
33
122
  return url;
34
123
  }
35
- resolveRoute() {
124
+ resolveRoute({ url } = {}) {
125
+ if (url) {
126
+ return {
127
+ ...route,
128
+ path: url.path,
129
+ actualPath: url.path,
130
+ };
131
+ }
36
132
  return route;
37
133
  }
38
134
  commitNavigation(navigation) {
39
- if (blocked) {
40
- blocked = false;
41
- return;
135
+ const finalNavigation = redirectTarget ?? navigation;
136
+ redirectTarget = null;
137
+ if (!finalNavigation.history) {
138
+ this.history.save(finalNavigation);
139
+ }
140
+ if (finalNavigation.url) {
141
+ route = {
142
+ name: 'changed-after-navigate',
143
+ path: finalNavigation.url.path,
144
+ actualPath: finalNavigation.url.path,
145
+ params: {},
146
+ };
147
+ url = finalNavigation.url;
42
148
  }
43
- route = {
44
- ...route,
45
- name: 'changed-after-navigate',
46
- path: navigation.url?.path ?? route.path,
47
- };
48
- url = navigation.url ?? url;
149
+ }
150
+ setupHistoryListener(history) {
151
+ history.listen(async ({ url: historyUrl, navigateState, replace, isBack }) => {
152
+ const currentUrl = this.getCurrentUrl();
153
+ const { pathname, query, hash } = this.resolveUrl({ url: historyUrl });
154
+ const resolvedUrl = this.resolveUrl({ url: historyUrl, query, hash });
155
+ const isSameUrlNavigation = currentUrl?.path === resolvedUrl.path;
156
+ if (isSameUrlNavigation) {
157
+ const navigation = {
158
+ type: 'updateCurrentRoute',
159
+ url: resolvedUrl,
160
+ navigateState,
161
+ replace,
162
+ isBack,
163
+ history: true,
164
+ key: this.history.getNextKey(),
165
+ };
166
+ this.commitNavigation(navigation);
167
+ }
168
+ else {
169
+ await this.internalNavigate({
170
+ url: historyUrl,
171
+ replace,
172
+ navigateState,
173
+ isBack,
174
+ }, { history: true });
175
+ }
176
+ });
49
177
  }
50
178
  })({});
179
+ const history = new MockHistory();
180
+ history.save({
181
+ url: currentUrl,
182
+ type: 'navigate',
183
+ key: 0,
184
+ });
185
+ router.setupHistoryListener(history);
186
+ router.history = history;
187
+ return router;
51
188
  };
52
189
 
53
190
  export { createMockRouter };
package/lib/router.js CHANGED
@@ -9,6 +9,69 @@ const isNavigationRoute = (route) => {
9
9
  return (route.actualPath !== undefined &&
10
10
  route.params !== undefined);
11
11
  };
12
+ class MockHistory extends router.History {
13
+ historyStack = [];
14
+ currentIndex = -1;
15
+ keyCounter = 0;
16
+ getNextKey() {
17
+ return ++this.keyCounter;
18
+ }
19
+ save(navigation) {
20
+ if (!navigation.replace && this.currentIndex < this.historyStack.length - 1) {
21
+ this.historyStack = this.historyStack.slice(0, this.currentIndex + 1);
22
+ }
23
+ if (navigation.replace && this.currentIndex >= 0) {
24
+ this.historyStack[this.currentIndex] = navigation;
25
+ }
26
+ else {
27
+ this.historyStack.push(navigation);
28
+ this.currentIndex = this.historyStack.length - 1;
29
+ }
30
+ }
31
+ async go(to) {
32
+ const targetIndex = this.currentIndex + to;
33
+ if (targetIndex < 0 || targetIndex >= this.historyStack.length) {
34
+ return;
35
+ }
36
+ this.currentIndex = targetIndex;
37
+ await this.notifyListener(this.historyStack[targetIndex], to);
38
+ }
39
+ async notifyListener(navigation, to = 0) {
40
+ if (this.listener && navigation.url) {
41
+ await this.listener({
42
+ url: navigation.url.path,
43
+ type: navigation.type,
44
+ navigateState: navigation.navigateState,
45
+ replace: navigation.replace,
46
+ history: true,
47
+ isBack: to < 0,
48
+ hasUAVisualTransition: navigation.hasUAVisualTransition,
49
+ viewTransition: navigation.viewTransition,
50
+ viewTransitionTypes: navigation.viewTransitionTypes,
51
+ });
52
+ }
53
+ }
54
+ getCurrentState() {
55
+ if (this.currentIndex < 0 || this.currentIndex >= this.historyStack.length) {
56
+ return undefined;
57
+ }
58
+ const navigation = this.historyStack[this.currentIndex];
59
+ return {
60
+ key: String(navigation.key ?? this.currentIndex),
61
+ type: navigation.type,
62
+ navigateState: navigation.navigateState,
63
+ index: this.currentIndex,
64
+ viewTransition: navigation.viewTransition,
65
+ viewTransitionTypes: navigation.viewTransitionTypes,
66
+ };
67
+ }
68
+ getNavigation(index) {
69
+ return this.historyStack[index];
70
+ }
71
+ getCurrentIndex() {
72
+ return this.currentIndex;
73
+ }
74
+ }
12
75
  const createMockRouter = ({ currentRoute = { name: 'root', path: '/' }, currentUrl = url.parse(currentRoute.path), } = {}) => {
13
76
  let route = isNavigationRoute(currentRoute)
14
77
  ? currentRoute
@@ -18,16 +81,42 @@ const createMockRouter = ({ currentRoute = { name: 'root', path: '/' }, currentU
18
81
  ...currentRoute,
19
82
  };
20
83
  let url = currentUrl;
21
- let blocked = false;
22
- return new (class extends router.AbstractRouter {
84
+ let redirectTarget = null;
85
+ const router$1 = new (class extends router.AbstractRouter {
23
86
  onRedirect = async (navigation) => {
24
- this.commitNavigation(navigation);
25
- blocked = true;
87
+ redirectTarget = navigation;
26
88
  };
89
+ async navigate(navigateOptions) {
90
+ const options = typeof navigateOptions === 'string' ? { url: navigateOptions } : navigateOptions;
91
+ return super.navigate(options);
92
+ }
93
+ async updateCurrentRoute(updateRouteOptions) {
94
+ return super.updateCurrentRoute(updateRouteOptions);
95
+ }
27
96
  getCurrentRoute() {
97
+ const history = this.history;
98
+ const state = history.getCurrentState();
99
+ if (state && state.index >= 0) {
100
+ const navigation = history.getNavigation(state.index);
101
+ if (navigation?.url) {
102
+ return {
103
+ ...route,
104
+ path: navigation.url.path,
105
+ actualPath: navigation.url.path,
106
+ };
107
+ }
108
+ }
28
109
  return route;
29
110
  }
30
111
  getCurrentUrl() {
112
+ const history = this.history;
113
+ const state = history.getCurrentState();
114
+ if (state && state.index >= 0) {
115
+ const navigation = history.getNavigation(state.index);
116
+ if (navigation?.url) {
117
+ return navigation.url;
118
+ }
119
+ }
31
120
  return url;
32
121
  }
33
122
  getLastRoute() {
@@ -36,22 +125,70 @@ const createMockRouter = ({ currentRoute = { name: 'root', path: '/' }, currentU
36
125
  getLastUrl() {
37
126
  return url;
38
127
  }
39
- resolveRoute() {
128
+ resolveRoute({ url } = {}) {
129
+ if (url) {
130
+ return {
131
+ ...route,
132
+ path: url.path,
133
+ actualPath: url.path,
134
+ };
135
+ }
40
136
  return route;
41
137
  }
42
138
  commitNavigation(navigation) {
43
- if (blocked) {
44
- blocked = false;
45
- return;
139
+ const finalNavigation = redirectTarget ?? navigation;
140
+ redirectTarget = null;
141
+ if (!finalNavigation.history) {
142
+ this.history.save(finalNavigation);
143
+ }
144
+ if (finalNavigation.url) {
145
+ route = {
146
+ name: 'changed-after-navigate',
147
+ path: finalNavigation.url.path,
148
+ actualPath: finalNavigation.url.path,
149
+ params: {},
150
+ };
151
+ url = finalNavigation.url;
46
152
  }
47
- route = {
48
- ...route,
49
- name: 'changed-after-navigate',
50
- path: navigation.url?.path ?? route.path,
51
- };
52
- url = navigation.url ?? url;
153
+ }
154
+ setupHistoryListener(history) {
155
+ history.listen(async ({ url: historyUrl, navigateState, replace, isBack }) => {
156
+ const currentUrl = this.getCurrentUrl();
157
+ const { pathname, query, hash } = this.resolveUrl({ url: historyUrl });
158
+ const resolvedUrl = this.resolveUrl({ url: historyUrl, query, hash });
159
+ const isSameUrlNavigation = currentUrl?.path === resolvedUrl.path;
160
+ if (isSameUrlNavigation) {
161
+ const navigation = {
162
+ type: 'updateCurrentRoute',
163
+ url: resolvedUrl,
164
+ navigateState,
165
+ replace,
166
+ isBack,
167
+ history: true,
168
+ key: this.history.getNextKey(),
169
+ };
170
+ this.commitNavigation(navigation);
171
+ }
172
+ else {
173
+ await this.internalNavigate({
174
+ url: historyUrl,
175
+ replace,
176
+ navigateState,
177
+ isBack,
178
+ }, { history: true });
179
+ }
180
+ });
53
181
  }
54
182
  })({});
183
+ const history = new MockHistory();
184
+ history.save({
185
+ url: currentUrl,
186
+ type: 'navigate',
187
+ key: 0,
188
+ });
189
+ router$1.setupHistoryListener(history);
190
+ router$1.history = history;
191
+ return router$1;
55
192
  };
56
193
 
57
194
  exports.createMockRouter = createMockRouter;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tramvai/test-mocks",
3
- "version": "7.20.1",
3
+ "version": "7.20.3",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
@@ -19,14 +19,14 @@
19
19
  "dependencies": {
20
20
  "@tinkoff/hook-runner": "0.9.1",
21
21
  "@tinkoff/pubsub": "0.10.1",
22
- "@tinkoff/router": "0.7.69",
22
+ "@tinkoff/router": "0.7.71",
23
23
  "@tinkoff/url": "0.13.1",
24
- "@tramvai/core": "7.20.1",
25
- "@tramvai/module-common": "7.20.1",
26
- "@tramvai/module-cookie": "7.20.1",
27
- "@tramvai/state": "7.20.1",
28
- "@tramvai/tokens-common": "7.20.1",
29
- "@tramvai/tokens-core": "7.20.1"
24
+ "@tramvai/core": "7.20.3",
25
+ "@tramvai/module-common": "7.20.3",
26
+ "@tramvai/module-cookie": "7.20.3",
27
+ "@tramvai/state": "7.20.3",
28
+ "@tramvai/tokens-common": "7.20.3",
29
+ "@tramvai/tokens-core": "7.20.3"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@tinkoff/dippy": "^1.0.0",