diff --git a/src/Components/Components/src/NavigationManager.cs b/src/Components/Components/src/NavigationManager.cs index 1900b91c629b..fecbfaca6c28 100644 --- a/src/Components/Components/src/NavigationManager.cs +++ b/src/Components/Components/src/NavigationManager.cs @@ -61,7 +61,6 @@ public event EventHandler OnNotFound // The URI. Always represented an absolute URI. private string? _uri; private bool _isInitialized; - internal string NotFoundPageRoute { get; set; } = string.Empty; /// /// Gets or sets the current base URI. The is always represented as an absolute URI in string form with trailing slash. @@ -211,7 +210,7 @@ private void NotFoundCore() } else { - _notFound.Invoke(this, new NotFoundEventArgs(NotFoundPageRoute)); + _notFound.Invoke(this, new NotFoundEventArgs()); } } diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index f5e0f10aad7d..442ad26d73a2 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -8,8 +8,9 @@ Microsoft.AspNetCore.Components.NavigationManager.OnNotFound -> System.EventHand Microsoft.AspNetCore.Components.NavigationManager.NotFound() -> void Microsoft.AspNetCore.Components.Routing.IHostEnvironmentNavigationManager.Initialize(string! baseUri, string! uri, System.Func! onNavigateTo) -> void Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs -Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundEventArgs(string! url) -> void -Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.Path.get -> string! +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.NotFoundEventArgs() -> void +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.Path.get -> string? +Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs.Path.set -> void Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.ComponentStatePersistenceManager(Microsoft.Extensions.Logging.ILogger! logger, System.IServiceProvider! serviceProvider) -> void Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.SetPlatformRenderMode(Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode) -> void Microsoft.AspNetCore.Components.Infrastructure.RegisterPersistentComponentStateServiceCollectionExtensions diff --git a/src/Components/Components/src/Routing/NotFoundEventArgs.cs b/src/Components/Components/src/Routing/NotFoundEventArgs.cs index e1e81e5cfc82..438d2d316dff 100644 --- a/src/Components/Components/src/Routing/NotFoundEventArgs.cs +++ b/src/Components/Components/src/Routing/NotFoundEventArgs.cs @@ -9,16 +9,7 @@ namespace Microsoft.AspNetCore.Components.Routing; public sealed class NotFoundEventArgs : EventArgs { /// - /// Gets the path of NotFoundPage. + /// Gets the path of NotFoundPage. If the path is set, it indicates that a subscriber has handled the rendering of the NotFound contents. /// - public string Path { get; } - - /// - /// Initializes a new instance of . - /// - public NotFoundEventArgs(string url) - { - Path = url; - } - + public string? Path { get; set; } } diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 673950373e28..df11600f6770 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -29,6 +29,7 @@ static readonly IReadOnlyDictionary _emptyParametersDictionary string _locationAbsolute; bool _navigationInterceptionEnabled; ILogger _logger; + string _notFoundPageRoute; private string _updateScrollPositionForHashLastLocation; private bool _updateScrollPositionForHash; @@ -166,7 +167,7 @@ public async Task SetParametersAsync(ParameterView parameters) var routeAttribute = (RouteAttribute)routeAttributes[0]; if (routeAttribute.Template != null) { - NavigationManager.NotFoundPageRoute = routeAttribute.Template; + _notFoundPageRoute = routeAttribute.Template; } } @@ -388,10 +389,12 @@ private void OnLocationChanged(object sender, LocationChangedEventArgs args) } } - private void OnNotFound(object sender, EventArgs args) + private void OnNotFound(object sender, NotFoundEventArgs args) { - if (_renderHandle.IsInitialized) + if (_renderHandle.IsInitialized && NotFoundPage != null) { + // setting the path signals to the endpoint renderer that router handled rendering + args.Path = _notFoundPageRoute; Log.DisplayingNotFound(_logger); RenderNotFound(); } diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 6e9bb59ab45a..22119d0522c9 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -106,7 +106,7 @@ await _renderer.InitializeStandardComponentServicesAsync( waitForQuiescence: result.IsPost || isErrorHandlerOrReExecuted); Task quiesceTask; - if (!result.IsPost) + if (!result.IsPost || isReExecuted) { quiesceTask = htmlContent.QuiescenceTask; } @@ -130,6 +130,11 @@ await _renderer.InitializeStandardComponentServicesAsync( } } + if (_renderer.NotFoundEventArgs != null) + { + _renderer.SetNotFoundWhenResponseNotStarted(); + } + if (!quiesceTask.IsCompleted) { // An incomplete QuiescenceTask indicates there may be streaming rendering updates. @@ -158,6 +163,10 @@ await _renderer.InitializeStandardComponentServicesAsync( if (!quiesceTask.IsCompletedSuccessfully) { await _renderer.SendStreamingUpdatesAsync(context, quiesceTask, bufferWriter); + if (_renderer.NotFoundEventArgs != null) + { + await _renderer.SetNotFoundWhenResponseHasStarted(); + } } else { @@ -171,6 +180,17 @@ await _renderer.InitializeStandardComponentServicesAsync( componentStateHtmlContent.WriteTo(bufferWriter, HtmlEncoder.Default); } + if (context.Response.StatusCode == StatusCodes.Status404NotFound && + !isReExecuted && + string.IsNullOrEmpty(_renderer.NotFoundEventArgs?.Path)) + { + // Router did not handle the NotFound event, otherwise this would not be empty. + // Don't flush the response if we have an unhandled 404 rendering + // This will allow the StatusCodePages middleware to re-execute the request + context.Response.ContentType = null; + return; + } + // Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying // response asynchronously. In the absence of this line, the buffer gets synchronously written to the // response as part of the Dispose which has a perf impact. diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index cdf17e376a00..a06d3e66d622 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -79,27 +79,28 @@ private Task ReturnErrorResponse(string detailedMessage) : Task.CompletedTask; } - internal async Task SetNotFoundResponseAsync(string baseUri, NotFoundEventArgs args) + internal void SetNotFoundWhenResponseNotStarted() { - if (_httpContext.Response.HasStarted || - // POST waits for quiescence -> rendering the NotFoundPage would be queued for the next batch - // but we want to send the signal to the renderer to stop rendering future batches -> use client rendering - HttpMethods.IsPost(_httpContext.Request.Method)) - { - if (string.IsNullOrEmpty(_notFoundUrl)) - { - _notFoundUrl = GetNotFoundUrl(baseUri, args); - } - var defaultBufferSize = 16 * 1024; - await using var writer = new HttpResponseStreamWriter(_httpContext.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool.Shared, ArrayPool.Shared); - using var bufferWriter = new BufferedTextWriter(writer); - HandleNotFoundAfterResponseStarted(bufferWriter, _httpContext, _notFoundUrl); - await bufferWriter.FlushAsync(); - } - else + _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; + + // When the application triggers a NotFound event, we continue rendering the current batch. + // However, after completing this batch, we do not want to process any further UI updates, + // as we are going to return a 404 status and discard the UI updates generated so far. + SignalRendererToFinishRendering(); + } + + internal async Task SetNotFoundWhenResponseHasStarted() + { + if (string.IsNullOrEmpty(_notFoundUrl)) { - _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; + var baseUri = $"{_httpContext.Request.Scheme}://{_httpContext.Request.Host}{_httpContext.Request.PathBase}/"; + _notFoundUrl = GetNotFoundUrl(baseUri, NotFoundEventArgs); } + var defaultBufferSize = 16 * 1024; + await using var writer = new HttpResponseStreamWriter(_httpContext.Response.Body, Encoding.UTF8, defaultBufferSize, ArrayPool.Shared, ArrayPool.Shared); + using var bufferWriter = new BufferedTextWriter(writer); + HandleNotFoundAfterResponseStarted(bufferWriter, _httpContext, _notFoundUrl); + await bufferWriter.FlushAsync(); // When the application triggers a NotFound event, we continue rendering the current batch. // However, after completing this batch, we do not want to process any further UI updates, @@ -107,15 +108,15 @@ internal async Task SetNotFoundResponseAsync(string baseUri, NotFoundEventArgs a SignalRendererToFinishRendering(); } - private string GetNotFoundUrl(string baseUri, NotFoundEventArgs args) + private string GetNotFoundUrl(string baseUri, NotFoundEventArgs? args) { - string path = args.Path; + string? path = args?.Path; if (string.IsNullOrEmpty(path)) { var pathFormat = _httpContext.Items[nameof(StatusCodePagesOptions)] as string; if (string.IsNullOrEmpty(pathFormat)) { - throw new InvalidOperationException("The NotFoundPage route must be specified or re-execution middleware has to be set to render NotFoundPage when the response has started."); + throw new InvalidOperationException($"The {nameof(Router.NotFoundPage)} route must be specified or re-execution middleware has to be set to render not found content."); } path = pathFormat; diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index f5d0699e1efe..5a7d38941b23 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -63,6 +63,7 @@ public EndpointHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory log } internal HttpContext? HttpContext => _httpContext; + internal NotFoundEventArgs? NotFoundEventArgs { get; private set; } internal void SetHttpContext(HttpContext httpContext) { @@ -85,10 +86,7 @@ internal async Task InitializeStandardComponentServicesAsync( var navigationManager = httpContext.RequestServices.GetRequiredService(); ((IHostEnvironmentNavigationManager)navigationManager)?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request), OnNavigateTo); - navigationManager?.OnNotFound += (sender, args) => - { - _ = GetErrorHandledTask(SetNotFoundResponseAsync(navigationManager.BaseUri, args)); - }; + navigationManager?.OnNotFound += (sender, args) => NotFoundEventArgs = args; var authenticationStateProvider = httpContext.RequestServices.GetService(); if (authenticationStateProvider is IHostEnvironmentAuthenticationStateProvider hostEnvironmentAuthenticationStateProvider) diff --git a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs index 3c27365169cb..7fa40b2cd010 100644 --- a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs +++ b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs @@ -944,9 +944,9 @@ public async Task Renderer_WhenNoNotFoundPathProvided_Throws() httpContext.Items[nameof(StatusCodePagesOptions)] = null; // simulate missing re-execution route var exception = await Assert.ThrowsAsync(async () => - await renderer.SetNotFoundResponseAsync(httpContext, new NotFoundEventArgs("")) + await renderer.SetNotFoundResponseAsync(httpContext, new NotFoundEventArgs()) ); - string expectedError = "The NotFoundPage route must be specified or re-execution middleware has to be set to render NotFoundPage when the response has started."; + string expectedError = $"The {nameof(Router.NotFoundPage)} route must be specified or re-execution middleware has to be set to render not found content."; Assert.Equal(expectedError, exception.Message); } @@ -1823,7 +1823,7 @@ protected override void ProcessPendingRender() public async Task SetNotFoundResponseAsync(HttpContext httpContext, NotFoundEventArgs args) { SetHttpContext(httpContext); - await SetNotFoundResponseAsync(httpContext.Request.PathBase, args); + await SetNotFoundWhenResponseHasStarted(); } } diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 3de7fdc739b6..1b3c340084a5 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -128,6 +128,16 @@ public void BrowserNavigationToNotExistingPath_ReExecutesTo404(bool streaming) private void AssertReExecutionPageRendered() => Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text); + private void AssertBrowserDefaultNotFoundViewRendered() + { + var mainMessage = Browser.FindElement(By.Id("main-message")); + + Browser.True( + () => mainMessage.FindElement(By.CssSelector("p")).Text + .Contains("No webpage was found for the web address:", StringComparison.OrdinalIgnoreCase) + ); + } + private void AssertNotFoundPageRendered() { Browser.Equal("Welcome On Custom Not Found Page", () => Browser.FindElement(By.Id("test-info")).Text); @@ -135,6 +145,15 @@ private void AssertNotFoundPageRendered() Browser.Equal("About", () => Browser.FindElement(By.Id("about-link")).Text); } + private void AssertNotFoundContentNotRendered(bool responseHasStarted) + { + if (!responseHasStarted) + { + AssertBrowserDefaultNotFoundViewRendered(); + } + // otherwise, the render view does not differ from the original page + } + private void AssertUrlNotChanged(string expectedUrl) => Browser.True(() => Browser.Url.Contains(expectedUrl), $"Expected URL to contain '{expectedUrl}', but found '{Browser.Url}'"); @@ -152,13 +171,61 @@ public void NotFoundSetOnInitialization_ResponseNotStarted_SSR(bool hasReExecuti string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr?useCustomNotFoundPage={hasCustomNotFoundPageSet}"; Navigate(testUrl); - if (hasCustomNotFoundPageSet) + bool notFoundContentForRenderingProvided = hasCustomNotFoundPageSet || hasReExecutionMiddleware; + if (notFoundContentForRenderingProvided) { - AssertNotFoundPageRendered(); + AssertNotFoundRendered(hasReExecutionMiddleware, hasCustomNotFoundPageSet); } else { - AssertNotFoundFragmentRendered(); + AssertNotFoundContentNotRendered(responseHasStarted: false); + } + AssertUrlNotChanged(testUrl); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void NotFoundSetOnInitialization_AfterAsyncOperation_ResponseNotStarted_SSR(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet) + { + string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; + string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr?doAsync=true&useCustomNotFoundPage={hasCustomNotFoundPageSet}"; + Navigate(testUrl); + + bool notFoundContentForRenderingProvided = hasCustomNotFoundPageSet || hasReExecutionMiddleware; + if (notFoundContentForRenderingProvided) + { + AssertNotFoundRendered(hasReExecutionMiddleware, hasCustomNotFoundPageSet); + } + else + { + AssertNotFoundContentNotRendered(responseHasStarted: false); + } + AssertUrlNotChanged(testUrl); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + // our custom router does not support NotFoundPage to simulate the most probable custom router behavior + public void NotFoundSetOnInitialization_ResponseNotStarted_CustomRouter_SSR(bool hasReExecutionMiddleware) + { + string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; + string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr?useCustomRouter=true"; + Navigate(testUrl); + + if (hasReExecutionMiddleware) + { + AssertReExecutionPageRendered(); + } + else + { + // Apps that don't support re-execution and don't have blazor's router, + // cannot render custom NotFound contents. + // The browser will display default 404 page. + AssertNotFoundContentNotRendered(responseHasStarted: false); } AssertUrlNotChanged(testUrl); } @@ -173,7 +240,15 @@ public void NotFoundSetOnInitialization_ResponseStarted_SSR(bool hasReExecutionM string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr-streaming?useCustomNotFoundPage={hasCustomNotFoundPageSet}"; Navigate(testUrl); - AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + bool notFoundContentForRenderingProvided = hasCustomNotFoundPageSet || hasReExecutionMiddleware; + if (notFoundContentForRenderingProvided) + { + AssertNotFoundRendered(hasReExecutionMiddleware, hasCustomNotFoundPageSet); + } + else + { + AssertNotFoundContentNotRendered(responseHasStarted: true); + } AssertUrlNotChanged(testUrl); } @@ -187,11 +262,35 @@ public void NotFoundSetOnInitialization_ResponseStarted_EnhancedNavigationDisabl string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr-streaming?useCustomNotFoundPage={hasCustomNotFoundPageSet}"; Navigate(testUrl); - AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + AssertNotFoundRendered(hasReExecutionMiddleware, hasCustomNotFoundPageSet); AssertUrlChanged(testUrl); } - private void AssertNotFoundRendered_ResponseStarted_Or_POST(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet, string testUrl) + [Theory] + [InlineData(true)] + [InlineData(false)] + // our custom router does not support NotFoundPage to simulate the most probable custom router behavior + public void NotFoundSetOnInitialization_ResponseStarted_CustomRouter_SSR(bool hasReExecutionMiddleware) + { + string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; + string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr-streaming?useCustomRouter=true"; + Navigate(testUrl); + + if (hasReExecutionMiddleware) + { + AssertReExecutionPageRendered(); + } + else + { + // Apps that don't support re-execution and don't have blazor's router, + // cannot render custom NotFound contents. + // The browser will display default 404 page. + AssertNotFoundContentNotRendered(responseHasStarted: true); + } + AssertUrlNotChanged(testUrl); + } + + private void AssertNotFoundRendered(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet) { if (hasCustomNotFoundPageSet) { @@ -203,8 +302,7 @@ private void AssertNotFoundRendered_ResponseStarted_Or_POST(bool hasReExecutionM } else { - // this throws an exception logged on the server - AssertNotFoundContentNotRendered(); + throw new InvalidOperationException("NotFound page will not be rendered without re-execution middleware or custom NotFoundPage set. Use AssertNotFoundNotRendered in this case."); } } @@ -216,11 +314,54 @@ private void AssertNotFoundRendered_ResponseStarted_Or_POST(bool hasReExecutionM public void NotFoundSetOnFormSubmit_ResponseNotStarted_SSR(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet) { string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; - string testUrl = $"{ServerPathBase}{reexecution}/post-not-found-ssr-streaming?useCustomNotFoundPage={hasCustomNotFoundPageSet}"; + string testUrl = $"{ServerPathBase}{reexecution}/post-not-found-ssr?useCustomNotFoundPage={hasCustomNotFoundPageSet}"; Navigate(testUrl); Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); - AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + bool notFoundContentForRenderingProvided = hasCustomNotFoundPageSet || hasReExecutionMiddleware; + if (notFoundContentForRenderingProvided) + { + AssertNotFoundRendered(hasReExecutionMiddleware, hasCustomNotFoundPageSet); + } + else + { + AssertNotFoundContentNotRendered(responseHasStarted: false); + } + AssertUrlNotChanged(testUrl); + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void NotFoundSetOnFormSubmit_AfterAsyncOperation_ResponseNotStarted_SSR(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet) + { + string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; + string testUrl = $"{ServerPathBase}{reexecution}/post-not-found-ssr?doAsync=true&useCustomNotFoundPage={hasCustomNotFoundPageSet}"; + Navigate(testUrl); + Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); + + bool notFoundContentForRenderingProvided = hasCustomNotFoundPageSet || hasReExecutionMiddleware; + if (notFoundContentForRenderingProvided) + { + AssertNotFoundRendered(hasReExecutionMiddleware, hasCustomNotFoundPageSet); + } + else + { + AssertNotFoundContentNotRendered(responseHasStarted: false); + } + AssertUrlNotChanged(testUrl); + } + + [Fact] + public void NotFoundSetOnFormSubmit_ResponseNotStarted_CustomRouter_SSR() + { + string testUrl = $"{ServerPathBase}/reexecution/post-not-found-ssr?useCustomRouter=true"; + Navigate(testUrl); + Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); + + AssertReExecutionPageRendered(); AssertUrlNotChanged(testUrl); } @@ -236,15 +377,28 @@ public void NotFoundSetOnFormSubmit_ResponseStarted_SSR(bool hasReExecutionMiddl Navigate(testUrl); Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); - AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + bool notFoundContentForRenderingProvided = hasCustomNotFoundPageSet || hasReExecutionMiddleware; + if (notFoundContentForRenderingProvided) + { + AssertNotFoundRendered(hasReExecutionMiddleware, hasCustomNotFoundPageSet); + } + else + { + AssertNotFoundContentNotRendered(responseHasStarted: true); + } AssertUrlNotChanged(testUrl); } - private void AssertNotFoundFragmentRendered() => - Browser.Equal("There's nothing here", () => Browser.FindElement(By.Id("not-found-fragment")).Text); + [Fact] + public void NotFoundSetOnFormSubmit_ResponseStarted_CustomRouter_SSR() + { + string testUrl = $"{ServerPathBase}/reexecution/post-not-found-ssr-streaming?useCustomRouter=true"; + Navigate(testUrl); + Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); - private void AssertNotFoundContentNotRendered() => - Browser.Equal("Any content", () => Browser.FindElement(By.Id("test-info")).Text); + AssertReExecutionPageRendered(); + AssertUrlNotChanged(testUrl); + } [Fact] public void StatusCodePagesWithReExecution() diff --git a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs index 7a1163de31c8..3e0a68780580 100644 --- a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs +++ b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs @@ -41,11 +41,13 @@ public void CanRenderNotFoundInteractive(string renderingMode, bool useCustomNot } else { - var bodyText = Browser.FindElement(By.TagName("body")).Text; - Assert.Contains("There's nothing here", bodyText); + AssertNotFoundContentNotRendered(); } } + private void AssertNotFoundContentNotRendered() => + Browser.Equal("Any content", () => Browser.FindElement(By.Id("test-info")).Text); + [Fact] public async Task CanSetNotFoundStatus() { @@ -170,14 +172,7 @@ public void CanRenderNotFoundIfNotFoundPageTypeNotProvided_SSR(bool streamingSta { string streamingPath = streamingStarted ? "-streaming" : ""; Navigate($"{ServerPathBase}/reexecution/set-not-found-ssr{streamingPath}"); - if (streamingStarted) - { - AssertReExecutedPageRendered(); - } - else - { - AssertNotFoundFragmentRendered(); - } + AssertReExecutedPageRendered(); } [Theory] @@ -185,8 +180,8 @@ public void CanRenderNotFoundIfNotFoundPageTypeNotProvided_SSR(bool streamingSta [InlineData("WebAssemblyNonPrerendered")] public void DoesNotReExecuteIf404WasHandled_Interactive(string renderMode) { - Navigate($"{ServerPathBase}/reexecution/set-not-found?renderMode={renderMode}"); - AssertNotFoundFragmentRendered(); + Navigate($"{ServerPathBase}/reexecution/set-not-found?useCustomNotFoundPage=true&renderMode={renderMode}"); + AssertNotFoundPageRendered(); } private void AssertNotFoundFragmentRendered() => diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index d2adde64c256..c8a92f2ba9d5 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -1,12 +1,17 @@ @using Components.TestServer.RazorComponents.Pages.Forms @using Components.WasmMinimal.Pages.NotFound @using TestContentPackage.NotFound +@using Components.TestServer.RazorComponents @code { [Parameter] [SupplyParameterFromQuery(Name = "useCustomNotFoundPage")] public string? UseCustomNotFoundPage { get; set; } + [Parameter] + [SupplyParameterFromQuery(Name = "useCustomRouter")] + public string? UseCustomRouter { get; set; } + private Type? NotFoundPageType { get; set; } protected override void OnParametersSet() @@ -30,24 +35,36 @@ - @if (NotFoundPageType != null) + @if(string.Equals(UseCustomRouter, "true", StringComparison.OrdinalIgnoreCase)) { - + - + } else { - - - - - -

There's nothing here

-
+ @if (NotFoundPageType is not null) + { + + + + + + + } + else + { + + + + + +

There's nothing here

+
+ } }