@esmx/router 3.0.0-rc.18 → 3.0.0-rc.20

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 (158) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +70 -0
  3. package/README.zh-CN.md +70 -0
  4. package/dist/error.d.ts +23 -0
  5. package/dist/error.mjs +61 -0
  6. package/dist/increment-id.d.ts +7 -0
  7. package/dist/increment-id.mjs +11 -0
  8. package/dist/index.d.ts +5 -3
  9. package/dist/index.mjs +14 -3
  10. package/dist/index.test.mjs +8 -0
  11. package/dist/location.d.ts +15 -0
  12. package/dist/location.mjs +53 -0
  13. package/dist/location.test.d.ts +8 -0
  14. package/dist/location.test.mjs +370 -0
  15. package/dist/matcher.d.ts +3 -0
  16. package/dist/matcher.mjs +44 -0
  17. package/dist/matcher.test.mjs +1492 -0
  18. package/dist/micro-app.d.ts +18 -0
  19. package/dist/micro-app.dom.test.d.ts +1 -0
  20. package/dist/micro-app.dom.test.mjs +532 -0
  21. package/dist/micro-app.mjs +80 -0
  22. package/dist/navigation.d.ts +43 -0
  23. package/dist/navigation.mjs +143 -0
  24. package/dist/navigation.test.d.ts +1 -0
  25. package/dist/navigation.test.mjs +681 -0
  26. package/dist/options.d.ts +4 -0
  27. package/dist/options.mjs +88 -0
  28. package/dist/route-task.d.ts +40 -0
  29. package/dist/route-task.mjs +75 -0
  30. package/dist/route-task.test.d.ts +1 -0
  31. package/dist/route-task.test.mjs +673 -0
  32. package/dist/route-transition.d.ts +53 -0
  33. package/dist/route-transition.mjs +307 -0
  34. package/dist/route-transition.test.d.ts +1 -0
  35. package/dist/route-transition.test.mjs +146 -0
  36. package/dist/route.d.ts +72 -0
  37. package/dist/route.mjs +194 -0
  38. package/dist/route.test.d.ts +1 -0
  39. package/dist/route.test.mjs +1664 -0
  40. package/dist/router-back.test.d.ts +1 -0
  41. package/dist/router-back.test.mjs +361 -0
  42. package/dist/router-forward.test.d.ts +1 -0
  43. package/dist/router-forward.test.mjs +376 -0
  44. package/dist/router-go.test.d.ts +1 -0
  45. package/dist/router-go.test.mjs +73 -0
  46. package/dist/router-guards-cleanup.test.d.ts +1 -0
  47. package/dist/router-guards-cleanup.test.mjs +437 -0
  48. package/dist/router-link.d.ts +10 -0
  49. package/dist/router-link.mjs +126 -0
  50. package/dist/router-push.test.d.ts +1 -0
  51. package/dist/router-push.test.mjs +115 -0
  52. package/dist/router-replace.test.d.ts +1 -0
  53. package/dist/router-replace.test.mjs +114 -0
  54. package/dist/router-resolve.test.d.ts +1 -0
  55. package/dist/router-resolve.test.mjs +393 -0
  56. package/dist/router-restart-app.dom.test.d.ts +1 -0
  57. package/dist/router-restart-app.dom.test.mjs +616 -0
  58. package/dist/router-window-navigation.test.d.ts +1 -0
  59. package/dist/router-window-navigation.test.mjs +359 -0
  60. package/dist/router.d.ts +109 -102
  61. package/dist/router.mjs +260 -361
  62. package/dist/types.d.ts +246 -0
  63. package/dist/types.mjs +18 -0
  64. package/dist/util.d.ts +26 -0
  65. package/dist/util.mjs +53 -0
  66. package/dist/util.test.d.ts +1 -0
  67. package/dist/util.test.mjs +1020 -0
  68. package/package.json +10 -13
  69. package/src/error.ts +84 -0
  70. package/src/increment-id.ts +12 -0
  71. package/src/index.test.ts +9 -0
  72. package/src/index.ts +54 -3
  73. package/src/location.test.ts +406 -0
  74. package/src/location.ts +96 -0
  75. package/src/matcher.test.ts +1685 -0
  76. package/src/matcher.ts +59 -0
  77. package/src/micro-app.dom.test.ts +708 -0
  78. package/src/micro-app.ts +101 -0
  79. package/src/navigation.test.ts +858 -0
  80. package/src/navigation.ts +195 -0
  81. package/src/options.ts +131 -0
  82. package/src/route-task.test.ts +901 -0
  83. package/src/route-task.ts +105 -0
  84. package/src/route-transition.test.ts +178 -0
  85. package/src/route-transition.ts +425 -0
  86. package/src/route.test.ts +2014 -0
  87. package/src/route.ts +308 -0
  88. package/src/router-back.test.ts +487 -0
  89. package/src/router-forward.test.ts +506 -0
  90. package/src/router-go.test.ts +91 -0
  91. package/src/router-guards-cleanup.test.ts +595 -0
  92. package/src/router-link.ts +235 -0
  93. package/src/router-push.test.ts +140 -0
  94. package/src/router-replace.test.ts +139 -0
  95. package/src/router-resolve.test.ts +475 -0
  96. package/src/router-restart-app.dom.test.ts +783 -0
  97. package/src/router-window-navigation.test.ts +457 -0
  98. package/src/router.ts +289 -470
  99. package/src/types.ts +341 -0
  100. package/src/util.test.ts +1262 -0
  101. package/src/util.ts +116 -0
  102. package/dist/history/abstract.d.ts +0 -29
  103. package/dist/history/abstract.mjs +0 -107
  104. package/dist/history/base.d.ts +0 -79
  105. package/dist/history/base.mjs +0 -275
  106. package/dist/history/html.d.ts +0 -30
  107. package/dist/history/html.mjs +0 -183
  108. package/dist/history/index.d.ts +0 -7
  109. package/dist/history/index.mjs +0 -16
  110. package/dist/matcher/create-matcher.d.ts +0 -5
  111. package/dist/matcher/create-matcher.mjs +0 -218
  112. package/dist/matcher/create-matcher.spec.mjs +0 -0
  113. package/dist/matcher/index.d.ts +0 -1
  114. package/dist/matcher/index.mjs +0 -1
  115. package/dist/task-pipe/index.d.ts +0 -1
  116. package/dist/task-pipe/index.mjs +0 -1
  117. package/dist/task-pipe/task.d.ts +0 -30
  118. package/dist/task-pipe/task.mjs +0 -66
  119. package/dist/types/index.d.ts +0 -694
  120. package/dist/types/index.mjs +0 -6
  121. package/dist/utils/bom.d.ts +0 -5
  122. package/dist/utils/bom.mjs +0 -10
  123. package/dist/utils/encoding.d.ts +0 -48
  124. package/dist/utils/encoding.mjs +0 -44
  125. package/dist/utils/guards.d.ts +0 -9
  126. package/dist/utils/guards.mjs +0 -12
  127. package/dist/utils/index.d.ts +0 -7
  128. package/dist/utils/index.mjs +0 -27
  129. package/dist/utils/path.d.ts +0 -60
  130. package/dist/utils/path.mjs +0 -282
  131. package/dist/utils/path.spec.mjs +0 -27
  132. package/dist/utils/scroll.d.ts +0 -25
  133. package/dist/utils/scroll.mjs +0 -59
  134. package/dist/utils/utils.d.ts +0 -16
  135. package/dist/utils/utils.mjs +0 -11
  136. package/dist/utils/warn.d.ts +0 -2
  137. package/dist/utils/warn.mjs +0 -12
  138. package/src/history/abstract.ts +0 -149
  139. package/src/history/base.ts +0 -408
  140. package/src/history/html.ts +0 -228
  141. package/src/history/index.ts +0 -20
  142. package/src/matcher/create-matcher.spec.ts +0 -3
  143. package/src/matcher/create-matcher.ts +0 -292
  144. package/src/matcher/index.ts +0 -1
  145. package/src/task-pipe/index.ts +0 -1
  146. package/src/task-pipe/task.ts +0 -97
  147. package/src/types/index.ts +0 -858
  148. package/src/utils/bom.ts +0 -14
  149. package/src/utils/encoding.ts +0 -153
  150. package/src/utils/guards.ts +0 -25
  151. package/src/utils/index.ts +0 -27
  152. package/src/utils/path.spec.ts +0 -32
  153. package/src/utils/path.ts +0 -418
  154. package/src/utils/scroll.ts +0 -120
  155. package/src/utils/utils.ts +0 -30
  156. package/src/utils/warn.ts +0 -13
  157. /package/dist/{matcher/create-matcher.spec.d.ts → index.test.d.ts} +0 -0
  158. /package/dist/{utils/path.spec.d.ts → matcher.test.d.ts} +0 -0
