From 599f8462094661da909d535da81d9fb7fe90ea96 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 30 May 2025 14:45:33 +0200 Subject: [PATCH 01/28] Fix SSR non-streaming: rendering has higher priority than re-execution. --- .../Endpoints/src/RazorComponentEndpointInvoker.cs | 7 ------- .../src/Rendering/EndpointHtmlRenderer.EventDispatch.cs | 1 - 2 files changed, 8 deletions(-) diff --git a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs index 245f811d7f76..39aefaf164e2 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs @@ -102,13 +102,6 @@ await _renderer.InitializeStandardComponentServicesAsync( ParameterView.Empty, waitForQuiescence: result.IsPost || isErrorHandlerOrReExecuted); - bool avoidStartingResponse = hasStatusCodePage && !isReExecuted && context.Response.StatusCode == StatusCodes.Status404NotFound; - if (avoidStartingResponse) - { - // the request is going to be re-executed, we should avoid writing to the response - return; - } - Task quiesceTask; if (!result.IsPost) { diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 6af020fc847a..3ae2f6fcd36a 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -91,7 +91,6 @@ private async Task SetNotFoundResponseAsync(string baseUri) else { _httpContext.Response.StatusCode = StatusCodes.Status404NotFound; - _httpContext.Response.ContentType = null; } // When the application triggers a NotFound event, we continue rendering the current batch. From 842941efd3fd8f015109e997cefd0f313a3a66c9 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 30 May 2025 14:48:35 +0200 Subject: [PATCH 02/28] Add tests: move component common for global and local interactivity to shared project + redefine test cases. --- .../ServerRenderingTests/InteractivityTest.cs | 59 +++++++++++++++++++ .../NoInteractivityTest.cs | 54 ++++++++--------- .../StatusCodePagesTest.cs | 39 ------------ .../ComponentThatSetsNotFound.razor | 26 ++++++++ .../Components.Shared.csproj | 17 ++++++ .../NotFoundPage.razor | 0 .../PageThatSetsNotFound-no-streaming.razor | 11 ++++ .../PageThatSetsNotFound-streaming.razor | 11 ++++ .../ReexecutedPage.razor | 0 .../Components.Shared/_Imports.razor | 1 + .../Components.TestServer.csproj | 1 + ...omponentEndpointsNoInteractivityStartup.cs | 15 ++++- .../RazorComponentEndpointsStartup.cs | 9 +++ .../RazorComponents/App.razor | 2 +- .../Pages/NotFound/PageThatSetsNotFound.razor | 23 -------- .../StreamingSetNotFound.razor | 30 ---------- .../PageThatSetsNotFound-Interactive.razor | 28 +++++++++ .../Components.WasmMinimal/Routes.razor | 2 +- 18 files changed, 201 insertions(+), 127 deletions(-) delete mode 100644 src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs create mode 100644 src/Components/test/testassets/Components.Shared/ComponentThatSetsNotFound.razor create mode 100644 src/Components/test/testassets/Components.Shared/Components.Shared.csproj rename src/Components/test/testassets/{Components.TestServer/RazorComponents/Pages/NotFound => Components.Shared}/NotFoundPage.razor (100%) create mode 100644 src/Components/test/testassets/Components.Shared/PageThatSetsNotFound-no-streaming.razor create mode 100644 src/Components/test/testassets/Components.Shared/PageThatSetsNotFound-streaming.razor rename src/Components/test/testassets/{Components.TestServer/RazorComponents/Pages/NotFound => Components.Shared}/ReexecutedPage.razor (100%) create mode 100644 src/Components/test/testassets/Components.Shared/_Imports.razor delete mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/PageThatSetsNotFound.razor delete mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor create mode 100644 src/Components/test/testassets/Components.WasmMinimal/Pages/NotFound/PageThatSetsNotFound-Interactive.razor diff --git a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs index dd9f2a3a47e8..ecac592aac33 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs @@ -1448,4 +1448,63 @@ public void BrowserNavigationToNotExistingPathReExecutesTo404(string renderMode) private void Assert404ReExecuted() => Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text); + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CanRenderNotFoundPage_SSR(bool streamingStarted) + { + string streamingPath = streamingStarted ? "-streaming" : ""; + Navigate($"{ServerPathBase}/set-not-found-ssr{streamingPath}?useCustomNotFoundPage=true"); + AssertCustomNotFoundPageRendered() + } + + [Theory] + [InlineData("ServerNonPrerendered")] + [InlineData("WebAssemblyNonPrerendered")] + public void CanRenderNotFoundPage_Interactive(string renderMode) + { + string streamingPath = streamingStarted ? "-streaming" : ""; + Navigate($"{ServerPathBase}/set-not-found?useCustomNotFoundPage=true&renderMode={renderMode}"); + AssertCustomNotFoundPageRendered(); + } + + private void AssertCustomNotFoundPageRendered() + { + var infoText = Browser.FindElement(By.Id("test-info")).Text; + Assert.Contains("Welcome On Custom Not Found Page", infoText); + // custom page should have a custom layout + var aboutLink = Browser.FindElement(By.Id("about-link")).Text; + Assert.Contains("About", aboutLink); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DoesNotReExecuteIf404WasHandled_SSR(bool streamingStarted) + { + string streamingPath = streamingStarted ? "-streaming" : ""; + Navigate($"{ServerPathBase}/reexecution/set-not-found-ssr{streamingPath}"); + AssertNotFoundFragmentRendered(); + } + + [Theory] + [InlineData("ServerNonPrerendered")] + [InlineData("WebAssemblyNonPrerendered")] + public async Task DoesNotReExecuteIf404WasHandled_Interactive(string renderMode) + { + Navigate($"{ServerPathBase}/reexecution/set-not-found?renderMode={renderMode}"); + await Task.Delay(5000); + AssertNotFoundFragmentRendered(); + } + + private void AssertNotFoundFragmentRendered() => + Browser.Equal("There's nothing here", () => Browser.FindElement(By.CssSelector("body > p")).Text); + + [Fact] + public void StatusCodePagesWithReExecution() + { + Navigate($"{ServerPathBase}/reexecution/trigger-404"); + Assert404ReExecuted(); + } } diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 35f7140f9170..26399b8f556f 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -80,13 +80,6 @@ public void NavigatesWithoutInteractivityByRequestRedirection(bool controlFlowBy Browser.Equal("Routing test cases", () => Browser.Exists(By.Id("test-info")).Text); } - [Fact] - public void CanRenderNotFoundPageAfterStreamingStarted() - { - Navigate($"{ServerPathBase}/streaming-set-not-found"); - Browser.Equal("Default Not Found Page", () => Browser.Title); - } - [Theory] [InlineData(true)] [InlineData(false)] @@ -127,36 +120,35 @@ private void Assert404ReExecuted() => [Theory] [InlineData(true)] [InlineData(false)] - public void CanRenderNotFoundPageNoStreaming(bool useCustomNotFoundPage) + public void CanRenderNotFoundPage(bool streamingStarted) { - string query = useCustomNotFoundPage ? "&useCustomNotFoundPage=true" : ""; - Navigate($"{ServerPathBase}/set-not-found?shouldSet=true{query}"); - - if (useCustomNotFoundPage) - { - var infoText = Browser.FindElement(By.Id("test-info")).Text; - Assert.Contains("Welcome On Custom Not Found Page", infoText); - // custom page should have a custom layout - var aboutLink = Browser.FindElement(By.Id("about-link")).Text; - Assert.Contains("About", aboutLink); - } - else - { - var bodyText = Browser.FindElement(By.TagName("body")).Text; - Assert.Contains("There's nothing here", bodyText); - } + string streamingPath = streamingStarted ? "-streaming" : ""; + Navigate($"{ServerPathBase}/set-not-found-ssr{streamingPath}?useCustomNotFoundPage=true"); + + var infoText = Browser.FindElement(By.Id("test-info")).Text; + Assert.Contains("Welcome On Custom Not Found Page", infoText); + // custom page should have a custom layout + var aboutLink = Browser.FindElement(By.Id("about-link")).Text; + Assert.Contains("About", aboutLink); } [Theory] - [InlineData(true)] [InlineData(false)] - public void CanRenderNotFoundPageWithStreaming(bool useCustomNotFoundPage) + [InlineData(true)] + public void DoesNotReExecuteIf404WasHandled(bool streamingStarted) { - // when streaming started, we always render page under "not-found" path - string query = useCustomNotFoundPage ? "?useCustomNotFoundPage=true" : ""; - Navigate($"{ServerPathBase}/streaming-set-not-found{query}"); + string streamingPath = streamingStarted ? "-streaming" : ""; + Navigate($"{ServerPathBase}/reexecution/set-not-found-ssr{streamingPath}"); + AssertNotFoundFragmentRendered(); + } + + private void AssertNotFoundFragmentRendered() => + Browser.Equal("There's nothing here", () => Browser.FindElement(By.CssSelector("body > p")).Text); - string expectedTitle = "Default Not Found Page"; - Browser.Equal(expectedTitle, () => Browser.Title); + [Fact] + public void StatusCodePagesWithReExecution() + { + Navigate($"{ServerPathBase}/reexecution/trigger-404"); + Browser.Equal("Re-executed page", () => Browser.Title); } } diff --git a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs deleted file mode 100644 index 58ac90b39bbe..000000000000 --- a/src/Components/test/E2ETest/ServerRenderingTests/StatusCodePagesTest.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Components.TestServer.RazorComponents; -using Components.TestServer.RazorComponents.Pages.StreamingRendering; -using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; -using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; -using Microsoft.AspNetCore.E2ETesting; -using OpenQA.Selenium; -using TestServer; -using Xunit.Abstractions; - -namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests; - -public class StatusCodePagesTest(BrowserFixture browserFixture, BasicTestAppServerSiteFixture> serverFixture, ITestOutputHelper output) - : ServerTestBase>>(browserFixture, serverFixture, output) -{ - - [Theory] - [InlineData(false, false)] - [InlineData(true, false)] - [InlineData(true, true)] - public void StatusCodePagesWithReExecution(bool streaming, bool responseStarted) - { - string streamingPath = streaming ? "streaming-" : ""; - Navigate($"{ServerPathBase}/reexecution/{streamingPath}set-not-found?responseStarted={responseStarted}"); - - // streaming when response started does not re-execute - string expectedTitle = responseStarted - ? "Default Not Found Page" - : "Re-executed page"; - Browser.Equal(expectedTitle, () => Browser.Title); - } -} diff --git a/src/Components/test/testassets/Components.Shared/ComponentThatSetsNotFound.razor b/src/Components/test/testassets/Components.Shared/ComponentThatSetsNotFound.razor new file mode 100644 index 000000000000..fe024d3bb8d0 --- /dev/null +++ b/src/Components/test/testassets/Components.Shared/ComponentThatSetsNotFound.razor @@ -0,0 +1,26 @@ +@inject NavigationManager NavigationManager + +@if (!WaitForInteractivity || RendererInfo.IsInteractive) +{ + Original page + +

Any content

+ +} + +@code{ + [Parameter] + public bool StartStreaming { get; set; } = false; + + [Parameter] + public bool WaitForInteractivity { get; set; } = false; + + protected async override Task OnInitializedAsync() + { + if (StartStreaming) + { + await Task.Yield(); + } + NavigationManager.NotFound(); + } +} \ No newline at end of file diff --git a/src/Components/test/testassets/Components.Shared/Components.Shared.csproj b/src/Components/test/testassets/Components.Shared/Components.Shared.csproj new file mode 100644 index 000000000000..e2b71dba2459 --- /dev/null +++ b/src/Components/test/testassets/Components.Shared/Components.Shared.csproj @@ -0,0 +1,17 @@ + + + + $(DefaultNetCoreTargetFramework) + enable + enable + + + + + + + + + + + \ No newline at end of file diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/NotFoundPage.razor b/src/Components/test/testassets/Components.Shared/NotFoundPage.razor similarity index 100% rename from src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/NotFoundPage.razor rename to src/Components/test/testassets/Components.Shared/NotFoundPage.razor diff --git a/src/Components/test/testassets/Components.Shared/PageThatSetsNotFound-no-streaming.razor b/src/Components/test/testassets/Components.Shared/PageThatSetsNotFound-no-streaming.razor new file mode 100644 index 000000000000..6ac4db557e13 --- /dev/null +++ b/src/Components/test/testassets/Components.Shared/PageThatSetsNotFound-no-streaming.razor @@ -0,0 +1,11 @@ +@page "/reexecution/set-not-found-ssr" +@page "/set-not-found-ssr" +@attribute [StreamRendering(false)] + +@* + this page is used in global interactivity and no interactivity scenarios + the content is rendered on the server without streaming and might become + interactive later if interactivity was enabled in the app +*@ + + \ No newline at end of file diff --git a/src/Components/test/testassets/Components.Shared/PageThatSetsNotFound-streaming.razor b/src/Components/test/testassets/Components.Shared/PageThatSetsNotFound-streaming.razor new file mode 100644 index 000000000000..061d35386bc0 --- /dev/null +++ b/src/Components/test/testassets/Components.Shared/PageThatSetsNotFound-streaming.razor @@ -0,0 +1,11 @@ +@page "/reexecution/set-not-found-ssr-streaming" +@page "/set-not-found-ssr-streaming" +@attribute [StreamRendering(true)] + +@* + this page is used in global interactivity and no interactivity scenarios + the content is rendered on the server with streaming and might become + interactive later if interactivity was enabled in the app +*@ + + \ No newline at end of file diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/ReexecutedPage.razor b/src/Components/test/testassets/Components.Shared/ReexecutedPage.razor similarity index 100% rename from src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/ReexecutedPage.razor rename to src/Components/test/testassets/Components.Shared/ReexecutedPage.razor diff --git a/src/Components/test/testassets/Components.Shared/_Imports.razor b/src/Components/test/testassets/Components.Shared/_Imports.razor new file mode 100644 index 000000000000..2b9557ce5a53 --- /dev/null +++ b/src/Components/test/testassets/Components.Shared/_Imports.razor @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Components.Web \ No newline at end of file diff --git a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj index 1341319c942c..1e80ca221765 100644 --- a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj +++ b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj @@ -35,6 +35,7 @@ + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs index 4520d9202397..6bc67a18c6fc 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsNoInteractivityStartup.cs @@ -50,6 +50,15 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.Map("/reexecution", reexecutionApp => { + app.Map("/trigger-404", trigger404App => + { + trigger404App.Run(async context => + { + context.Response.StatusCode = 404; + await context.Response.WriteAsync("Triggered a 404 status code."); + }); + }); + if (!env.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); @@ -62,7 +71,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) reexecutionApp.UseAntiforgery(); reexecutionApp.UseEndpoints(endpoints => { - endpoints.MapRazorComponents(); + endpoints.MapRazorComponents() + .AddAdditionalAssemblies(Assembly.Load("Components.Shared")); }); }); @@ -83,7 +93,8 @@ private void ConfigureSubdirPipeline(IApplicationBuilder app, IWebHostEnvironmen app.UseAntiforgery(); app.UseEndpoints(endpoints => { - endpoints.MapRazorComponents(); + endpoints.MapRazorComponents() + .AddAdditionalAssemblies(Assembly.Load("Components.Shared")); }); } } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs index ea4f7f7ad220..0c01031da9a7 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs @@ -75,6 +75,14 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.Map("/reexecution", reexecutionApp => { + app.Map("/trigger-404", app => + { + app.Run(async context => + { + context.Response.StatusCode = 404; + await context.Response.WriteAsync("Triggered a 404 status code."); + }); + }); reexecutionApp.UseStatusCodePagesWithReExecute("/not-found-reexecute", createScopeForErrors: true); reexecutionApp.UseRouting(); @@ -125,6 +133,7 @@ private void ConfigureEndpoints(IApplicationBuilder app, IWebHostEnvironment env } _ = endpoints.MapRazorComponents() + .AddAdditionalAssemblies(Assembly.Load("Components.Shared")) .AddAdditionalAssemblies(Assembly.Load("Components.WasmMinimal")) .AddInteractiveServerRenderMode(options => { diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index ace8d627d941..a4e8ff2dff98 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -29,7 +29,7 @@ - + diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/PageThatSetsNotFound.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/PageThatSetsNotFound.razor deleted file mode 100644 index e397f81672a3..000000000000 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/NotFound/PageThatSetsNotFound.razor +++ /dev/null @@ -1,23 +0,0 @@ -@page "/reexecution/set-not-found" -@page "/set-not-found" -@attribute [StreamRendering(false)] -@inject NavigationManager NavigationManager - -Original page - -

Any content

- -@code{ - [Parameter] - [SupplyParameterFromQuery(Name = "shouldSet")] - public bool? ShouldSet { get; set; } - - protected override void OnInitialized() - { - bool shouldSet = ShouldSet ?? true; - if (shouldSet) - { - NavigationManager.NotFound(); - } - } -} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor deleted file mode 100644 index 92b5f95d7c4b..000000000000 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/StreamingRendering/StreamingSetNotFound.razor +++ /dev/null @@ -1,30 +0,0 @@ -@page "/reexecution/streaming-set-not-found" -@page "/streaming-set-not-found" -@attribute [StreamRendering] -@inject NavigationManager NavigationManager - -@code { - [Parameter] - [SupplyParameterFromQuery(Name = "shouldSet")] - public bool? ShouldSet { get; set; } - - [Parameter] - [SupplyParameterFromQuery(Name = "responseStarted")] - public bool? ResponseStarted { get; set; } - - protected override async Task OnInitializedAsync() - { - bool shouldSet = ShouldSet ?? true; - bool responseStarted = ResponseStarted ?? true; - if (responseStarted) - { - // Simulate some delay before triggering NotFound to start streaming response - await Task.Yield(); - } - - if (shouldSet) - { - NavigationManager.NotFound(); - } - } -} diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFound/PageThatSetsNotFound-Interactive.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFound/PageThatSetsNotFound-Interactive.razor new file mode 100644 index 000000000000..46c0c65cecf9 --- /dev/null +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/NotFound/PageThatSetsNotFound-Interactive.razor @@ -0,0 +1,28 @@ +@page "/reexecution/set-not-found" +@page "/set-not-found" + +@* + this page is used only in global interactivity scenarios + the component's content will be rendered when it becomes interactive +*@ + + + +@code{ + [Parameter, SupplyParameterFromQuery(Name = "renderMode")] + public string? RenderModeStr { get; set; } + + private RenderModeId _renderMode; + + protected override void OnInitialized() + { + if (!string.IsNullOrEmpty(RenderModeStr)) + { + _renderMode = RenderModeHelper.ParseRenderMode(RenderModeStr); + } + else + { + throw new ArgumentException("RenderModeStr cannot be null or empty. Did you mean to redirect to /set-not-found-ssr?", nameof(RenderModeStr)); + } + } +} \ No newline at end of file diff --git a/src/Components/test/testassets/Components.WasmMinimal/Routes.razor b/src/Components/test/testassets/Components.WasmMinimal/Routes.razor index 92f41cc8b4f1..ca687782bc22 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Routes.razor +++ b/src/Components/test/testassets/Components.WasmMinimal/Routes.razor @@ -22,7 +22,7 @@ } } - + From 079b66fcac631142bb70660900988fb57c19eb3f Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 30 May 2025 15:00:13 +0200 Subject: [PATCH 03/28] Missing build fixes for the previous commit. --- .../test/E2ETest/ServerRenderingTests/InteractivityTest.cs | 3 +-- .../Components.WasmMinimal/Components.WasmMinimal.csproj | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs index ecac592aac33..c5bc24012ae4 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs @@ -1456,7 +1456,7 @@ public void CanRenderNotFoundPage_SSR(bool streamingStarted) { string streamingPath = streamingStarted ? "-streaming" : ""; Navigate($"{ServerPathBase}/set-not-found-ssr{streamingPath}?useCustomNotFoundPage=true"); - AssertCustomNotFoundPageRendered() + AssertCustomNotFoundPageRendered(); } [Theory] @@ -1464,7 +1464,6 @@ public void CanRenderNotFoundPage_SSR(bool streamingStarted) [InlineData("WebAssemblyNonPrerendered")] public void CanRenderNotFoundPage_Interactive(string renderMode) { - string streamingPath = streamingStarted ? "-streaming" : ""; Navigate($"{ServerPathBase}/set-not-found?useCustomNotFoundPage=true&renderMode={renderMode}"); AssertCustomNotFoundPageRendered(); } diff --git a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj index deabcd7fccae..ecacf8c5078d 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj +++ b/src/Components/test/testassets/Components.WasmMinimal/Components.WasmMinimal.csproj @@ -16,6 +16,7 @@ + From 301efac35cdf105f6604c8064ab271c1062aee1d Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 30 May 2025 15:01:41 +0200 Subject: [PATCH 04/28] Enable `Router` to stream in the `NotFound` content. --- .../Components/src/Routing/Router.cs | 1 + .../EndpointHtmlRenderer.EventDispatch.cs | 19 +++++++------------ .../src/Rendering/EndpointHtmlRenderer.cs | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/Components/Components/src/Routing/Router.cs b/src/Components/Components/src/Routing/Router.cs index 51fb3fc823d2..02d22d5bb8c7 100644 --- a/src/Components/Components/src/Routing/Router.cs +++ b/src/Components/Components/src/Routing/Router.cs @@ -19,6 +19,7 @@ namespace Microsoft.AspNetCore.Components.Routing; /// /// A component that supplies route data corresponding to the current navigation state. /// +[StreamRendering] public partial class Router : IComponent, IHandleAfterRender, IDisposable { // Dictionary is intentionally used instead of ReadOnlyDictionary to reduce Blazor size diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 3ae2f6fcd36a..4f308c7984f7 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -77,26 +77,21 @@ private Task ReturnErrorResponse(string detailedMessage) : Task.CompletedTask; } - private async Task SetNotFoundResponseAsync(string baseUri) + private void SetNotFoundResponse(object? sender, EventArgs args) { if (_httpContext.Response.HasStarted) { - 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); - var notFoundUri = $"{baseUri}not-found"; - HandleNavigationAfterResponseStarted(bufferWriter, _httpContext, notFoundUri); - await bufferWriter.FlushAsync(); + // We're expecting the Router to continue streaming the NotFound contents } 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(); + // 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(); + } } private async Task OnNavigateTo(string uri) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index 33e52fa0ed57..cc491759cb20 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -85,7 +85,7 @@ internal async Task InitializeStandardComponentServicesAsync( if (navigationManager != null) { - navigationManager.OnNotFound += async (sender, args) => await SetNotFoundResponseAsync(navigationManager.BaseUri); + navigationManager.OnNotFound += SetNotFoundResponse; } var authenticationStateProvider = httpContext.RequestServices.GetService(); From e7ce5baca483bd1c44c0185c4475026378843e4b Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Fri, 30 May 2025 19:06:28 +0200 Subject: [PATCH 05/28] Remove debugging delay. --- .../test/E2ETest/ServerRenderingTests/InteractivityTest.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs index c5bc24012ae4..9536cbbbcee9 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs @@ -1490,10 +1490,9 @@ public void DoesNotReExecuteIf404WasHandled_SSR(bool streamingStarted) [Theory] [InlineData("ServerNonPrerendered")] [InlineData("WebAssemblyNonPrerendered")] - public async Task DoesNotReExecuteIf404WasHandled_Interactive(string renderMode) + public void DoesNotReExecuteIf404WasHandled_Interactive(string renderMode) { Navigate($"{ServerPathBase}/reexecution/set-not-found?renderMode={renderMode}"); - await Task.Delay(5000); AssertNotFoundFragmentRendered(); } From b166369da335f833f83be933e91cbab86232acd4 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 10 Jun 2025 12:40:18 +0200 Subject: [PATCH 06/28] Fix tests added with this PR. --- .../ServerRenderingTests/InteractivityTest.cs | 57 ----------------- .../NoInteractivityTest.cs | 2 +- .../E2ETest/Tests/GlobalInteractivityTest.cs | 63 +++++++++++++++++++ .../RazorComponents/App.razor | 2 +- .../Components.WasmMinimal/Routes.razor | 2 +- 5 files changed, 66 insertions(+), 60 deletions(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs index 9536cbbbcee9..dd9f2a3a47e8 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs @@ -1448,61 +1448,4 @@ public void BrowserNavigationToNotExistingPathReExecutesTo404(string renderMode) private void Assert404ReExecuted() => Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text); - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void CanRenderNotFoundPage_SSR(bool streamingStarted) - { - string streamingPath = streamingStarted ? "-streaming" : ""; - Navigate($"{ServerPathBase}/set-not-found-ssr{streamingPath}?useCustomNotFoundPage=true"); - AssertCustomNotFoundPageRendered(); - } - - [Theory] - [InlineData("ServerNonPrerendered")] - [InlineData("WebAssemblyNonPrerendered")] - public void CanRenderNotFoundPage_Interactive(string renderMode) - { - Navigate($"{ServerPathBase}/set-not-found?useCustomNotFoundPage=true&renderMode={renderMode}"); - AssertCustomNotFoundPageRendered(); - } - - private void AssertCustomNotFoundPageRendered() - { - var infoText = Browser.FindElement(By.Id("test-info")).Text; - Assert.Contains("Welcome On Custom Not Found Page", infoText); - // custom page should have a custom layout - var aboutLink = Browser.FindElement(By.Id("about-link")).Text; - Assert.Contains("About", aboutLink); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void DoesNotReExecuteIf404WasHandled_SSR(bool streamingStarted) - { - string streamingPath = streamingStarted ? "-streaming" : ""; - Navigate($"{ServerPathBase}/reexecution/set-not-found-ssr{streamingPath}"); - AssertNotFoundFragmentRendered(); - } - - [Theory] - [InlineData("ServerNonPrerendered")] - [InlineData("WebAssemblyNonPrerendered")] - public void DoesNotReExecuteIf404WasHandled_Interactive(string renderMode) - { - Navigate($"{ServerPathBase}/reexecution/set-not-found?renderMode={renderMode}"); - AssertNotFoundFragmentRendered(); - } - - private void AssertNotFoundFragmentRendered() => - Browser.Equal("There's nothing here", () => Browser.FindElement(By.CssSelector("body > p")).Text); - - [Fact] - public void StatusCodePagesWithReExecution() - { - Navigate($"{ServerPathBase}/reexecution/trigger-404"); - Assert404ReExecuted(); - } } diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 26399b8f556f..536eab21885a 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -143,7 +143,7 @@ public void DoesNotReExecuteIf404WasHandled(bool streamingStarted) } private void AssertNotFoundFragmentRendered() => - Browser.Equal("There's nothing here", () => Browser.FindElement(By.CssSelector("body > p")).Text); + Browser.Equal("There's nothing here", () => Browser.FindElement(By.Id("not-found-fragment")).Text); [Fact] public void StatusCodePagesWithReExecution() diff --git a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs index 0595baa95bc0..2bc20d31dee6 100644 --- a/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs +++ b/src/Components/test/E2ETest/Tests/GlobalInteractivityTest.cs @@ -140,4 +140,67 @@ public void CanNavigateBetweenStaticPagesViaEnhancedNav() Browser.Equal("Global interactivity page: Static via attribute", () => h1.Text); Browser.Equal("static", () => Browser.Exists(By.Id("execution-mode")).Text); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CanRenderNotFoundPage_SSR(bool streamingStarted) + { + string streamingPath = streamingStarted ? "-streaming" : ""; + Navigate($"{ServerPathBase}/set-not-found-ssr{streamingPath}?useCustomNotFoundPage=true"); + AssertCustomNotFoundPageRendered(); + } + + [Theory] + [InlineData("ServerNonPrerendered")] + [InlineData("WebAssemblyNonPrerendered")] + public void CanRenderNotFoundPage_Interactive(string renderMode) + { + Navigate($"{ServerPathBase}/set-not-found?useCustomNotFoundPage=true&renderMode={renderMode}"); + AssertCustomNotFoundPageRendered(); + } + + private void AssertCustomNotFoundPageRendered() + { + var infoText = Browser.FindElement(By.Id("test-info")).Text; + Assert.Contains("Welcome On Custom Not Found Page", infoText); + // custom page should have a custom layout + var aboutLink = Browser.FindElement(By.Id("about-link")).Text; + Assert.Contains("About", aboutLink); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void DoesNotReExecuteIf404WasHandled_SSR(bool streamingStarted) + { + string streamingPath = streamingStarted ? "-streaming" : ""; + Navigate($"{ServerPathBase}/reexecution/set-not-found-ssr{streamingPath}"); + AssertNotFoundFragmentRendered(); + } + + [Theory] + [InlineData("ServerNonPrerendered")] + [InlineData("WebAssemblyNonPrerendered")] + public void DoesNotReExecuteIf404WasHandled_Interactive(string renderMode) + { + Navigate($"{ServerPathBase}/reexecution/set-not-found?renderMode={renderMode}"); + AssertNotFoundFragmentRendered(); + } + + private void AssertNotFoundFragmentRendered() + { + var body = Browser.FindElement(By.TagName("body")); + var notFound = Browser.FindElement(By.Id("not-found-fragment")).Text; + Browser.Equal("There's nothing here", () => Browser.FindElement(By.Id("not-found-fragment")).Text); + } + + [Fact] + public void StatusCodePagesWithReExecution() + { + Navigate($"{ServerPathBase}/reexecution/trigger-404"); + Assert404ReExecuted(); + } + private void Assert404ReExecuted() => + Browser.Equal("Welcome On Page Re-executed After Not Found Event", () => Browser.Exists(By.Id("test-info")).Text); } diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor index a4e8ff2dff98..9912b1055e79 100644 --- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor @@ -34,7 +34,7 @@ - There's nothing here +

There's nothing here