-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Make OnNavigateAsync EventCallback and cancel previous navigation #25011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,9 +10,7 @@ | |
| using Microsoft.Extensions.DependencyInjection; | ||
| using Microsoft.Extensions.Logging; | ||
| using Microsoft.Extensions.Logging.Abstractions; | ||
| using Moq; | ||
| using Xunit; | ||
| using Microsoft.AspNetCore.Components; | ||
|
|
||
| namespace Microsoft.AspNetCore.Components.Test.Routing | ||
| { | ||
|
|
@@ -42,109 +40,61 @@ public async Task CanRunOnNavigateAsync() | |
| { | ||
| // Arrange | ||
| var called = false; | ||
| async Task OnNavigateAsync(NavigationContext args) | ||
| Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) => | ||
| { | ||
| await Task.CompletedTask; | ||
| called = true; | ||
| } | ||
| _router.OnNavigateAsync = OnNavigateAsync; | ||
|
|
||
| // Act | ||
| await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); | ||
|
|
||
| // Assert | ||
| Assert.True(called); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task CanHandleSingleFailedOnNavigateAsync() | ||
| { | ||
| // Arrange | ||
| var called = false; | ||
| async Task OnNavigateAsync(NavigationContext args) | ||
| { | ||
| called = true; | ||
| await Task.CompletedTask; | ||
| throw new Exception("This is an uncaught exception."); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Orthogonal to this change: We should test what happens when this throws synchronously and asynchronously. For example: Task OnNavigateAsync(NavigationContext args)
{
called = true;
throw new Exception("This is an uncaught exception.");
return Task.CompletedTask;
}Which throws synchronously VS the method above. (Which also throws synchronously, but the async machinery will transform it into a failed task). For throwing asynchronously we actually need to go async async Task OnNavigateAsync(NavigationContext args)
{
called = true;
await Task.Delay(1000); // Or better than this, a task completion source
throw new Exception("This is an uncaught exception.");
}
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added an E2E test for this. E2E tests work a little bit better here because there isn't a good way to attach the event callback the router component in the unit tests. |
||
| _router.OnNavigateAsync = OnNavigateAsync; | ||
| }; | ||
| _router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync); | ||
|
|
||
| // Act | ||
| await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); | ||
| await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false)); | ||
|
|
||
| // Assert | ||
| Assert.True(called); | ||
| Assert.Single(_renderer.HandledExceptions); | ||
| var unhandledException = _renderer.HandledExceptions[0]; | ||
| Assert.Equal("This is an uncaught exception.", unhandledException.Message); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task CanceledFailedOnNavigateAsyncDoesNothing() | ||
| { | ||
| // Arrange | ||
| var onNavigateInvoked = 0; | ||
| async Task OnNavigateAsync(NavigationContext args) | ||
| Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) => | ||
| { | ||
| onNavigateInvoked += 1; | ||
| if (args.Path.EndsWith("jan")) | ||
| { | ||
| await Task.Delay(Timeout.Infinite, args.CancellationToken); | ||
| throw new Exception("This is an uncaught exception."); | ||
| } | ||
| } | ||
| var refreshCalled = false; | ||
| }; | ||
| var refreshCalled = 0; | ||
| _renderer.OnUpdateDisplay = (renderBatch) => | ||
| { | ||
| if (!refreshCalled) | ||
| { | ||
| refreshCalled = true; | ||
| return; | ||
| } | ||
| Assert.True(false, "OnUpdateDisplay called more than once."); | ||
| refreshCalled += 1; | ||
| return; | ||
| }; | ||
| _router.OnNavigateAsync = OnNavigateAsync; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It will be good to capture the new behavior of OnUpdateDisplay, I imagine that this has changed since we are removing it. I would suggest we capture on an integer, the number of times it is being called and check against how many times we expect it to be called. That will help catch regressions if in the future we make a change to the implementation that results in a subtle change in the rendering sequence.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added this. |
||
| _router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync); | ||
|
|
||
| // Act | ||
| var janTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); | ||
| var febTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false)); | ||
| var janTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false)); | ||
| var febTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false)); | ||
|
|
||
| await janTask; | ||
| await febTask; | ||
|
|
||
| // Assert that we render the second route component and don't throw an exception | ||
| Assert.Empty(_renderer.HandledExceptions); | ||
| Assert.Equal(2, onNavigateInvoked); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task CanHandleSingleCancelledOnNavigateAsync() | ||
| { | ||
| // Arrange | ||
| async Task OnNavigateAsync(NavigationContext args) | ||
| { | ||
| var tcs = new TaskCompletionSource<int>(); | ||
| tcs.TrySetCanceled(); | ||
| await tcs.Task; | ||
| } | ||
| _renderer.OnUpdateDisplay = (renderBatch) => Assert.True(false, "OnUpdateDisplay called more than once."); | ||
| _router.OnNavigateAsync = OnNavigateAsync; | ||
|
|
||
| // Act | ||
| await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); | ||
|
|
||
| // Assert | ||
| Assert.Single(_renderer.HandledExceptions); | ||
| var unhandledException = _renderer.HandledExceptions[0]; | ||
| Assert.Equal("OnNavigateAsync can only be cancelled via NavigateContext.CancellationToken.", unhandledException.Message); | ||
| Assert.Equal(2, refreshCalled); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task AlreadyCanceledOnNavigateAsyncDoesNothing() | ||
| { | ||
| // Arrange | ||
| var triggerCancel = new TaskCompletionSource(); | ||
| async Task OnNavigateAsync(NavigationContext args) | ||
| Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) => | ||
| { | ||
| if (args.Path.EndsWith("jan")) | ||
| { | ||
|
|
@@ -153,7 +103,7 @@ async Task OnNavigateAsync(NavigationContext args) | |
| tcs.TrySetCanceled(); | ||
| await tcs.Task; | ||
| } | ||
| } | ||
| }; | ||
| var refreshCalled = false; | ||
| _renderer.OnUpdateDisplay = (renderBatch) => | ||
| { | ||
|
|
@@ -164,11 +114,11 @@ async Task OnNavigateAsync(NavigationContext args) | |
| } | ||
| Assert.True(false, "OnUpdateDisplay called more than once."); | ||
| }; | ||
| _router.OnNavigateAsync = OnNavigateAsync; | ||
| _router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync); | ||
|
|
||
| // Act (start the operations then await them) | ||
| var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); | ||
| var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false)); | ||
| var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false)); | ||
| var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false)); | ||
| triggerCancel.TrySetResult(); | ||
|
|
||
| await jan; | ||
|
|
@@ -180,16 +130,16 @@ public void CanCancelPreviousOnNavigateAsync() | |
| { | ||
| // Arrange | ||
| var cancelled = ""; | ||
| async Task OnNavigateAsync(NavigationContext args) | ||
| Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) => | ||
| { | ||
| await Task.CompletedTask; | ||
| args.CancellationToken.Register(() => cancelled = args.Path); | ||
| }; | ||
| _router.OnNavigateAsync = OnNavigateAsync; | ||
| _router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync); | ||
|
|
||
| // Act | ||
| _ = _router.RunOnNavigateWithRefreshAsync("jan", false); | ||
| _ = _router.RunOnNavigateWithRefreshAsync("feb", false); | ||
| _ = _router.RunOnNavigateAsync("jan", false); | ||
| _ = _router.RunOnNavigateAsync("feb", false); | ||
|
|
||
| // Assert | ||
| var expected = "jan"; | ||
|
|
@@ -200,7 +150,7 @@ async Task OnNavigateAsync(NavigationContext args) | |
| public async Task RefreshesOnceOnCancelledOnNavigateAsync() | ||
| { | ||
| // Arrange | ||
| async Task OnNavigateAsync(NavigationContext args) | ||
| Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) => | ||
| { | ||
| if (args.Path.EndsWith("jan")) | ||
| { | ||
|
|
@@ -217,11 +167,11 @@ async Task OnNavigateAsync(NavigationContext args) | |
| } | ||
| Assert.True(false, "OnUpdateDisplay called more than once."); | ||
| }; | ||
| _router.OnNavigateAsync = OnNavigateAsync; | ||
| _router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync); | ||
|
|
||
| // Act | ||
| var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/jan", false)); | ||
| var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateWithRefreshAsync("http://example.com/feb", false)); | ||
| var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false)); | ||
| var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false)); | ||
|
|
||
| await jan; | ||
| await feb; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we removing coverage for this? Shouldn't we still test what happens when an exception is thrown from the handler?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have an E2E test that validates this scenario here so I removed this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair, for future reference I would suggest you consider keeping the unit test and the E2E test. They complement each other.
The unit test validates the component and helps catch regressions faster while the E2E test validates the integration. Figuring out a regression with a failing E2E test is much harder than with a unit test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out that doing this in the E2E tests is the only option I have. There isn't a super nice way to create an EventCallback with the Router component instantiated with the tests (hence why we use
new EventCallback(null, action). This exercises the execution of the async action but doesn't exercise the renderer.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I do know how to do this, but it requires a bit of changes. It involves:
There are a lot of samples in the Renderer tests, but it is a bit more involved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll let you use your best judgement here.