@esmx/router-vue 3.0.0-rc.16 → 3.0.0-rc.19
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/LICENSE +1 -1
- package/README.md +563 -0
- package/README.zh-CN.md +563 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.mjs +11 -4
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.mjs +206 -0
- package/dist/plugin.d.ts +55 -11
- package/dist/plugin.mjs +32 -16
- package/dist/plugin.test.d.ts +1 -0
- package/dist/plugin.test.mjs +436 -0
- package/dist/router-link.d.ts +202 -0
- package/dist/router-link.mjs +84 -0
- package/dist/router-link.test.d.ts +1 -0
- package/dist/router-link.test.mjs +456 -0
- package/dist/router-view.d.ts +30 -0
- package/dist/router-view.mjs +17 -0
- package/dist/router-view.test.d.ts +1 -0
- package/dist/router-view.test.mjs +459 -0
- package/dist/use.d.ts +198 -3
- package/dist/use.mjs +75 -9
- package/dist/use.test.d.ts +1 -0
- package/dist/use.test.mjs +461 -0
- package/dist/util.d.ts +7 -0
- package/dist/util.mjs +24 -0
- package/dist/util.test.d.ts +1 -0
- package/dist/util.test.mjs +319 -0
- package/dist/vue2.d.ts +13 -0
- package/dist/vue2.mjs +0 -0
- package/dist/vue3.d.ts +13 -0
- package/dist/vue3.mjs +0 -0
- package/package.json +31 -14
- package/src/index.test.ts +263 -0
- package/src/index.ts +16 -4
- package/src/plugin.test.ts +574 -0
- package/src/plugin.ts +86 -31
- package/src/router-link.test.ts +569 -0
- package/src/router-link.ts +148 -0
- package/src/router-view.test.ts +599 -0
- package/src/router-view.ts +61 -0
- package/src/use.test.ts +616 -0
- package/src/use.ts +307 -11
- package/src/util.test.ts +418 -0
- package/src/util.ts +32 -0
- package/src/vue2.ts +16 -0
- package/src/vue3.ts +15 -0
- package/dist/link.d.ts +0 -101
- package/dist/link.mjs +0 -103
- package/dist/symbols.d.ts +0 -3
- package/dist/symbols.mjs +0 -3
- package/dist/view.d.ts +0 -21
- package/dist/view.mjs +0 -75
- package/src/link.ts +0 -177
- package/src/symbols.ts +0 -8
- package/src/view.ts +0 -95
package/src/plugin.ts
CHANGED
|
@@ -1,36 +1,91 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { RouterLink } from './router-link';
|
|
2
|
+
import { RouterView } from './router-view';
|
|
3
|
+
import { getRoute, getRouter } from './use';
|
|
4
|
+
import { isVue3 } from './util';
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
// $route: Route;
|
|
11
|
-
$route: ShallowReactive<Route>;
|
|
12
|
-
$router: RouterInstance;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface GlobalComponents {
|
|
16
|
-
// RouterView:
|
|
17
|
-
// RouterLink:
|
|
18
|
-
}
|
|
6
|
+
interface VueApp {
|
|
7
|
+
config?: {
|
|
8
|
+
globalProperties: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
prototype?: Record<string, unknown>;
|
|
11
|
+
component(name: string, component: unknown): void;
|
|
19
12
|
}
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Vue plugin for @esmx/router integration.
|
|
16
|
+
* Registers RouterLink and RouterView components globally.
|
|
17
|
+
* Compatible with both Vue 2.7+ and Vue 3.
|
|
18
|
+
*
|
|
19
|
+
* @example Vue 3 installation
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { createApp } from 'vue';
|
|
22
|
+
* import { Router } from '@esmx/router';
|
|
23
|
+
* import { RouterPlugin, useProvideRouter } from '@esmx/router-vue';
|
|
24
|
+
*
|
|
25
|
+
* const routes = [
|
|
26
|
+
* { path: '/', component: Home },
|
|
27
|
+
* { path: '/about', component: About }
|
|
28
|
+
* ];
|
|
29
|
+
*
|
|
30
|
+
* const router = new Router({ routes });
|
|
31
|
+
* const app = createApp({
|
|
32
|
+
* setup() {
|
|
33
|
+
* useProvideRouter(router);
|
|
34
|
+
* }
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* app.use(RouterPlugin);
|
|
38
|
+
* app.mount('#app');
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example Vue 2 installation
|
|
42
|
+
* ```typescript
|
|
43
|
+
* import Vue from 'vue';
|
|
44
|
+
* import { Router } from '@esmx/router';
|
|
45
|
+
* import { RouterPlugin, useProvideRouter } from '@esmx/router-vue';
|
|
46
|
+
*
|
|
47
|
+
* const routes = [
|
|
48
|
+
* { path: '/', component: Home },
|
|
49
|
+
* { path: '/about', component: About }
|
|
50
|
+
* ];
|
|
51
|
+
*
|
|
52
|
+
* const router = new Router({ routes });
|
|
53
|
+
* Vue.use(RouterPlugin);
|
|
54
|
+
*
|
|
55
|
+
* new Vue({
|
|
56
|
+
* setup() {
|
|
57
|
+
* useProvideRouter(router);
|
|
58
|
+
* }
|
|
59
|
+
* }).$mount('#app');
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export const RouterPlugin = {
|
|
63
|
+
/**
|
|
64
|
+
* Install the router plugin.
|
|
65
|
+
* @param app Vue application instance (Vue 3) or Vue constructor (Vue 2)
|
|
66
|
+
*/
|
|
67
|
+
install(app: unknown): void {
|
|
68
|
+
const vueApp = app as VueApp;
|
|
69
|
+
const target = vueApp.config?.globalProperties || vueApp.prototype;
|
|
25
70
|
|
|
26
|
-
|
|
27
|
-
|
|
71
|
+
if (!target) {
|
|
72
|
+
throw new Error('[@esmx/router-vue] Invalid Vue app instance');
|
|
73
|
+
}
|
|
74
|
+
Object.defineProperties(target, {
|
|
75
|
+
$router: {
|
|
76
|
+
get() {
|
|
77
|
+
return getRouter(isVue3 ? null : this);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
$route: {
|
|
81
|
+
get() {
|
|
82
|
+
return getRoute(isVue3 ? null : this);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
28
86
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
app.component('router-link', RouterLink);
|
|
35
|
-
};
|
|
36
|
-
}
|
|
87
|
+
// Register global components
|
|
88
|
+
vueApp.component('RouterLink', RouterLink);
|
|
89
|
+
vueApp.component('RouterView', RouterView);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import type { RouteConfig } from '@esmx/router';
|
|
2
|
+
import { Router, RouterMode } from '@esmx/router';
|
|
3
|
+
/**
|
|
4
|
+
* @vitest-environment happy-dom
|
|
5
|
+
*/
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
+
import { createApp, defineComponent, h, nextTick } from 'vue';
|
|
8
|
+
import { RouterLink } from './router-link';
|
|
9
|
+
import { useProvideRouter } from './use';
|
|
10
|
+
|
|
11
|
+
describe('router-link.ts - RouterLink Component', () => {
|
|
12
|
+
let router: Router;
|
|
13
|
+
let app: ReturnType<typeof createApp>;
|
|
14
|
+
let container: HTMLElement;
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
// Create DOM container
|
|
18
|
+
container = document.createElement('div');
|
|
19
|
+
container.id = 'test-app';
|
|
20
|
+
document.body.appendChild(container);
|
|
21
|
+
|
|
22
|
+
// Create test routes
|
|
23
|
+
const routes: RouteConfig[] = [
|
|
24
|
+
{
|
|
25
|
+
path: '/',
|
|
26
|
+
component: defineComponent({
|
|
27
|
+
name: 'Home',
|
|
28
|
+
template: '<div>Home Page</div>'
|
|
29
|
+
}),
|
|
30
|
+
meta: { title: 'Home' }
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
path: '/about',
|
|
34
|
+
component: defineComponent({
|
|
35
|
+
name: 'About',
|
|
36
|
+
template: '<div>About Page</div>'
|
|
37
|
+
}),
|
|
38
|
+
meta: { title: 'About' }
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: '/contact',
|
|
42
|
+
component: defineComponent({
|
|
43
|
+
name: 'Contact',
|
|
44
|
+
template: '<div>Contact Page</div>'
|
|
45
|
+
}),
|
|
46
|
+
meta: { title: 'Contact' }
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Create router instance
|
|
51
|
+
router = new Router({
|
|
52
|
+
root: '#test-app',
|
|
53
|
+
routes,
|
|
54
|
+
mode: RouterMode.memory,
|
|
55
|
+
base: new URL('http://localhost:3000/')
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Initialize router and wait for it to be ready
|
|
59
|
+
await router.replace('/');
|
|
60
|
+
await nextTick();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(async () => {
|
|
64
|
+
// Clean up
|
|
65
|
+
if (app) {
|
|
66
|
+
app.unmount();
|
|
67
|
+
}
|
|
68
|
+
if (router) {
|
|
69
|
+
router.destroy();
|
|
70
|
+
}
|
|
71
|
+
if (container.parentNode) {
|
|
72
|
+
container.parentNode.removeChild(container);
|
|
73
|
+
}
|
|
74
|
+
// Wait for cleanup
|
|
75
|
+
await nextTick();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('Component Definition', () => {
|
|
79
|
+
it('should have correct component name', () => {
|
|
80
|
+
expect(RouterLink.name).toBe('RouterLink');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should have properly configured props', () => {
|
|
84
|
+
const props = RouterLink.props;
|
|
85
|
+
|
|
86
|
+
// Verify required props
|
|
87
|
+
expect(props.to).toBeDefined();
|
|
88
|
+
expect(props.to.required).toBe(true);
|
|
89
|
+
|
|
90
|
+
// Verify default values
|
|
91
|
+
expect(props.type).toBeDefined();
|
|
92
|
+
expect(props.type.default).toBe('push');
|
|
93
|
+
|
|
94
|
+
expect(props.exact).toBeDefined();
|
|
95
|
+
expect(props.exact.default).toBe('include');
|
|
96
|
+
|
|
97
|
+
expect(props.tag).toBeDefined();
|
|
98
|
+
expect(props.tag.default).toBe('a');
|
|
99
|
+
|
|
100
|
+
expect(props.event).toBeDefined();
|
|
101
|
+
expect(props.event.default).toBe('click');
|
|
102
|
+
|
|
103
|
+
expect(props.replace).toBeDefined();
|
|
104
|
+
expect(props.replace.default).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should have setup function defined', () => {
|
|
108
|
+
expect(RouterLink.setup).toBeDefined();
|
|
109
|
+
expect(typeof RouterLink.setup).toBe('function');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('Component Rendering', () => {
|
|
114
|
+
it('should render basic router link', async () => {
|
|
115
|
+
const TestApp = defineComponent({
|
|
116
|
+
setup() {
|
|
117
|
+
useProvideRouter(router);
|
|
118
|
+
return () =>
|
|
119
|
+
h(RouterLink, { to: '/about' }, () => 'About Link');
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
app = createApp(TestApp);
|
|
124
|
+
app.mount(container);
|
|
125
|
+
await nextTick();
|
|
126
|
+
|
|
127
|
+
const linkElement = container.querySelector('a');
|
|
128
|
+
expect(linkElement).toBeTruthy();
|
|
129
|
+
expect(linkElement?.textContent).toBe('About Link');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should render with custom tag', async () => {
|
|
133
|
+
const TestApp = defineComponent({
|
|
134
|
+
setup() {
|
|
135
|
+
useProvideRouter(router);
|
|
136
|
+
return () =>
|
|
137
|
+
h(
|
|
138
|
+
RouterLink,
|
|
139
|
+
{
|
|
140
|
+
to: '/contact',
|
|
141
|
+
tag: 'button'
|
|
142
|
+
},
|
|
143
|
+
() => 'Contact Button'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
app = createApp(TestApp);
|
|
149
|
+
app.mount(container);
|
|
150
|
+
await nextTick();
|
|
151
|
+
|
|
152
|
+
const buttonElement = container.querySelector('button');
|
|
153
|
+
expect(buttonElement).toBeTruthy();
|
|
154
|
+
expect(buttonElement?.textContent).toBe('Contact Button');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should render with active class when route matches', async () => {
|
|
158
|
+
// Navigate to /about first and wait for completion
|
|
159
|
+
await router.push('/about');
|
|
160
|
+
await nextTick();
|
|
161
|
+
|
|
162
|
+
const TestApp = defineComponent({
|
|
163
|
+
setup() {
|
|
164
|
+
useProvideRouter(router);
|
|
165
|
+
return () =>
|
|
166
|
+
h(
|
|
167
|
+
RouterLink,
|
|
168
|
+
{
|
|
169
|
+
to: '/about',
|
|
170
|
+
activeClass: 'active-link'
|
|
171
|
+
},
|
|
172
|
+
() => 'Current Page'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
app = createApp(TestApp);
|
|
178
|
+
app.mount(container);
|
|
179
|
+
await nextTick();
|
|
180
|
+
|
|
181
|
+
const linkElement = container.querySelector('a');
|
|
182
|
+
expect(linkElement).toBeTruthy();
|
|
183
|
+
expect(linkElement?.classList.contains('active-link')).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should handle different navigation types', async () => {
|
|
187
|
+
const TestApp = defineComponent({
|
|
188
|
+
setup() {
|
|
189
|
+
useProvideRouter(router);
|
|
190
|
+
return () =>
|
|
191
|
+
h('div', [
|
|
192
|
+
h(
|
|
193
|
+
RouterLink,
|
|
194
|
+
{
|
|
195
|
+
to: '/about',
|
|
196
|
+
type: 'push'
|
|
197
|
+
},
|
|
198
|
+
() => 'Push Link'
|
|
199
|
+
),
|
|
200
|
+
h(
|
|
201
|
+
RouterLink,
|
|
202
|
+
{
|
|
203
|
+
to: '/contact',
|
|
204
|
+
type: 'replace'
|
|
205
|
+
},
|
|
206
|
+
() => 'Replace Link'
|
|
207
|
+
)
|
|
208
|
+
]);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
app = createApp(TestApp);
|
|
213
|
+
app.mount(container);
|
|
214
|
+
await nextTick();
|
|
215
|
+
|
|
216
|
+
const links = container.querySelectorAll('a');
|
|
217
|
+
expect(links).toHaveLength(2);
|
|
218
|
+
expect(links[0]?.textContent).toBe('Push Link');
|
|
219
|
+
expect(links[1]?.textContent).toBe('Replace Link');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('Navigation Functionality', () => {
|
|
224
|
+
it('should navigate when clicked', async () => {
|
|
225
|
+
const TestApp = defineComponent({
|
|
226
|
+
setup() {
|
|
227
|
+
useProvideRouter(router);
|
|
228
|
+
return () =>
|
|
229
|
+
h(
|
|
230
|
+
RouterLink,
|
|
231
|
+
{ to: '/about' },
|
|
232
|
+
() => 'Navigate to About'
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
app = createApp(TestApp);
|
|
238
|
+
app.mount(container);
|
|
239
|
+
await nextTick();
|
|
240
|
+
|
|
241
|
+
const linkElement = container.querySelector('a');
|
|
242
|
+
expect(linkElement).toBeTruthy();
|
|
243
|
+
|
|
244
|
+
// Simulate click and wait for navigation
|
|
245
|
+
const clickPromise = new Promise<void>((resolve) => {
|
|
246
|
+
router.afterEach(() => resolve());
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
linkElement?.click();
|
|
250
|
+
await clickPromise;
|
|
251
|
+
await nextTick();
|
|
252
|
+
|
|
253
|
+
// Check if navigation occurred
|
|
254
|
+
expect(router.route.path).toBe('/about');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should handle custom events', async () => {
|
|
258
|
+
const TestApp = defineComponent({
|
|
259
|
+
setup() {
|
|
260
|
+
useProvideRouter(router);
|
|
261
|
+
return () =>
|
|
262
|
+
h(
|
|
263
|
+
RouterLink,
|
|
264
|
+
{
|
|
265
|
+
to: '/contact',
|
|
266
|
+
event: 'mouseenter'
|
|
267
|
+
},
|
|
268
|
+
() => 'Hover to Navigate'
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
app = createApp(TestApp);
|
|
274
|
+
app.mount(container);
|
|
275
|
+
await nextTick();
|
|
276
|
+
|
|
277
|
+
const linkElement = container.querySelector('a');
|
|
278
|
+
expect(linkElement).toBeTruthy();
|
|
279
|
+
|
|
280
|
+
// Simulate mouseenter event and wait for navigation
|
|
281
|
+
const navigationPromise = new Promise<void>((resolve) => {
|
|
282
|
+
router.afterEach(() => resolve());
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const event = new MouseEvent('mouseenter', { bubbles: true });
|
|
286
|
+
linkElement?.dispatchEvent(event);
|
|
287
|
+
await navigationPromise;
|
|
288
|
+
await nextTick();
|
|
289
|
+
|
|
290
|
+
// Check if navigation occurred
|
|
291
|
+
expect(router.route.path).toBe('/contact');
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should handle object-based route navigation', async () => {
|
|
295
|
+
const TestApp = defineComponent({
|
|
296
|
+
setup() {
|
|
297
|
+
useProvideRouter(router);
|
|
298
|
+
return () =>
|
|
299
|
+
h(
|
|
300
|
+
RouterLink,
|
|
301
|
+
{
|
|
302
|
+
to: { path: '/about', query: { tab: 'info' } }
|
|
303
|
+
},
|
|
304
|
+
() => 'About with Query'
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
app = createApp(TestApp);
|
|
310
|
+
app.mount(container);
|
|
311
|
+
await nextTick();
|
|
312
|
+
|
|
313
|
+
const linkElement = container.querySelector('a');
|
|
314
|
+
expect(linkElement).toBeTruthy();
|
|
315
|
+
|
|
316
|
+
// Simulate click and wait for navigation
|
|
317
|
+
const navigationPromise = new Promise<void>((resolve) => {
|
|
318
|
+
router.afterEach(() => resolve());
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
linkElement?.click();
|
|
322
|
+
await navigationPromise;
|
|
323
|
+
await nextTick();
|
|
324
|
+
|
|
325
|
+
// Check if navigation occurred with query
|
|
326
|
+
expect(router.route.path).toBe('/about');
|
|
327
|
+
expect(router.route.query.tab).toBe('info');
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('Props Validation', () => {
|
|
332
|
+
it('should accept string as to prop', async () => {
|
|
333
|
+
const TestApp = defineComponent({
|
|
334
|
+
setup() {
|
|
335
|
+
useProvideRouter(router);
|
|
336
|
+
return () =>
|
|
337
|
+
h(RouterLink, { to: '/about' }, () => 'String Route');
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(() => {
|
|
342
|
+
app = createApp(TestApp);
|
|
343
|
+
app.mount(container);
|
|
344
|
+
}).not.toThrow();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should accept object as to prop', async () => {
|
|
348
|
+
const TestApp = defineComponent({
|
|
349
|
+
setup() {
|
|
350
|
+
useProvideRouter(router);
|
|
351
|
+
return () =>
|
|
352
|
+
h(
|
|
353
|
+
RouterLink,
|
|
354
|
+
{
|
|
355
|
+
to: { path: '/contact' }
|
|
356
|
+
},
|
|
357
|
+
() => 'Object Route'
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
expect(() => {
|
|
363
|
+
app = createApp(TestApp);
|
|
364
|
+
app.mount(container);
|
|
365
|
+
}).not.toThrow();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('should handle array of events', async () => {
|
|
369
|
+
const TestApp = defineComponent({
|
|
370
|
+
setup() {
|
|
371
|
+
useProvideRouter(router);
|
|
372
|
+
return () =>
|
|
373
|
+
h(
|
|
374
|
+
RouterLink,
|
|
375
|
+
{
|
|
376
|
+
to: '/about',
|
|
377
|
+
event: ['click', 'keydown']
|
|
378
|
+
},
|
|
379
|
+
() => 'Multi Event Link'
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
app = createApp(TestApp);
|
|
385
|
+
app.mount(container);
|
|
386
|
+
await nextTick();
|
|
387
|
+
|
|
388
|
+
const linkElement = container.querySelector('a');
|
|
389
|
+
expect(linkElement).toBeTruthy();
|
|
390
|
+
|
|
391
|
+
// Test click event
|
|
392
|
+
const clickPromise = new Promise<void>((resolve) => {
|
|
393
|
+
router.afterEach(() => resolve());
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
linkElement?.click();
|
|
397
|
+
await clickPromise;
|
|
398
|
+
await nextTick();
|
|
399
|
+
expect(router.route.path).toBe('/about');
|
|
400
|
+
|
|
401
|
+
// Reset route and test keydown event
|
|
402
|
+
await router.push('/');
|
|
403
|
+
await nextTick();
|
|
404
|
+
|
|
405
|
+
const keydownPromise = new Promise<void>((resolve) => {
|
|
406
|
+
router.afterEach(() => resolve());
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const keyEvent = new KeyboardEvent('keydown', { key: 'Enter' });
|
|
410
|
+
linkElement?.dispatchEvent(keyEvent);
|
|
411
|
+
await keydownPromise;
|
|
412
|
+
await nextTick();
|
|
413
|
+
expect(router.route.path).toBe('/about');
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
describe('Error Handling', () => {
|
|
418
|
+
it('should throw error when router context is missing', () => {
|
|
419
|
+
const TestApp = defineComponent({
|
|
420
|
+
setup() {
|
|
421
|
+
// No useProvideRouter call - missing router context
|
|
422
|
+
return () =>
|
|
423
|
+
h(RouterLink, { to: '/about' }, () => 'No Router');
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(() => {
|
|
428
|
+
app = createApp(TestApp);
|
|
429
|
+
app.mount(container);
|
|
430
|
+
}).toThrow();
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('Slot Rendering', () => {
|
|
435
|
+
it('should render default slot content', async () => {
|
|
436
|
+
const TestApp = defineComponent({
|
|
437
|
+
setup() {
|
|
438
|
+
useProvideRouter(router);
|
|
439
|
+
return () =>
|
|
440
|
+
h(
|
|
441
|
+
RouterLink,
|
|
442
|
+
{ to: '/about' },
|
|
443
|
+
{
|
|
444
|
+
default: () =>
|
|
445
|
+
h(
|
|
446
|
+
'span',
|
|
447
|
+
{ class: 'link-text' },
|
|
448
|
+
'Custom Content'
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
app = createApp(TestApp);
|
|
456
|
+
app.mount(container);
|
|
457
|
+
await nextTick();
|
|
458
|
+
|
|
459
|
+
const spanElement = container.querySelector('span.link-text');
|
|
460
|
+
expect(spanElement).toBeTruthy();
|
|
461
|
+
expect(spanElement?.textContent).toBe('Custom Content');
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should render complex slot content', async () => {
|
|
465
|
+
const TestApp = defineComponent({
|
|
466
|
+
setup() {
|
|
467
|
+
useProvideRouter(router);
|
|
468
|
+
return () =>
|
|
469
|
+
h(
|
|
470
|
+
RouterLink,
|
|
471
|
+
{ to: '/contact' },
|
|
472
|
+
{
|
|
473
|
+
default: () => [
|
|
474
|
+
h('i', { class: 'icon' }, '→'),
|
|
475
|
+
h('span', 'Contact Us')
|
|
476
|
+
]
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
app = createApp(TestApp);
|
|
483
|
+
app.mount(container);
|
|
484
|
+
await nextTick();
|
|
485
|
+
|
|
486
|
+
const iconElement = container.querySelector('i.icon');
|
|
487
|
+
const spanElement = container.querySelector('span');
|
|
488
|
+
expect(iconElement).toBeTruthy();
|
|
489
|
+
expect(spanElement).toBeTruthy();
|
|
490
|
+
expect(iconElement?.textContent).toBe('→');
|
|
491
|
+
expect(spanElement?.textContent).toBe('Contact Us');
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('Active State Management', () => {
|
|
496
|
+
it('should apply active class with exact matching', async () => {
|
|
497
|
+
// Navigate to exact route and wait for completion
|
|
498
|
+
await router.push('/about');
|
|
499
|
+
await nextTick();
|
|
500
|
+
|
|
501
|
+
const TestApp = defineComponent({
|
|
502
|
+
setup() {
|
|
503
|
+
useProvideRouter(router);
|
|
504
|
+
return () =>
|
|
505
|
+
h('div', [
|
|
506
|
+
h(
|
|
507
|
+
RouterLink,
|
|
508
|
+
{
|
|
509
|
+
to: '/about',
|
|
510
|
+
exact: 'exact',
|
|
511
|
+
activeClass: 'exact-active'
|
|
512
|
+
},
|
|
513
|
+
() => 'Exact Match'
|
|
514
|
+
),
|
|
515
|
+
h(
|
|
516
|
+
RouterLink,
|
|
517
|
+
{
|
|
518
|
+
to: '/about/sub',
|
|
519
|
+
exact: 'exact',
|
|
520
|
+
activeClass: 'exact-active'
|
|
521
|
+
},
|
|
522
|
+
() => 'Not Exact'
|
|
523
|
+
)
|
|
524
|
+
]);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
app = createApp(TestApp);
|
|
529
|
+
app.mount(container);
|
|
530
|
+
await nextTick();
|
|
531
|
+
|
|
532
|
+
const links = container.querySelectorAll('a');
|
|
533
|
+
expect(links[0]?.classList.contains('exact-active')).toBe(true);
|
|
534
|
+
expect(links[1]?.classList.contains('exact-active')).toBe(false);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('should apply active class with include matching', async () => {
|
|
538
|
+
// Navigate to a route and wait for completion
|
|
539
|
+
await router.push('/about');
|
|
540
|
+
await nextTick();
|
|
541
|
+
|
|
542
|
+
const TestApp = defineComponent({
|
|
543
|
+
setup() {
|
|
544
|
+
useProvideRouter(router);
|
|
545
|
+
return () =>
|
|
546
|
+
h(
|
|
547
|
+
RouterLink,
|
|
548
|
+
{
|
|
549
|
+
to: '/about',
|
|
550
|
+
exact: 'include',
|
|
551
|
+
activeClass: 'include-active'
|
|
552
|
+
},
|
|
553
|
+
() => 'Include Match'
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
app = createApp(TestApp);
|
|
559
|
+
app.mount(container);
|
|
560
|
+
await nextTick();
|
|
561
|
+
|
|
562
|
+
const linkElement = container.querySelector('a');
|
|
563
|
+
// Should be active because current route '/about' matches exactly
|
|
564
|
+
expect(linkElement?.classList.contains('include-active')).toBe(
|
|
565
|
+
true
|
|
566
|
+
);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
});
|