@@ -0,0 +1,235 @@
1
+ import type { Router } from './router';
2
+ import type {
3
+ RouterLinkAttributes,
4
+ RouterLinkProps,
5
+ RouterLinkResolved,
6
+ RouterLinkType
7
+ } from './types';
8
+
9
+ // Constants definition
10
+ const CSS_CLASSES = {
11
+ BASE: 'router-link',
12
+ ACTIVE: 'router-link-active',
13
+ EXACT_ACTIVE: 'router-link-exact-active'
14
+ } satisfies Record<string, string>;
15
+ /**
16
+ * Normalize navigation type with backward compatibility for deprecated replace property
17
+ */
18
+ function normalizeNavigationType(props: RouterLinkProps): RouterLinkType {
19
+ if (props.replace) {
20
+ console.warn(
21
+ '[RouterLink] The `replace` property is deprecated and will be removed in a future version.\n' +
22
+ 'Please use `type="replace"` instead.\n' +
23
+ 'Before: <RouterLink replace={true} />\n' +
24
+ 'After: <RouterLink type="replace" />'
25
+ );
26
+ return 'replace';
27
+ }
28
+ return props.type || 'push';
29
+ }
30
+
31
+ /**
32
+ * Get event type list - normalize and validate event types
33
+ */
34
+ function getEventTypeList(eventType: unknown | unknown[]): string[] {
35
+ const events = Array.isArray(eventType) ? eventType : [eventType];
36
+ const validEvents = events
37
+ .filter((type): type is string => typeof type === 'string')
38
+ .map((type) => type.trim())
39
+ .filter(Boolean);
40
+ return validEvents.length ? validEvents : ['click'];
41
+ }
42
+
43
+ /**
44
+ * Event guard check - determines if the router should handle the navigation
45
+ *
46
+ * Returns false: Let browser handle default behavior (normal link navigation)
47
+ * Returns true: Router takes over navigation, prevents default browser behavior
48
+ *
49
+ * This function intelligently decides when to let the browser handle clicks
50
+ * (like Ctrl+click for new tabs) vs when to use SPA routing
51
+ */
52
+ function guardEvent(e: MouseEvent): boolean {
53
+ // don't redirect with control keys
54
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return false;
55
+ // don't redirect when preventDefault called
56
+ if (e.defaultPrevented) return false;
57
+ // don't redirect on right click
58
+ if (e.button !== undefined && e.button !== 0) return false;
59
+ // Prevent default browser navigation to enable SPA routing
60
+ // Note: this may be a Weex event which doesn't have this method
61
+ if (e.preventDefault) e.preventDefault();
62
+
63
+ return true;
64
+ }
65
+
66
+ /**
67
+ * Execute route navigation
68
+ */
69
+ async function executeNavigation(
70
+ router: Router,
71
+ props: RouterLinkProps,
72
+ linkType: RouterLinkType
73
+ ): Promise<void> {
74
+ const { to, layerOptions } = props;
75
+
76
+ switch (linkType) {
77
+ case 'push':
78
+ await router.push(to);
79
+ break;
80
+ case 'replace':
81
+ await router.replace(to);
82
+ break;
83
+ case 'pushWindow':
84
+ await router.pushWindow(to);
85
+ break;
86
+ case 'replaceWindow':
87
+ await router.replaceWindow(to);
88
+ break;
89
+ case 'pushLayer':
90
+ await router.pushLayer(
91
+ layerOptions
92
+ ? typeof to === 'string'
93
+ ? { path: to, layer: layerOptions }
94
+ : { ...to, layer: layerOptions }
95
+ : to
96
+ );
97
+ break;
98
+ default:
99
+ await router.push(to);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Create navigation function
105
+ */
106
+ function createNavigateFunction(
107
+ router: Router,
108
+ props: RouterLinkProps,
109
+ navigationType: RouterLinkType
110
+ ): (e?: MouseEvent) => Promise<void> {
111
+ return async (e?: MouseEvent): Promise<void> => {
112
+ if (e && !guardEvent(e)) {
113
+ return;
114
+ }
115
+
116
+ await executeNavigation(router, props, navigationType);
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Compute HTML attributes
122
+ */
123
+ function computeAttributes(
124
+ href: string,
125
+ navigationType: RouterLinkType,
126
+ isExternal: boolean,
127
+ isActive: boolean,
128
+ isExactActive: boolean,
129
+ activeClass?: string
130
+ ): RouterLinkAttributes {
131
+ // Only pushWindow opens in a new window, replaceWindow replaces current window
132
+ const isNewWindow = navigationType === 'pushWindow';
133
+
134
+ // Build CSS classes
135
+ const classes: string[] = [CSS_CLASSES.BASE];
136
+ if (isActive) {
137
+ classes.push(activeClass || CSS_CLASSES.ACTIVE);
138
+ }
139
+ if (isExactActive) {
140
+ classes.push(CSS_CLASSES.EXACT_ACTIVE);
141
+ }
142
+
143
+ const attributes: RouterLinkAttributes = {
144
+ href,
145
+ class: classes.join(' ')
146
+ };
147
+
148
+ // Set target for new window
149
+ if (isNewWindow) {
150
+ attributes.target = '_blank';
151
+ }
152
+
153
+ // Build rel attribute
154
+ const relParts: string[] = [];
155
+ if (isNewWindow) {
156
+ relParts.push('noopener', 'noreferrer');
157
+ }
158
+ if (isExternal) {
159
+ relParts.push('external', 'nofollow');
160
+ }
161
+ if (relParts.length > 0) {
162
+ attributes.rel = relParts.join(' ');
163
+ }
164
+
165
+ return attributes;
166
+ }
167
+
168
+ /**
169
+ * Create event handlers generator function
170
+ */
171
+ function createEventHandlersGenerator(
172
+ navigate: (e?: MouseEvent) => Promise<void>,
173
+ eventTypes: string[]
174
+ ): (
175
+ nameTransform?: (eventType: string) => string
176
+ ) => Record<string, (e: MouseEvent) => Promise<void>> {
177
+ return (nameTransform?: (eventType: string) => string) => {
178
+ const handlers: Record<string, (e: MouseEvent) => Promise<void>> = {};
179
+
180
+ eventTypes.forEach((eventType) => {
181
+ const eventName = nameTransform
182
+ ? nameTransform(eventType)
183
+ : eventType.toLowerCase();
184
+ handlers[eventName] = navigate;
185
+ });
186
+
187
+ return handlers;
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Framework-agnostic link resolver
193
+ *
194
+ * @param router Router instance
195
+ * @param props Link configuration
196
+ * @returns Resolution result
197
+ */
198
+ export function createLinkResolver(
199
+ router: Router,
200
+ props: RouterLinkProps
201
+ ): RouterLinkResolved {
202
+ const route = router.resolve(props.to);
203
+ const type = normalizeNavigationType(props);
204
+ const href = route.url.href;
205
+
206
+ const isActive = router.isRouteMatched(route, props.exact);
207
+ const isExactActive = router.isRouteMatched(route, 'exact');
208
+ const isExternal = route.url.origin !== router.route.url.origin;
209
+
210
+ const navigate = createNavigateFunction(router, props, type);
211
+
212
+ const attributes = computeAttributes(
213
+ href,
214
+ type,
215
+ isExternal,
216
+ isActive,
217
+ isExactActive,
218
+ props.activeClass
219
+ );
220
+
221
+ const eventTypes = getEventTypeList(props.event || 'click');
222
+ const getEventHandlers = createEventHandlersGenerator(navigate, eventTypes);
223
+
224
+ return {
225
+ route,
226
+ type,
227
+ isActive,
228
+ isExactActive,
229
+ isExternal,
230
+ tag: props.tag || 'a',
231
+ attributes,
232
+ navigate,
233
+ getEventHandlers
234
+ };
235
+ }
@@ -0,0 +1,140 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
2
+ import { Router } from './router';
3
+ import { RouteType, RouterMode } from './types';
4
+
5
+ describe('Router Push Tests', () => {
6
+ let router: Router;
7
+
8
+ beforeEach(async () => {
9
+ router = new Router({
10
+ mode: RouterMode.memory,
11
+ base: new URL('http://localhost:3000/'),
12
+ routes: [
13
+ { path: '/', component: () => 'Home' },
14
+ { path: '/user/:id', component: () => 'User' },
15
+ { path: '/about', component: () => 'About' },
16
+ {
17
+ path: '/async',
18
+ asyncComponent: () =>
19
+ new Promise((resolve) =>
20
+ setTimeout(() => resolve('AsyncComponent'), 10)
21
+ )
22
+ }
23
+ ]
24
+ });
25
+
26
+ await router.replace('/');
27
+ });
28
+
29
+ afterEach(() => {
30
+ router.destroy();
31
+ });
32
+
33
+ describe('Basic push navigation', () => {
34
+ test('should successfully push to new route', async () => {
35
+ const route = await router.push('/user/123');
36
+
37
+ expect(route.path).toBe('/user/123');
38
+ expect(route.params.id).toBe('123');
39
+ expect(route.type).toBe(RouteType.push);
40
+ expect(route.handle).not.toBe(null);
41
+ expect(router.route.path).toBe('/user/123');
42
+ });
43
+
44
+ test('should handle query parameters in push', async () => {
45
+ const route = await router.push(
46
+ '/user/123?tab=profile&active=true'
47
+ );
48
+
49
+ expect(route.params.id).toBe('123');
50
+ expect(route.query.tab).toBe('profile');
51
+ expect(route.query.active).toBe('true');
52
+ });
53
+
54
+ test('should handle async component loading', async () => {
55
+ const route = await router.push('/async');
56
+
57
+ expect(route.path).toBe('/async');
58
+ expect(route.handle).not.toBe(null);
59
+ });
60
+ });
61
+
62
+ describe('Error handling', () => {
63
+ test('should throw error for async component loading failure', async () => {
64
+ const errorRouter = new Router({
65
+ mode: RouterMode.memory,
66
+ base: new URL('http://localhost:3000/'),
67
+ routes: [
68
+ { path: '/', component: () => 'Home' },
69
+ {
70
+ path: '/error',
71
+ asyncComponent: () =>
72
+ Promise.reject(new Error('Loading failed'))
73
+ }
74
+ ]
75
+ });
76
+
77
+ await errorRouter.replace('/');
78
+ await expect(errorRouter.push('/error')).rejects.toThrow();
79
+
80
+ errorRouter.destroy();
81
+ });
82
+
83
+ test('should throw error when guard prevents navigation', async () => {
84
+ const guardRouter = new Router({
85
+ mode: RouterMode.memory,
86
+ base: new URL('http://localhost:3000/'),
87
+ routes: [
88
+ { path: '/', component: () => 'Home' },
89
+ {
90
+ path: '/protected',
91
+ component: () => 'Protected',
92
+ beforeEnter: () => false
93
+ }
94
+ ]
95
+ });
96
+
97
+ await guardRouter.replace('/');
98
+ await expect(guardRouter.push('/protected')).rejects.toThrow();
99
+
100
+ guardRouter.destroy();
101
+ });
102
+ });
103
+
104
+ describe('Concurrent navigation', () => {
105
+ test('should handle concurrent push operations', async () => {
106
+ const promises = [
107
+ router.push('/user/1').catch((err) => err),
108
+ router.push('/user/2').catch((err) => err)
109
+ ];
110
+
111
+ const [result1, result2] = await Promise.all(promises);
112
+
113
+ const successResults = [result1, result2].filter(
114
+ (r) => !(r instanceof Error)
115
+ );
116
+ const errorResults = [result1, result2].filter(
117
+ (r) => r instanceof Error
118
+ );
119
+
120
+ expect(successResults).toHaveLength(1);
121
+ expect(errorResults).toHaveLength(1);
122
+ });
123
+ });
124
+
125
+ describe('Edge cases', () => {
126
+ test('should handle push to current route', async () => {
127
+ await router.push('/about');
128
+ const route = await router.push('/about');
129
+
130
+ expect(route.path).toBe('/about');
131
+ expect(route.handle).not.toBe(null);
132
+ });
133
+
134
+ test('should handle empty parameter', async () => {
135
+ const route = await router.push('');
136
+ expect(route).toBeDefined();
137
+ expect(typeof route.path).toBe('string');
138
+ });
139
+ });
140
+ });
@@ -0,0 +1,139 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
2
+ import { Router } from './router';
3
+ import { RouteType, RouterMode } from './types';
4
+
5
+ describe('Router Replace Tests', () => {
6
+ let router: Router;
7
+
8
+ beforeEach(async () => {
9
+ router = new Router({
10
+ mode: RouterMode.memory,
11
+ base: new URL('http://localhost:3000/'),
12
+ routes: [
13
+ { path: '/', component: () => 'Home' },
14
+ { path: '/user/:id', component: () => 'User' },
15
+ { path: '/about', component: () => 'About' },
16
+ {
17
+ path: '/async',
18
+ asyncComponent: () =>
19
+ new Promise((resolve) =>
20
+ setTimeout(() => resolve('AsyncComponent'), 10)
21
+ )
22
+ }
23
+ ]
24
+ });
25
+
26
+ await router.replace('/');
27
+ });
28
+
29
+ afterEach(() => {
30
+ router.destroy();
31
+ });
32
+
33
+ describe('Basic replace navigation', () => {
34
+ test('should successfully replace current route', async () => {
35
+ await router.push('/about');
36
+ const route = await router.replace('/user/123');
37
+
38
+ expect(route.path).toBe('/user/123');
39
+ expect(route.params.id).toBe('123');
40
+ expect(route.type).toBe(RouteType.replace);
41
+ expect(route.handle).not.toBe(null);
42
+ expect(router.route.path).toBe('/user/123');
43
+ });
44
+
45
+ test('should handle query parameters in replace', async () => {
46
+ const route = await router.replace(
47
+ '/user/123?tab=profile&active=true'
48
+ );
49
+
50
+ expect(route.params.id).toBe('123');
51
+ expect(route.query.tab).toBe('profile');
52
+ expect(route.query.active).toBe('true');
53
+ });
54
+
55
+ test('should handle async component loading', async () => {
56
+ const route = await router.replace('/async');
57
+
58
+ expect(route.path).toBe('/async');
59
+ expect(route.handle).not.toBe(null);
60
+ });
61
+ });
62
+
63
+ describe('Error handling', () => {
64
+ test('should throw error for async component loading failure', async () => {
65
+ const errorRouter = new Router({
66
+ mode: RouterMode.memory,
67
+ base: new URL('http://localhost:3000/'),
68
+ routes: [
69
+ { path: '/', component: () => 'Home' },
70
+ {
71
+ path: '/error',
72
+ asyncComponent: () =>
73
+ Promise.reject(new Error('Loading failed'))
74
+ }
75
+ ]
76
+ });
77
+
78
+ await errorRouter.replace('/');
79
+ await expect(errorRouter.replace('/error')).rejects.toThrow();
80
+
81
+ errorRouter.destroy();
82
+ });
83
+
84
+ test('should throw error when guard prevents navigation', async () => {
85
+ const guardRouter = new Router({
86
+ mode: RouterMode.memory,
87
+ base: new URL('http://localhost:3000/'),
88
+ routes: [
89
+ { path: '/', component: () => 'Home' },
90
+ {
91
+ path: '/protected',
92
+ component: () => 'Protected',
93
+ beforeEnter: () => false
94
+ }
95
+ ]
96
+ });
97
+
98
+ await guardRouter.replace('/');
99
+ await expect(guardRouter.replace('/protected')).rejects.toThrow();
100
+
101
+ guardRouter.destroy();
102
+ });
103
+ });
104
+
105
+ describe('History management', () => {
106
+ test('should not create new history entry', async () => {
107
+ await router.push('/about');
108
+ await router.replace('/user/123');
109
+
110
+ const backRoute = await router.back();
111
+ expect(backRoute?.path).toBe('/');
112
+ });
113
+
114
+ test('should replace current entry in history', async () => {
115
+ await router.push('/about');
116
+ await router.push('/user/456');
117
+ await router.replace('/user/789');
118
+
119
+ const backRoute = await router.back();
120
+ expect(backRoute?.path).toBe('/about');
121
+ });
122
+ });
123
+
124
+ describe('Edge cases', () => {
125
+ test('should handle replace to current route', async () => {
126
+ await router.push('/about');
127
+ const route = await router.replace('/about');
128
+
129
+ expect(route.path).toBe('/about');
130
+ expect(route.handle).not.toBe(null);
131
+ });
132
+
133
+ test('should handle empty parameter', async () => {
134
+ const route = await router.replace('');
135
+ expect(route).toBeDefined();
136
+ expect(typeof route.path).toBe('string');
137
+ });
138
+ });
139
+ });