5
5
6
6
using System . Reflection ;
7
7
using Microsoft . AspNetCore . Components . RenderTree ;
8
+ using Microsoft . AspNetCore . Components . Rendering ;
8
9
using Microsoft . AspNetCore . Components . Test . Helpers ;
9
10
using Microsoft . Extensions . DependencyInjection ;
10
11
using Microsoft . Extensions . Logging ;
@@ -301,6 +302,192 @@ await renderer.Dispatcher.InvokeAsync(() =>
301
302
Assert . Contains ( "Use either NotFound or NotFoundPage" , exception . Message ) ;
302
303
}
303
304
305
+ [ Fact ]
306
+ public async Task OnNotFound_WithNotFoundPageSet_UsesNotFoundPage ( )
307
+ {
308
+ // Create a new router instance for this test to control Attach() timing
309
+ var services = new ServiceCollection ( ) ;
310
+ var testNavManager = new TestNavigationManager ( ) ;
311
+ services . AddSingleton < ILoggerFactory > ( NullLoggerFactory . Instance ) ;
312
+ services . AddSingleton < NavigationManager > ( testNavManager ) ;
313
+ services . AddSingleton < INavigationInterception , TestNavigationInterception > ( ) ;
314
+ services . AddSingleton < IScrollToLocationHash , TestScrollToLocationHash > ( ) ;
315
+ var serviceProvider = services . BuildServiceProvider ( ) ;
316
+
317
+ var testRenderer = new TestRenderer ( serviceProvider ) ;
318
+ testRenderer . ShouldHandleExceptions = true ;
319
+ var testRouter = ( Router ) testRenderer . InstantiateComponent < Router > ( ) ;
320
+ testRouter . AppAssembly = Assembly . GetExecutingAssembly ( ) ;
321
+ testRouter . Found = routeData => ( builder ) => builder . AddContent ( 0 , $ "Rendering route matching { routeData . PageType } ") ;
322
+
323
+ var parameters = new Dictionary < string , object >
324
+ {
325
+ { nameof ( Router . AppAssembly ) , typeof ( RouterTest ) . Assembly } ,
326
+ { nameof ( Router . NotFoundPage ) , typeof ( NotFoundTestComponent ) }
327
+ } ;
328
+
329
+ // Assign the root component ID which will call Attach()
330
+ testRenderer . AssignRootComponentId ( testRouter ) ;
331
+
332
+ // Act
333
+ await testRenderer . Dispatcher . InvokeAsync ( ( ) =>
334
+ testRouter . SetParametersAsync ( ParameterView . FromDictionary ( parameters ) ) ) ;
335
+
336
+ // Trigger the NavigationManager's OnNotFound event
337
+ await testRenderer . Dispatcher . InvokeAsync ( ( ) => testNavManager . TriggerNotFound ( ) ) ;
338
+
339
+ // Assert
340
+ var lastBatch = testRenderer . Batches . Last ( ) ;
341
+ var renderedFrame = lastBatch . ReferenceFrames . First ( ) ;
342
+ Assert . Equal ( RenderTreeFrameType . Component , renderedFrame . FrameType ) ;
343
+ Assert . Equal ( typeof ( RouteView ) , renderedFrame . ComponentType ) ;
344
+
345
+ // Verify that the RouteData contains the NotFoundTestComponent
346
+ var routeViewFrame = lastBatch . ReferenceFrames . Skip ( 1 ) . First ( ) ;
347
+ Assert . Equal ( RenderTreeFrameType . Attribute , routeViewFrame . FrameType ) ;
348
+ var routeData = ( RouteData ) routeViewFrame . AttributeValue ;
349
+ Assert . Equal ( typeof ( NotFoundTestComponent ) , routeData . PageType ) ;
350
+ }
351
+
352
+ [ Fact ]
353
+ public async Task OnNotFound_WithArgsPathSet_RendersComponentByRoute ( )
354
+ {
355
+ // Create a new router instance for this test to control Attach() timing
356
+ var services = new ServiceCollection ( ) ;
357
+ var testNavManager = new TestNavigationManager ( ) ;
358
+ services . AddSingleton < ILoggerFactory > ( NullLoggerFactory . Instance ) ;
359
+ services . AddSingleton < NavigationManager > ( testNavManager ) ;
360
+ services . AddSingleton < INavigationInterception , TestNavigationInterception > ( ) ;
361
+ services . AddSingleton < IScrollToLocationHash , TestScrollToLocationHash > ( ) ;
362
+ var serviceProvider = services . BuildServiceProvider ( ) ;
363
+
364
+ var testRenderer = new TestRenderer ( serviceProvider ) ;
365
+ testRenderer . ShouldHandleExceptions = true ;
366
+ var testRouter = ( Router ) testRenderer . InstantiateComponent < Router > ( ) ;
367
+ testRouter . AppAssembly = Assembly . GetExecutingAssembly ( ) ;
368
+ testRouter . Found = routeData => ( builder ) => builder . AddContent ( 0 , $ "Rendering route matching { routeData . PageType } ") ;
369
+
370
+ var parameters = new Dictionary < string , object >
371
+ {
372
+ { nameof ( Router . AppAssembly ) , typeof ( RouterTest ) . Assembly }
373
+ } ;
374
+
375
+ // Subscribe to OnNotFound event BEFORE router attaches and set args.Path
376
+ testNavManager . OnNotFound += ( sender , args ) =>
377
+ {
378
+ args . Path = "/jan" ; // Point to an existing route
379
+ } ;
380
+
381
+ // Assign the root component ID which will call Attach()
382
+ testRenderer . AssignRootComponentId ( testRouter ) ;
383
+
384
+ // Act
385
+ await testRenderer . Dispatcher . InvokeAsync ( ( ) =>
386
+ testRouter . SetParametersAsync ( ParameterView . FromDictionary ( parameters ) ) ) ;
387
+
388
+ // Trigger the NavigationManager's OnNotFound event
389
+ await testRenderer . Dispatcher . InvokeAsync ( ( ) => testNavManager . TriggerNotFound ( ) ) ;
390
+
391
+ // Assert
392
+ var lastBatch = testRenderer . Batches . Last ( ) ;
393
+ var renderedFrame = lastBatch . ReferenceFrames . First ( ) ;
394
+ Assert . Equal ( RenderTreeFrameType . Component , renderedFrame . FrameType ) ;
395
+ Assert . Equal ( typeof ( RouteView ) , renderedFrame . ComponentType ) ;
396
+
397
+ // Verify that the RouteData contains the correct component type
398
+ var routeViewFrame = lastBatch . ReferenceFrames . Skip ( 1 ) . First ( ) ;
399
+ Assert . Equal ( RenderTreeFrameType . Attribute , routeViewFrame . FrameType ) ;
400
+ var routeData = ( RouteData ) routeViewFrame . AttributeValue ;
401
+ Assert . Equal ( typeof ( JanComponent ) , routeData . PageType ) ;
402
+ }
403
+
404
+ [ Fact ]
405
+ public async Task OnNotFound_WithBothNotFoundPageAndArgsPath_PreferArgs ( )
406
+ {
407
+ // Create a new router instance for this test to control Attach() timing
408
+ var services = new ServiceCollection ( ) ;
409
+ var testNavManager = new TestNavigationManager ( ) ;
410
+ services . AddSingleton < ILoggerFactory > ( NullLoggerFactory . Instance ) ;
411
+ services . AddSingleton < NavigationManager > ( testNavManager ) ;
412
+ services . AddSingleton < INavigationInterception , TestNavigationInterception > ( ) ;
413
+ services . AddSingleton < IScrollToLocationHash , TestScrollToLocationHash > ( ) ;
414
+ var serviceProvider = services . BuildServiceProvider ( ) ;
415
+
416
+ var testRenderer = new TestRenderer ( serviceProvider ) ;
417
+ testRenderer . ShouldHandleExceptions = true ;
418
+ var testRouter = ( Router ) testRenderer . InstantiateComponent < Router > ( ) ;
419
+ testRouter . AppAssembly = Assembly . GetExecutingAssembly ( ) ;
420
+ testRouter . Found = routeData => ( builder ) => builder . AddContent ( 0 , $ "Rendering route matching { routeData . PageType } ") ;
421
+
422
+ var parameters = new Dictionary < string , object >
423
+ {
424
+ { nameof ( Router . AppAssembly ) , typeof ( RouterTest ) . Assembly } ,
425
+ { nameof ( Router . NotFoundPage ) , typeof ( NotFoundTestComponent ) }
426
+ } ;
427
+
428
+ // Subscribe to OnNotFound event BEFORE router attaches and sets up its own subscription
429
+ testNavManager . OnNotFound += ( sender , args ) =>
430
+ {
431
+ args . Path = "/jan" ; // This should take precedence over NotFoundPage
432
+ } ;
433
+
434
+ // Now assign the root component ID which will call Attach()
435
+ testRenderer . AssignRootComponentId ( testRouter ) ;
436
+
437
+ await testRenderer . Dispatcher . InvokeAsync ( ( ) =>
438
+ testRouter . SetParametersAsync ( ParameterView . FromDictionary ( parameters ) ) ) ;
439
+
440
+ // trigger the NavigationManager's OnNotFound event
441
+ await testRenderer . Dispatcher . InvokeAsync ( ( ) => testNavManager . TriggerNotFound ( ) ) ;
442
+
443
+ // The Router should have rendered using RenderComponentByRoute (args.Path) instead of NotFoundPage
444
+ var lastBatch = testRenderer . Batches . Last ( ) ;
445
+ var renderedFrame = lastBatch . ReferenceFrames . First ( ) ;
446
+ Assert . Equal ( RenderTreeFrameType . Component , renderedFrame . FrameType ) ;
447
+ Assert . Equal ( typeof ( RouteView ) , renderedFrame . ComponentType ) ;
448
+
449
+ // Verify that the RouteData contains the JanComponent (from args.Path), not NotFoundTestComponent
450
+ var routeViewFrame = lastBatch . ReferenceFrames . Skip ( 1 ) . First ( ) ;
451
+ Assert . Equal ( RenderTreeFrameType . Attribute , routeViewFrame . FrameType ) ;
452
+ var routeData = ( RouteData ) routeViewFrame . AttributeValue ;
453
+ Assert . Equal ( typeof ( JanComponent ) , routeData . PageType ) ;
454
+ }
455
+
456
+ [ Fact ]
457
+ public async Task FindComponentTypeByRoute_WithValidRoute_ReturnsComponentType ( )
458
+ {
459
+ var parameters = new Dictionary < string , object >
460
+ {
461
+ { nameof ( Router . AppAssembly ) , typeof ( RouterTest ) . Assembly }
462
+ } ;
463
+
464
+ await _renderer . Dispatcher . InvokeAsync ( ( ) =>
465
+ _router . SetParametersAsync ( ParameterView . FromDictionary ( parameters ) ) ) ;
466
+
467
+ var result = _router . FindComponentTypeByRoute ( "/jan" ) ;
468
+ Assert . Equal ( typeof ( JanComponent ) , result ) ;
469
+ }
470
+
471
+ [ Fact ]
472
+ public async Task RenderComponentByRoute_WithInvalidRoute_ThrowsException ( )
473
+ {
474
+ var parameters = new Dictionary < string , object >
475
+ {
476
+ { nameof ( Router . AppAssembly ) , typeof ( RouterTest ) . Assembly }
477
+ } ;
478
+
479
+ await _renderer . Dispatcher . InvokeAsync ( ( ) =>
480
+ _router . SetParametersAsync ( ParameterView . FromDictionary ( parameters ) ) ) ;
481
+
482
+ var builder = new RenderTreeBuilder ( ) ;
483
+
484
+ var exception = Assert . Throws < InvalidOperationException > ( ( ) =>
485
+ {
486
+ _router . RenderComponentByRoute ( builder , "/nonexistent-route" ) ;
487
+ } ) ;
488
+ Assert . Contains ( "No component found for route '/nonexistent-route'" , exception . Message ) ;
489
+ }
490
+
304
491
internal class TestNavigationManager : NavigationManager
305
492
{
306
493
public TestNavigationManager ( ) =>
@@ -311,6 +498,11 @@ public void NotifyLocationChanged(string uri, bool intercepted, string state = n
311
498
Uri = uri ;
312
499
NotifyLocationChanged ( intercepted ) ;
313
500
}
501
+
502
+ public void TriggerNotFound ( )
503
+ {
504
+ base . NotFound ( ) ;
505
+ }
314
506
}
315
507
316
508
internal sealed class TestNavigationInterception : INavigationInterception
0 commit comments