55
66using System . Reflection ;
77using Microsoft . AspNetCore . Components . RenderTree ;
8+ using Microsoft . AspNetCore . Components . Rendering ;
89using Microsoft . AspNetCore . Components . Test . Helpers ;
910using Microsoft . Extensions . DependencyInjection ;
1011using Microsoft . Extensions . Logging ;
@@ -301,6 +302,192 @@ await renderer.Dispatcher.InvokeAsync(() =>
301302 Assert . Contains ( "Use either NotFound or NotFoundPage" , exception . Message ) ;
302303 }
303304
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+
304491 internal class TestNavigationManager : NavigationManager
305492 {
306493 public TestNavigationManager ( ) =>
@@ -311,6 +498,11 @@ public void NotifyLocationChanged(string uri, bool intercepted, string state = n
311498 Uri = uri ;
312499 NotifyLocationChanged ( intercepted ) ;
313500 }
501+
502+ public void TriggerNotFound ( )
503+ {
504+ base . NotFound ( ) ;
505+ }
314506 }
315507
316508 internal sealed class TestNavigationInterception : INavigationInterception
0 commit comments