@esmx/router-vue 3.0.0-rc.103
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 +21 -0
- package/README.md +570 -0
- package/README.zh-CN.md +570 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +13 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.mjs +216 -0
- package/dist/plugin.d.ts +61 -0
- package/dist/plugin.mjs +41 -0
- package/dist/plugin.test.d.ts +1 -0
- package/dist/plugin.test.mjs +631 -0
- package/dist/router-link.d.ts +220 -0
- package/dist/router-link.mjs +119 -0
- package/dist/router-link.test.d.ts +1 -0
- package/dist/router-link.test.mjs +663 -0
- package/dist/router-view.d.ts +31 -0
- package/dist/router-view.mjs +15 -0
- package/dist/router-view.test.d.ts +1 -0
- package/dist/router-view.test.mjs +676 -0
- package/dist/run-with-context.test.d.ts +1 -0
- package/dist/run-with-context.test.mjs +57 -0
- package/dist/use.d.ts +260 -0
- package/dist/use.mjs +125 -0
- package/dist/use.test.d.ts +1 -0
- package/dist/use.test.mjs +381 -0
- package/dist/util.d.ts +20 -0
- package/dist/util.mjs +49 -0
- package/dist/util.test.d.ts +4 -0
- package/dist/util.test.mjs +604 -0
- package/dist/vue2.d.ts +15 -0
- package/dist/vue2.mjs +0 -0
- package/dist/vue3.d.ts +13 -0
- package/dist/vue3.mjs +0 -0
- package/package.json +85 -0
- package/src/index.test.ts +273 -0
- package/src/index.ts +15 -0
- package/src/plugin.test.ts +812 -0
- package/src/plugin.ts +107 -0
- package/src/router-link.test.ts +830 -0
- package/src/router-link.ts +172 -0
- package/src/router-view.test.ts +840 -0
- package/src/router-view.ts +59 -0
- package/src/run-with-context.test.ts +64 -0
- package/src/use.test.ts +484 -0
- package/src/use.ts +416 -0
- package/src/util.test.ts +760 -0
- package/src/util.ts +85 -0
- package/src/vue2.ts +18 -0
- package/src/vue3.ts +15 -0
|
@@ -0,0 +1,830 @@
|
|
|
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, ref } 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:8000/')
|
|
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
|
+
try {
|
|
70
|
+
// Wait for any pending navigation to complete before destroying
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
72
|
+
router.destroy();
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// Ignore router destruction errors, as they might be expected
|
|
75
|
+
// when navigation tasks are cancelled during cleanup
|
|
76
|
+
if (
|
|
77
|
+
!(error instanceof Error) ||
|
|
78
|
+
!error.message.includes('RouteTaskCancelledError')
|
|
79
|
+
) {
|
|
80
|
+
console.warn('Router destruction error:', error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (container.parentNode) {
|
|
85
|
+
container.parentNode.removeChild(container);
|
|
86
|
+
}
|
|
87
|
+
// Wait for cleanup
|
|
88
|
+
await nextTick();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('Component Definition', () => {
|
|
92
|
+
it('should have correct component name', () => {
|
|
93
|
+
expect(RouterLink.name).toBe('RouterLink');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should have properly configured props', () => {
|
|
97
|
+
const props = RouterLink.props;
|
|
98
|
+
|
|
99
|
+
// Verify required props
|
|
100
|
+
expect(props.to).toBeDefined();
|
|
101
|
+
expect(props.to.required).toBe(true);
|
|
102
|
+
|
|
103
|
+
// Verify default values
|
|
104
|
+
expect(props.type).toBeDefined();
|
|
105
|
+
expect(props.type.default).toBe('push');
|
|
106
|
+
|
|
107
|
+
expect(props.exact).toBeDefined();
|
|
108
|
+
expect(props.exact.default).toBe('include');
|
|
109
|
+
|
|
110
|
+
expect(props.tag).toBeDefined();
|
|
111
|
+
expect(props.tag.default).toBe('a');
|
|
112
|
+
|
|
113
|
+
expect(props.event).toBeDefined();
|
|
114
|
+
expect(props.event.default).toBe('click');
|
|
115
|
+
|
|
116
|
+
expect(props.replace).toBeDefined();
|
|
117
|
+
expect(props.replace.default).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should have setup function defined', () => {
|
|
121
|
+
expect(RouterLink.setup).toBeDefined();
|
|
122
|
+
expect(typeof RouterLink.setup).toBe('function');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Component Rendering', () => {
|
|
127
|
+
it('should render basic router link', async () => {
|
|
128
|
+
const TestApp = defineComponent({
|
|
129
|
+
setup() {
|
|
130
|
+
useProvideRouter(router);
|
|
131
|
+
return () =>
|
|
132
|
+
h(RouterLink, { to: '/about' }, () => 'About Link');
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
app = createApp(TestApp);
|
|
137
|
+
app.mount(container);
|
|
138
|
+
await nextTick();
|
|
139
|
+
|
|
140
|
+
const linkElement = container.querySelector('a');
|
|
141
|
+
expect(linkElement).toBeTruthy();
|
|
142
|
+
expect(linkElement?.textContent).toBe('About Link');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should render router link with custom attributes', async () => {
|
|
146
|
+
const TestApp = defineComponent({
|
|
147
|
+
setup() {
|
|
148
|
+
useProvideRouter(router);
|
|
149
|
+
return () =>
|
|
150
|
+
h(
|
|
151
|
+
RouterLink,
|
|
152
|
+
{
|
|
153
|
+
to: '/about',
|
|
154
|
+
'data-test': 'custom-attr',
|
|
155
|
+
title: 'Custom Title'
|
|
156
|
+
},
|
|
157
|
+
() => 'Link with Attributes'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
app = createApp(TestApp);
|
|
163
|
+
app.mount(container);
|
|
164
|
+
await nextTick();
|
|
165
|
+
|
|
166
|
+
const linkElement = container.querySelector('a');
|
|
167
|
+
expect(linkElement).toBeTruthy();
|
|
168
|
+
expect(linkElement?.getAttribute('data-test')).toBe('custom-attr');
|
|
169
|
+
expect(linkElement?.getAttribute('title')).toBe('Custom Title');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should render with custom tag', async () => {
|
|
173
|
+
const TestApp = defineComponent({
|
|
174
|
+
setup() {
|
|
175
|
+
useProvideRouter(router);
|
|
176
|
+
return () =>
|
|
177
|
+
h(
|
|
178
|
+
RouterLink,
|
|
179
|
+
{
|
|
180
|
+
to: '/contact',
|
|
181
|
+
tag: 'button'
|
|
182
|
+
},
|
|
183
|
+
() => 'Contact Button'
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
app = createApp(TestApp);
|
|
189
|
+
app.mount(container);
|
|
190
|
+
await nextTick();
|
|
191
|
+
|
|
192
|
+
const buttonElement = container.querySelector('button');
|
|
193
|
+
expect(buttonElement).toBeTruthy();
|
|
194
|
+
expect(buttonElement?.textContent).toBe('Contact Button');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should render with active class when route matches', async () => {
|
|
198
|
+
// Navigate to /about first and wait for completion
|
|
199
|
+
await router.push('/about');
|
|
200
|
+
await nextTick();
|
|
201
|
+
|
|
202
|
+
const TestApp = defineComponent({
|
|
203
|
+
setup() {
|
|
204
|
+
useProvideRouter(router);
|
|
205
|
+
return () =>
|
|
206
|
+
h(
|
|
207
|
+
RouterLink,
|
|
208
|
+
{
|
|
209
|
+
to: '/about',
|
|
210
|
+
activeClass: 'active-link'
|
|
211
|
+
},
|
|
212
|
+
() => 'Current Page'
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app = createApp(TestApp);
|
|
218
|
+
app.mount(container);
|
|
219
|
+
await nextTick();
|
|
220
|
+
|
|
221
|
+
const linkElement = container.querySelector('a');
|
|
222
|
+
expect(linkElement).toBeTruthy();
|
|
223
|
+
expect(linkElement?.classList.contains('active-link')).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should handle different navigation types', async () => {
|
|
227
|
+
const TestApp = defineComponent({
|
|
228
|
+
setup() {
|
|
229
|
+
useProvideRouter(router);
|
|
230
|
+
return () =>
|
|
231
|
+
h('div', [
|
|
232
|
+
h(
|
|
233
|
+
RouterLink,
|
|
234
|
+
{
|
|
235
|
+
to: '/about',
|
|
236
|
+
type: 'push'
|
|
237
|
+
},
|
|
238
|
+
() => 'Push Link'
|
|
239
|
+
),
|
|
240
|
+
h(
|
|
241
|
+
RouterLink,
|
|
242
|
+
{
|
|
243
|
+
to: '/contact',
|
|
244
|
+
type: 'replace'
|
|
245
|
+
},
|
|
246
|
+
() => 'Replace Link'
|
|
247
|
+
)
|
|
248
|
+
]);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
app = createApp(TestApp);
|
|
253
|
+
app.mount(container);
|
|
254
|
+
await nextTick();
|
|
255
|
+
|
|
256
|
+
const links = container.querySelectorAll('a');
|
|
257
|
+
expect(links).toHaveLength(2);
|
|
258
|
+
expect(links[0]?.textContent).toBe('Push Link');
|
|
259
|
+
expect(links[1]?.textContent).toBe('Replace Link');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('Navigation Functionality', () => {
|
|
264
|
+
it('should navigate when clicked', async () => {
|
|
265
|
+
const TestApp = defineComponent({
|
|
266
|
+
setup() {
|
|
267
|
+
useProvideRouter(router);
|
|
268
|
+
return () =>
|
|
269
|
+
h(
|
|
270
|
+
RouterLink,
|
|
271
|
+
{ to: '/about' },
|
|
272
|
+
() => 'Navigate to About'
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
app = createApp(TestApp);
|
|
278
|
+
app.mount(container);
|
|
279
|
+
await nextTick();
|
|
280
|
+
|
|
281
|
+
const linkElement = container.querySelector('a');
|
|
282
|
+
expect(linkElement).toBeTruthy();
|
|
283
|
+
|
|
284
|
+
// Simulate click and wait for navigation
|
|
285
|
+
const clickPromise = new Promise<void>((resolve) => {
|
|
286
|
+
router.afterEach(() => resolve());
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
linkElement?.click();
|
|
290
|
+
await clickPromise;
|
|
291
|
+
await nextTick();
|
|
292
|
+
|
|
293
|
+
// Check if navigation occurred
|
|
294
|
+
expect(router.route.path).toBe('/about');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should handle custom events', async () => {
|
|
298
|
+
const TestApp = defineComponent({
|
|
299
|
+
setup() {
|
|
300
|
+
useProvideRouter(router);
|
|
301
|
+
return () =>
|
|
302
|
+
h(
|
|
303
|
+
RouterLink,
|
|
304
|
+
{
|
|
305
|
+
to: '/contact',
|
|
306
|
+
event: 'mouseenter'
|
|
307
|
+
},
|
|
308
|
+
() => 'Hover to Navigate'
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
app = createApp(TestApp);
|
|
314
|
+
app.mount(container);
|
|
315
|
+
await nextTick();
|
|
316
|
+
|
|
317
|
+
const linkElement = container.querySelector('a');
|
|
318
|
+
expect(linkElement).toBeTruthy();
|
|
319
|
+
|
|
320
|
+
// Simulate mouseenter event and wait for navigation
|
|
321
|
+
const navigationPromise = new Promise<void>((resolve) => {
|
|
322
|
+
router.afterEach(() => resolve());
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const event = new MouseEvent('mouseenter', { bubbles: true });
|
|
326
|
+
linkElement?.dispatchEvent(event);
|
|
327
|
+
await navigationPromise;
|
|
328
|
+
await nextTick();
|
|
329
|
+
|
|
330
|
+
// Check if navigation occurred
|
|
331
|
+
expect(router.route.path).toBe('/contact');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should handle object-based route navigation', async () => {
|
|
335
|
+
const TestApp = defineComponent({
|
|
336
|
+
setup() {
|
|
337
|
+
useProvideRouter(router);
|
|
338
|
+
return () =>
|
|
339
|
+
h(
|
|
340
|
+
RouterLink,
|
|
341
|
+
{
|
|
342
|
+
to: { path: '/about', query: { tab: 'info' } }
|
|
343
|
+
},
|
|
344
|
+
() => 'About with Query'
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
app = createApp(TestApp);
|
|
350
|
+
app.mount(container);
|
|
351
|
+
await nextTick();
|
|
352
|
+
|
|
353
|
+
const linkElement = container.querySelector('a');
|
|
354
|
+
expect(linkElement).toBeTruthy();
|
|
355
|
+
|
|
356
|
+
// Simulate click and wait for navigation
|
|
357
|
+
const navigationPromise = new Promise<void>((resolve) => {
|
|
358
|
+
router.afterEach(() => resolve());
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
linkElement?.click();
|
|
362
|
+
await navigationPromise;
|
|
363
|
+
await nextTick();
|
|
364
|
+
|
|
365
|
+
// Check if navigation occurred with query
|
|
366
|
+
expect(router.route.path).toBe('/about');
|
|
367
|
+
expect(router.route.query.tab).toBe('info');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should handle custom navigation handler', async () => {
|
|
371
|
+
let customHandlerCalled = false;
|
|
372
|
+
let receivedEventName = '';
|
|
373
|
+
const TestApp = defineComponent({
|
|
374
|
+
setup() {
|
|
375
|
+
useProvideRouter(router);
|
|
376
|
+
return () =>
|
|
377
|
+
h(
|
|
378
|
+
RouterLink,
|
|
379
|
+
{
|
|
380
|
+
to: '/about',
|
|
381
|
+
beforeNavigate: (
|
|
382
|
+
event: Event,
|
|
383
|
+
eventName: string
|
|
384
|
+
) => {
|
|
385
|
+
customHandlerCalled = true;
|
|
386
|
+
receivedEventName = eventName;
|
|
387
|
+
event.preventDefault();
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
() => 'Custom Handler Link'
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
app = createApp(TestApp);
|
|
396
|
+
app.mount(container);
|
|
397
|
+
await nextTick();
|
|
398
|
+
|
|
399
|
+
const linkElement = container.querySelector('a');
|
|
400
|
+
expect(linkElement).toBeTruthy();
|
|
401
|
+
|
|
402
|
+
// Simulate click
|
|
403
|
+
linkElement?.click();
|
|
404
|
+
await nextTick();
|
|
405
|
+
|
|
406
|
+
// Check if custom handler was called with correct event name
|
|
407
|
+
expect(customHandlerCalled).toBe(true);
|
|
408
|
+
expect(receivedEventName).toBe('click');
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('Props Validation', () => {
|
|
413
|
+
it('should accept string as to prop', async () => {
|
|
414
|
+
const TestApp = defineComponent({
|
|
415
|
+
setup() {
|
|
416
|
+
useProvideRouter(router);
|
|
417
|
+
return () =>
|
|
418
|
+
h(RouterLink, { to: '/about' }, () => 'String Route');
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
expect(() => {
|
|
423
|
+
app = createApp(TestApp);
|
|
424
|
+
app.mount(container);
|
|
425
|
+
}).not.toThrow();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('should accept object as to prop', async () => {
|
|
429
|
+
const TestApp = defineComponent({
|
|
430
|
+
setup() {
|
|
431
|
+
useProvideRouter(router);
|
|
432
|
+
return () =>
|
|
433
|
+
h(
|
|
434
|
+
RouterLink,
|
|
435
|
+
{
|
|
436
|
+
to: { path: '/contact' }
|
|
437
|
+
},
|
|
438
|
+
() => 'Object Route'
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
expect(() => {
|
|
444
|
+
app = createApp(TestApp);
|
|
445
|
+
app.mount(container);
|
|
446
|
+
}).not.toThrow();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should handle array of events', async () => {
|
|
450
|
+
const TestApp = defineComponent({
|
|
451
|
+
setup() {
|
|
452
|
+
useProvideRouter(router);
|
|
453
|
+
return () =>
|
|
454
|
+
h(
|
|
455
|
+
RouterLink,
|
|
456
|
+
{
|
|
457
|
+
to: '/about',
|
|
458
|
+
event: ['click', 'keydown']
|
|
459
|
+
},
|
|
460
|
+
() => 'Multi Event Link'
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
app = createApp(TestApp);
|
|
466
|
+
app.mount(container);
|
|
467
|
+
await nextTick();
|
|
468
|
+
|
|
469
|
+
const linkElement = container.querySelector('a');
|
|
470
|
+
expect(linkElement).toBeTruthy();
|
|
471
|
+
|
|
472
|
+
// Test click event
|
|
473
|
+
const clickPromise = new Promise<void>((resolve) => {
|
|
474
|
+
router.afterEach(() => resolve());
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
linkElement?.click();
|
|
478
|
+
await clickPromise;
|
|
479
|
+
await nextTick();
|
|
480
|
+
expect(router.route.path).toBe('/about');
|
|
481
|
+
|
|
482
|
+
// Reset route and test keydown event
|
|
483
|
+
await router.push('/');
|
|
484
|
+
await nextTick();
|
|
485
|
+
|
|
486
|
+
const keydownPromise = new Promise<void>((resolve) => {
|
|
487
|
+
router.afterEach(() => resolve());
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const keyEvent = new KeyboardEvent('keydown', { key: 'Enter' });
|
|
491
|
+
linkElement?.dispatchEvent(keyEvent);
|
|
492
|
+
await keydownPromise;
|
|
493
|
+
await nextTick();
|
|
494
|
+
expect(router.route.path).toBe('/about');
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
describe('Error Handling', () => {
|
|
499
|
+
it('should throw error when router context is missing', () => {
|
|
500
|
+
const TestApp = defineComponent({
|
|
501
|
+
setup() {
|
|
502
|
+
// No useProvideRouter call - missing router context
|
|
503
|
+
return () =>
|
|
504
|
+
h(RouterLink, { to: '/about' }, () => 'No Router');
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
expect(() => {
|
|
509
|
+
app = createApp(TestApp);
|
|
510
|
+
app.mount(container);
|
|
511
|
+
}).toThrow();
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
describe('Slot Rendering', () => {
|
|
516
|
+
it('should render default slot content', async () => {
|
|
517
|
+
const TestApp = defineComponent({
|
|
518
|
+
setup() {
|
|
519
|
+
useProvideRouter(router);
|
|
520
|
+
return () =>
|
|
521
|
+
h(
|
|
522
|
+
RouterLink,
|
|
523
|
+
{ to: '/about' },
|
|
524
|
+
{
|
|
525
|
+
default: () =>
|
|
526
|
+
h(
|
|
527
|
+
'span',
|
|
528
|
+
{ class: 'link-text' },
|
|
529
|
+
'Custom Content'
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
app = createApp(TestApp);
|
|
537
|
+
app.mount(container);
|
|
538
|
+
await nextTick();
|
|
539
|
+
|
|
540
|
+
const spanElement = container.querySelector('span.link-text');
|
|
541
|
+
expect(spanElement).toBeTruthy();
|
|
542
|
+
expect(spanElement?.textContent).toBe('Custom Content');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should render complex slot content', async () => {
|
|
546
|
+
const TestApp = defineComponent({
|
|
547
|
+
setup() {
|
|
548
|
+
useProvideRouter(router);
|
|
549
|
+
return () =>
|
|
550
|
+
h(
|
|
551
|
+
RouterLink,
|
|
552
|
+
{ to: '/contact' },
|
|
553
|
+
{
|
|
554
|
+
default: () => [
|
|
555
|
+
h('i', { class: 'icon' }, '→'),
|
|
556
|
+
h('span', 'Contact Us')
|
|
557
|
+
]
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
app = createApp(TestApp);
|
|
564
|
+
app.mount(container);
|
|
565
|
+
await nextTick();
|
|
566
|
+
|
|
567
|
+
const iconElement = container.querySelector('i.icon');
|
|
568
|
+
const spanElement = container.querySelector('span');
|
|
569
|
+
expect(iconElement).toBeTruthy();
|
|
570
|
+
expect(spanElement).toBeTruthy();
|
|
571
|
+
expect(iconElement?.textContent).toBe('→');
|
|
572
|
+
expect(spanElement?.textContent).toBe('Contact Us');
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe('Active State Management', () => {
|
|
577
|
+
it('should apply active class with exact matching', async () => {
|
|
578
|
+
// Navigate to exact route and wait for completion
|
|
579
|
+
await router.push('/about');
|
|
580
|
+
await nextTick();
|
|
581
|
+
|
|
582
|
+
const TestApp = defineComponent({
|
|
583
|
+
setup() {
|
|
584
|
+
useProvideRouter(router);
|
|
585
|
+
return () =>
|
|
586
|
+
h('div', [
|
|
587
|
+
h(
|
|
588
|
+
RouterLink,
|
|
589
|
+
{
|
|
590
|
+
to: '/about',
|
|
591
|
+
exact: 'exact',
|
|
592
|
+
activeClass: 'exact-active'
|
|
593
|
+
},
|
|
594
|
+
() => 'Exact Match'
|
|
595
|
+
),
|
|
596
|
+
h(
|
|
597
|
+
RouterLink,
|
|
598
|
+
{
|
|
599
|
+
to: '/about/sub',
|
|
600
|
+
exact: 'exact',
|
|
601
|
+
activeClass: 'exact-active'
|
|
602
|
+
},
|
|
603
|
+
() => 'Not Exact'
|
|
604
|
+
)
|
|
605
|
+
]);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
app = createApp(TestApp);
|
|
610
|
+
app.mount(container);
|
|
611
|
+
await nextTick();
|
|
612
|
+
|
|
613
|
+
const links = container.querySelectorAll('a');
|
|
614
|
+
expect(links[0]?.classList.contains('exact-active')).toBe(true);
|
|
615
|
+
expect(links[1]?.classList.contains('exact-active')).toBe(false);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('should apply active class with include matching', async () => {
|
|
619
|
+
// Navigate to a route and wait for completion
|
|
620
|
+
await router.push('/about');
|
|
621
|
+
await nextTick();
|
|
622
|
+
|
|
623
|
+
const TestApp = defineComponent({
|
|
624
|
+
setup() {
|
|
625
|
+
useProvideRouter(router);
|
|
626
|
+
return () =>
|
|
627
|
+
h(
|
|
628
|
+
RouterLink,
|
|
629
|
+
{
|
|
630
|
+
to: '/about',
|
|
631
|
+
exact: 'include',
|
|
632
|
+
activeClass: 'include-active'
|
|
633
|
+
},
|
|
634
|
+
() => 'Include Match'
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
app = createApp(TestApp);
|
|
640
|
+
app.mount(container);
|
|
641
|
+
await nextTick();
|
|
642
|
+
|
|
643
|
+
const linkElement = container.querySelector('a');
|
|
644
|
+
// Should be active because current route '/about' matches exactly
|
|
645
|
+
expect(linkElement?.classList.contains('include-active')).toBe(
|
|
646
|
+
true
|
|
647
|
+
);
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
describe('Reactivity', () => {
|
|
652
|
+
it('should update active class when route changes', async () => {
|
|
653
|
+
await router.replace('/');
|
|
654
|
+
await nextTick();
|
|
655
|
+
|
|
656
|
+
const TestApp = defineComponent({
|
|
657
|
+
setup() {
|
|
658
|
+
useProvideRouter(router);
|
|
659
|
+
return () =>
|
|
660
|
+
h(
|
|
661
|
+
RouterLink,
|
|
662
|
+
{
|
|
663
|
+
to: '/about',
|
|
664
|
+
activeClass: 'active-link'
|
|
665
|
+
},
|
|
666
|
+
() => 'About'
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
app = createApp(TestApp);
|
|
672
|
+
app.mount(container);
|
|
673
|
+
await nextTick();
|
|
674
|
+
|
|
675
|
+
const linkElement = container.querySelector('a');
|
|
676
|
+
expect(linkElement?.classList.contains('active-link')).toBe(false);
|
|
677
|
+
|
|
678
|
+
const toAbout = new Promise<void>((resolve) => {
|
|
679
|
+
router.afterEach(() => resolve());
|
|
680
|
+
});
|
|
681
|
+
await router.push('/about');
|
|
682
|
+
await toAbout;
|
|
683
|
+
await nextTick();
|
|
684
|
+
expect(linkElement?.classList.contains('active-link')).toBe(true);
|
|
685
|
+
|
|
686
|
+
const toContact = new Promise<void>((resolve) => {
|
|
687
|
+
router.afterEach(() => resolve());
|
|
688
|
+
});
|
|
689
|
+
await router.push('/contact');
|
|
690
|
+
await toContact;
|
|
691
|
+
await nextTick();
|
|
692
|
+
expect(linkElement?.classList.contains('active-link')).toBe(false);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('should update rendering when props.to changes', async () => {
|
|
696
|
+
await router.replace('/about');
|
|
697
|
+
await nextTick();
|
|
698
|
+
|
|
699
|
+
const toProp = ref('/about');
|
|
700
|
+
|
|
701
|
+
const TestApp = defineComponent({
|
|
702
|
+
setup() {
|
|
703
|
+
useProvideRouter(router);
|
|
704
|
+
return () =>
|
|
705
|
+
h(
|
|
706
|
+
RouterLink,
|
|
707
|
+
{
|
|
708
|
+
to: toProp.value,
|
|
709
|
+
activeClass: 'active-link'
|
|
710
|
+
},
|
|
711
|
+
() => 'Dynamic To'
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
app = createApp(TestApp);
|
|
717
|
+
app.mount(container);
|
|
718
|
+
await nextTick();
|
|
719
|
+
|
|
720
|
+
const linkElement = container.querySelector('a');
|
|
721
|
+
expect(linkElement?.classList.contains('active-link')).toBe(true);
|
|
722
|
+
|
|
723
|
+
toProp.value = '/contact';
|
|
724
|
+
await nextTick();
|
|
725
|
+
expect(linkElement?.classList.contains('active-link')).toBe(false);
|
|
726
|
+
|
|
727
|
+
const toContact = new Promise<void>((resolve) => {
|
|
728
|
+
router.afterEach(() => resolve());
|
|
729
|
+
});
|
|
730
|
+
await router.push('/contact');
|
|
731
|
+
await toContact;
|
|
732
|
+
await nextTick();
|
|
733
|
+
expect(linkElement?.classList.contains('active-link')).toBe(true);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it('should update event handlers when props.event changes', async () => {
|
|
737
|
+
await router.replace('/');
|
|
738
|
+
await nextTick();
|
|
739
|
+
|
|
740
|
+
const eventProp = ref<'click' | 'mouseenter'>('click');
|
|
741
|
+
|
|
742
|
+
const TestApp = defineComponent({
|
|
743
|
+
setup() {
|
|
744
|
+
useProvideRouter(router);
|
|
745
|
+
return () =>
|
|
746
|
+
h(
|
|
747
|
+
RouterLink,
|
|
748
|
+
{
|
|
749
|
+
to: '/about',
|
|
750
|
+
event: eventProp.value
|
|
751
|
+
},
|
|
752
|
+
() => 'Event Link'
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
app = createApp(TestApp);
|
|
758
|
+
app.mount(container);
|
|
759
|
+
await nextTick();
|
|
760
|
+
|
|
761
|
+
const linkElement = container.querySelector('a');
|
|
762
|
+
expect(linkElement).toBeTruthy();
|
|
763
|
+
|
|
764
|
+
const clickNav = new Promise<void>((resolve) => {
|
|
765
|
+
router.afterEach(() => resolve());
|
|
766
|
+
});
|
|
767
|
+
linkElement?.click();
|
|
768
|
+
await clickNav;
|
|
769
|
+
await nextTick();
|
|
770
|
+
expect(router.route.path).toBe('/about');
|
|
771
|
+
|
|
772
|
+
const backNav = new Promise<void>((resolve) => {
|
|
773
|
+
router.afterEach(() => resolve());
|
|
774
|
+
});
|
|
775
|
+
await router.replace('/');
|
|
776
|
+
await backNav;
|
|
777
|
+
await nextTick();
|
|
778
|
+
|
|
779
|
+
eventProp.value = 'mouseenter';
|
|
780
|
+
await nextTick();
|
|
781
|
+
|
|
782
|
+
linkElement?.click();
|
|
783
|
+
await nextTick();
|
|
784
|
+
expect(router.route.path).toBe('/');
|
|
785
|
+
|
|
786
|
+
const hoverNav = new Promise<void>((resolve) => {
|
|
787
|
+
router.afterEach(() => resolve());
|
|
788
|
+
});
|
|
789
|
+
const event = new MouseEvent('mouseenter', { bubbles: true });
|
|
790
|
+
linkElement?.dispatchEvent(event);
|
|
791
|
+
await hoverNav;
|
|
792
|
+
await nextTick();
|
|
793
|
+
expect(router.route.path).toBe('/about');
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it('should re-render when tag prop changes', async () => {
|
|
797
|
+
const tagProp = ref<'a' | 'button'>('a');
|
|
798
|
+
|
|
799
|
+
const TestApp = defineComponent({
|
|
800
|
+
setup() {
|
|
801
|
+
useProvideRouter(router);
|
|
802
|
+
return () =>
|
|
803
|
+
h(
|
|
804
|
+
RouterLink,
|
|
805
|
+
{
|
|
806
|
+
to: '/about',
|
|
807
|
+
tag: tagProp.value
|
|
808
|
+
},
|
|
809
|
+
() => 'Tag Link'
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
app = createApp(TestApp);
|
|
815
|
+
app.mount(container);
|
|
816
|
+
await nextTick();
|
|
817
|
+
|
|
818
|
+
const anchorElement = container.querySelector('a');
|
|
819
|
+
expect(anchorElement).toBeTruthy();
|
|
820
|
+
|
|
821
|
+
tagProp.value = 'button';
|
|
822
|
+
await nextTick();
|
|
823
|
+
|
|
824
|
+
const buttonElement = container.querySelector('button');
|
|
825
|
+
const oldAnchor = container.querySelector('a');
|
|
826
|
+
expect(buttonElement).toBeTruthy();
|
|
827
|
+
expect(oldAnchor).toBeFalsy();
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
});
|