From bc5a8a7c8b6d72fc2d6eb9015c22001c10950708 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 1 Jul 2025 10:53:00 +0200 Subject: [PATCH 01/12] Add no interactivity + simplify namespace. --- .../test/testassets/Components.TestServer/Program.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Components/test/testassets/Components.TestServer/Program.cs b/src/Components/test/testassets/Components.TestServer/Program.cs index 800ad27ddce4..ed7c7fe5d4cc 100644 --- a/src/Components/test/testassets/Components.TestServer/Program.cs +++ b/src/Components/test/testassets/Components.TestServer/Program.cs @@ -23,7 +23,8 @@ public static async Task Main(string[] args) ["Server authentication"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"), ["CORS (WASM)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"), ["Prerendering (Server-side)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/prerendered"), - ["Razor Component Endpoints"] = (BuildWebHost>(CreateAdditionalArgs(args)), "/subdir"), + ["Razor Component Endpoints"] = (BuildWebHost>(CreateAdditionalArgs(args)), "/subdir"), + ["No interactivity"] = (BuildWebHost>(CreateAdditionalArgs(args)), "/subdir"), ["Deferred component content (Server-side)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/deferred-component-content"), ["Locked navigation (Server-side)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/locked-navigation"), ["Client-side with fallback"] = (BuildWebHost(CreateAdditionalArgs(args)), "/fallback"), From 410ab1106c7fbad0eca5ff2518cfa7cc9bd6b76b Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 1 Jul 2025 14:49:57 +0200 Subject: [PATCH 02/12] Add tests + fix copy-paste error in the POST test. --- .../NoInteractivityTest.cs | 40 ++++++++++++++++++- .../NotFound/ComponentThatPostsNotFound.razor | 8 ++-- .../NotFound/ComponentThatSetsNotFound.razor | 8 ++-- .../PageThatPostsNotFound-no-streaming.razor | 7 +++- .../PageThatPostsNotFound-streaming.razor | 2 +- .../PageThatSetsNotFound-no-streaming.razor | 7 +++- .../PageThatSetsNotFound-streaming.razor | 2 +- 7 files changed, 61 insertions(+), 13 deletions(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index bbeb1ad4d9bb..442bc7120b1a 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -152,6 +152,28 @@ public void NotFoundSetOnInitialization_ResponseNotStarted_SSR(bool hasReExecuti 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); + + if (hasCustomNotFoundPageSet) + { + AssertNotFoundPageRendered(); + } + else + { + AssertNotFoundFragmentRendered(); + } + AssertUrlNotChanged(testUrl); + } + [Theory] [InlineData(true, true)] [InlineData(true, false)] @@ -205,7 +227,23 @@ 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); + 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(); diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatPostsNotFound.razor b/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatPostsNotFound.razor index f9b6cd9c7a3c..1a878d861de5 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatPostsNotFound.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatPostsNotFound.razor @@ -1,4 +1,4 @@ -@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Forms @inject NavigationManager NavigationManager @@ -16,17 +16,17 @@ @code{ [Parameter] - public bool StartStreaming { get; set; } = false; + public bool DoAsyncOperationBeforeSettingNotFound { get; set; } = false; [Parameter] public bool WaitForInteractivity { get; set; } = false; private async Task HandleSubmit() { - if (StartStreaming) + if (DoAsyncOperationBeforeSettingNotFound) { await Task.Yield(); } NavigationManager.NotFound(); } -} \ No newline at end of file +} diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatSetsNotFound.razor b/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatSetsNotFound.razor index fe024d3bb8d0..26ffd71e3e64 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatSetsNotFound.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/ComponentThatSetsNotFound.razor @@ -1,4 +1,4 @@ -@inject NavigationManager NavigationManager +@inject NavigationManager NavigationManager @if (!WaitForInteractivity || RendererInfo.IsInteractive) { @@ -10,17 +10,17 @@ @code{ [Parameter] - public bool StartStreaming { get; set; } = false; + public bool DoAsyncOperationBeforeSettingNotFound { get; set; } = false; [Parameter] public bool WaitForInteractivity { get; set; } = false; protected async override Task OnInitializedAsync() { - if (StartStreaming) + if (DoAsyncOperationBeforeSettingNotFound) { await Task.Yield(); } NavigationManager.NotFound(); } -} \ No newline at end of file +} diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-no-streaming.razor b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-no-streaming.razor index 4aa3a245b047..0a48cfb42c29 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-no-streaming.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-no-streaming.razor @@ -8,4 +8,9 @@ interactive later if interactivity was enabled in the app *@ - \ No newline at end of file + + +@code{ + [SupplyParameterFromQuery(Name = "doAsync")] + public bool DoAsync { get; set; } = false; +} diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-streaming.razor b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-streaming.razor index a542c35c52f4..834c977d943b 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-streaming.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatPostsNotFound-streaming.razor @@ -8,4 +8,4 @@ interactive later if interactivity was enabled in the app *@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-no-streaming.razor b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-no-streaming.razor index b99ed19711d7..e72b821a2b18 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-no-streaming.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-no-streaming.razor @@ -8,4 +8,9 @@ interactive later if interactivity was enabled in the app *@ - \ No newline at end of file + + +@code{ + [SupplyParameterFromQuery(Name = "doAsync")] + public bool DoAsync { get; set; } = false; +} diff --git a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-streaming.razor b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-streaming.razor index e3124758ce65..7574e5920396 100644 --- a/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-streaming.razor +++ b/src/Components/test/testassets/TestContentPackage/NotFound/PageThatSetsNotFound-streaming.razor @@ -8,4 +8,4 @@ interactive later if interactivity was enabled in the app *@ - \ No newline at end of file + \ No newline at end of file From 90f9f352f860aaa6c0bc5ed09cd443a2a528990d Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 1 Jul 2025 14:53:54 +0200 Subject: [PATCH 03/12] Fix POST no response started case: Move enhanced nav initialization earlierst possible + make sure navigation enhancement callbacks are available by using a promise. --- src/Components/Web.JS/src/Boot.Web.ts | 6 +++--- .../src/Services/NavigationEnhancement.ts | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/Components/Web.JS/src/Boot.Web.ts b/src/Components/Web.JS/src/Boot.Web.ts index df605ceebc52..48b9a51b7e40 100644 --- a/src/Components/Web.JS/src/Boot.Web.ts +++ b/src/Components/Web.JS/src/Boot.Web.ts @@ -65,13 +65,13 @@ function boot(options?: Partial) : Promise { }, }; - attachComponentDescriptorHandler(rootComponentManager); - attachStreamingRenderingListener(options?.ssr, navigationEnhancementCallbacks); - if (!options?.ssr?.disableDomPreservation) { attachProgressivelyEnhancedNavigationListener(navigationEnhancementCallbacks); } + attachComponentDescriptorHandler(rootComponentManager); + attachStreamingRenderingListener(options?.ssr, navigationEnhancementCallbacks); + enableFocusOnNavigate(jsEventRegistry); // Wait until the initial page response completes before activating interactive components. diff --git a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts index e029c77178dd..4ffd97561136 100644 --- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts +++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts @@ -37,11 +37,18 @@ const acceptHeader = 'text/html; blazor-enhanced-nav=on'; let currentEnhancedNavigationAbortController: AbortController | null; let navigationEnhancementCallbacks: NavigationEnhancementCallbacks; let performingEnhancedPageLoad: boolean; +let navigationEnhancementCallbacksPromise: Promise | null = null; +let navigationEnhancementCallbacksResolver: ((callbacks: NavigationEnhancementCallbacks) => void) | null = null; // This gets initialized to the current URL when we load. // After that, it gets updated every time we successfully complete a navigation. let currentContentUrl = location.href; +// Initialize the promise for waiting for navigation enhancement callbacks +navigationEnhancementCallbacksPromise = new Promise((resolve) => { + navigationEnhancementCallbacksResolver = resolve; +}); + export interface NavigationEnhancementCallbacks { enhancedNavigationStarted: () => void; documentUpdated: () => void; @@ -58,6 +65,14 @@ export function hasNeverStartedAnyEnhancedPageLoad() { export function attachProgressivelyEnhancedNavigationListener(callbacks: NavigationEnhancementCallbacks) { navigationEnhancementCallbacks = callbacks; + + // Resolve the promise so any waiting performEnhancedPageLoad calls can proceed + if (navigationEnhancementCallbacksResolver) { + navigationEnhancementCallbacksResolver(callbacks); + navigationEnhancementCallbacksResolver = null; + navigationEnhancementCallbacksPromise = null; + } + document.addEventListener('click', onDocumentClick); document.addEventListener('submit', onDocumentSubmit); window.addEventListener('popstate', onPopState); @@ -195,6 +210,10 @@ function onDocumentSubmit(event: SubmitEvent) { export async function performEnhancedPageLoad(internalDestinationHref: string, interceptedLink: boolean, fetchOptions?: RequestInit, treatAsRedirectionFromMethod?: 'get' | 'post', changeUrl: boolean = true) { performingEnhancedPageLoad = true; + if (!navigationEnhancementCallbacks && navigationEnhancementCallbacksPromise) { + navigationEnhancementCallbacks = await navigationEnhancementCallbacksPromise; + } + // First, stop any preceding enhanced page load currentEnhancedNavigationAbortController?.abort(); From a3ea9a6addfd8a46a8d5f4dd1121d8061318c099 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 1 Jul 2025 17:00:18 +0200 Subject: [PATCH 04/12] Stop the renderer only when we reached the quiescence. --- .../EndpointHtmlRenderer.EventDispatch.cs | 2 +- .../EndpointHtmlRenderer.Prerendering.cs | 26 +++++++++++++++++++ .../src/Rendering/EndpointHtmlRenderer.cs | 13 ++++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index cdf17e376a00..02d48a6d3f09 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -104,7 +104,7 @@ internal async Task SetNotFoundResponseAsync(string baseUri, NotFoundEventArgs a // 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(); + RequestRendererToFinishRendering(); } private string GetNotFoundUrl(string baseUri, NotFoundEventArgs args) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index 16c44c92f641..5e8cf8cc6fce 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -149,6 +149,8 @@ internal async ValueTask RenderEndpointComponen var component = BeginRenderingComponent(rootComponentType, parameters); var result = new PrerenderedComponentHtmlContent(Dispatcher, component); + FinishRendereingOnQuiescenceIfRequested(result); + await WaitForResultReady(waitForQuiescence, result); return result; @@ -159,6 +161,30 @@ internal async ValueTask RenderEndpointComponen } } + private void FinishRendereingOnQuiescenceIfRequested(PrerenderedComponentHtmlContent htmlContent) + { + if (htmlContent.QuiescenceTask.IsCompleted) + { + SignalRendererToFinishRendering(); + return; + } + + // Set up a background task to wait for quiescence and signal when reached + _ = Task.Run(async () => + { + try + { + await htmlContent.QuiescenceTask.ConfigureAwait(false); + SignalRendererToFinishRendering(); + } + catch + { + // Ignore exceptions - they represent component failures, not successful quiescence + // We only want to signal on successful completion + } + }); + } + private async Task WaitForResultReady(bool waitForQuiescence, PrerenderedComponentHtmlContent result) { if (waitForQuiescence) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs index f5d0699e1efe..abcbbd47a138 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs @@ -44,6 +44,7 @@ internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrer private HttpContext _httpContext = default!; // Always set at the start of an inbound call private ResourceAssetCollection? _resourceCollection; private bool _rendererIsStopped; + private bool _rendererStopRequested; private readonly ILogger _logger; // The underlying Renderer always tracks the pending tasks representing *full* quiescence, i.e., @@ -185,11 +186,19 @@ protected override void AddPendingTask(ComponentState? componentState, Task task base.AddPendingTask(componentState, task); } + internal void RequestRendererToFinishRendering() + { + // requests a deferred stop of the renderer, which will have an effect after the current batch is completed + _rendererStopRequested = true; + } + // For testing purposes only internal void SignalRendererToFinishRendering() { - // sets a deferred stop on the renderer, which will have an effect after the current batch is completed - _rendererIsStopped = true; + if (_rendererStopRequested) + { + _rendererIsStopped = true; + } } protected override void ProcessPendingRender() From 82ea8cc082fa020b8ebd6a51514148063308c69a Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 1 Jul 2025 17:21:16 +0200 Subject: [PATCH 05/12] POST got fixed by waiting with renderer stop for quiescence. --- .../EndpointHtmlRenderer.EventDispatch.cs | 5 +-- .../NoInteractivityTest.cs | 44 +++++++++---------- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs index 02d48a6d3f09..1b1976463464 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.EventDispatch.cs @@ -81,10 +81,7 @@ private Task ReturnErrorResponse(string detailedMessage) internal async Task SetNotFoundResponseAsync(string baseUri, NotFoundEventArgs args) { - 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 (_httpContext.Response.HasStarted) { if (string.IsNullOrEmpty(_notFoundUrl)) { diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 442bc7120b1a..6ce48dd5ee78 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -141,14 +141,7 @@ public void NotFoundSetOnInitialization_ResponseNotStarted_SSR(bool hasReExecuti string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr?useCustomNotFoundPage={hasCustomNotFoundPageSet}"; Navigate(testUrl); - if (hasCustomNotFoundPageSet) - { - AssertNotFoundPageRendered(); - } - else - { - AssertNotFoundFragmentRendered(); - } + AssertNotFoundRendered_ResponseNotStarted(hasCustomNotFoundPageSet); AssertUrlNotChanged(testUrl); } @@ -163,14 +156,7 @@ public void NotFoundSetOnInitialization_AfterAsyncOperation_ResponseNotStarted_S string testUrl = $"{ServerPathBase}{reexecution}/set-not-found-ssr?doAsync=true&useCustomNotFoundPage={hasCustomNotFoundPageSet}"; Navigate(testUrl); - if (hasCustomNotFoundPageSet) - { - AssertNotFoundPageRendered(); - } - else - { - AssertNotFoundFragmentRendered(); - } + AssertNotFoundRendered_ResponseNotStarted(hasCustomNotFoundPageSet); AssertUrlNotChanged(testUrl); } @@ -184,7 +170,8 @@ 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); + + AssertNotFoundRendered_ResponseStarted(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); AssertUrlNotChanged(testUrl); } @@ -198,11 +185,12 @@ 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_ResponseStarted(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); AssertUrlChanged(testUrl); } - private void AssertNotFoundRendered_ResponseStarted_Or_POST(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet, string testUrl) + private void AssertNotFoundRendered_ResponseStarted(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet, string testUrl) { if (hasCustomNotFoundPageSet) { @@ -219,6 +207,18 @@ private void AssertNotFoundRendered_ResponseStarted_Or_POST(bool hasReExecutionM } } + private void AssertNotFoundRendered_ResponseNotStarted(bool hasCustomNotFoundPageSet) + { + if (hasCustomNotFoundPageSet) + { + AssertNotFoundPageRendered(); + } + else + { + AssertNotFoundFragmentRendered(); + } + } + [Theory] [InlineData(true, true)] [InlineData(true, false)] @@ -231,7 +231,7 @@ public void NotFoundSetOnFormSubmit_ResponseNotStarted_SSR(bool hasReExecutionMi Navigate(testUrl); Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); - AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + AssertNotFoundRendered_ResponseNotStarted(hasCustomNotFoundPageSet); AssertUrlNotChanged(testUrl); } @@ -247,7 +247,7 @@ public void NotFoundSetOnFormSubmit_AfterAsyncOperation_ResponseNotStarted_SSR(b Navigate(testUrl); Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); - AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + AssertNotFoundRendered_ResponseNotStarted(hasCustomNotFoundPageSet); AssertUrlNotChanged(testUrl); } @@ -263,7 +263,7 @@ 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); + AssertNotFoundRendered_ResponseStarted(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); AssertUrlNotChanged(testUrl); } From 137422037c90420874ac2243a984506f4ab6d33b Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 1 Jul 2025 17:24:05 +0200 Subject: [PATCH 06/12] Revert changes to Program.cs --- .../test/testassets/Components.TestServer/Program.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Components/test/testassets/Components.TestServer/Program.cs b/src/Components/test/testassets/Components.TestServer/Program.cs index ed7c7fe5d4cc..ad1c9e7e37e6 100644 --- a/src/Components/test/testassets/Components.TestServer/Program.cs +++ b/src/Components/test/testassets/Components.TestServer/Program.cs @@ -23,8 +23,7 @@ public static async Task Main(string[] args) ["Server authentication"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"), ["CORS (WASM)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/subdir"), ["Prerendering (Server-side)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/prerendered"), - ["Razor Component Endpoints"] = (BuildWebHost>(CreateAdditionalArgs(args)), "/subdir"), - ["No interactivity"] = (BuildWebHost>(CreateAdditionalArgs(args)), "/subdir"), + ["Razor Component Endpoints"] = (BuildWebHost>(CreateAdditionalArgs(args)), "/subdir"), ["Deferred component content (Server-side)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/deferred-component-content"), ["Locked navigation (Server-side)"] = (BuildWebHost(CreateAdditionalArgs(args)), "/locked-navigation"), ["Client-side with fallback"] = (BuildWebHost(CreateAdditionalArgs(args)), "/fallback"), @@ -112,4 +111,4 @@ private static int GetNextChildAppPortNumber() return 0; } } -} +} \ No newline at end of file From e78c6bbd1f2ac0db12365fd9f98573a64853d7ee Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 1 Jul 2025 17:52:00 +0200 Subject: [PATCH 07/12] Remove try-catch, if QuiescenceTask throws, we don't want to wait anymore. --- .../Rendering/EndpointHtmlRenderer.Prerendering.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index 5e8cf8cc6fce..00e9a7d5df79 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -172,16 +172,8 @@ private void FinishRendereingOnQuiescenceIfRequested(PrerenderedComponentHtmlCon // Set up a background task to wait for quiescence and signal when reached _ = Task.Run(async () => { - try - { - await htmlContent.QuiescenceTask.ConfigureAwait(false); - SignalRendererToFinishRendering(); - } - catch - { - // Ignore exceptions - they represent component failures, not successful quiescence - // We only want to signal on successful completion - } + await htmlContent.QuiescenceTask.ConfigureAwait(false); + SignalRendererToFinishRendering(); }); } From c7a3f49382b4135b80e154fa6bcd27698c8fa960 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Tue, 1 Jul 2025 17:58:04 +0200 Subject: [PATCH 08/12] Whitespace. --- src/Components/test/testassets/Components.TestServer/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/test/testassets/Components.TestServer/Program.cs b/src/Components/test/testassets/Components.TestServer/Program.cs index ad1c9e7e37e6..800ad27ddce4 100644 --- a/src/Components/test/testassets/Components.TestServer/Program.cs +++ b/src/Components/test/testassets/Components.TestServer/Program.cs @@ -111,4 +111,4 @@ private static int GetNextChildAppPortNumber() return 0; } } -} \ No newline at end of file +} From 0b761240da7f2e919af325f6df9d5d8bf3a54149 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 2 Jul 2025 15:42:03 +0200 Subject: [PATCH 09/12] Avoid new thread pool - simplify waiting for end of QuiescenceTask. --- .../src/Rendering/EndpointHtmlRenderer.Prerendering.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index 00e9a7d5df79..39a107e2588f 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -169,12 +169,9 @@ private void FinishRendereingOnQuiescenceIfRequested(PrerenderedComponentHtmlCon return; } - // Set up a background task to wait for quiescence and signal when reached - _ = Task.Run(async () => - { - await htmlContent.QuiescenceTask.ConfigureAwait(false); - SignalRendererToFinishRendering(); - }); + htmlContent.QuiescenceTask.ContinueWith( + _ => SignalRendererToFinishRendering(), + TaskScheduler.Default); } private async Task WaitForResultReady(bool waitForQuiescence, PrerenderedComponentHtmlContent result) From 314941f3ab1ae336cc74575f6062984ef39c4c5c Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 2 Jul 2025 15:44:05 +0200 Subject: [PATCH 10/12] Remove changes delegated to a separate PR. --- src/Components/Web.JS/src/Boot.Web.ts | 6 +++--- .../src/Services/NavigationEnhancement.ts | 19 ------------------- .../NoInteractivityTest.cs | 2 +- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/Components/Web.JS/src/Boot.Web.ts b/src/Components/Web.JS/src/Boot.Web.ts index 48b9a51b7e40..df605ceebc52 100644 --- a/src/Components/Web.JS/src/Boot.Web.ts +++ b/src/Components/Web.JS/src/Boot.Web.ts @@ -65,13 +65,13 @@ function boot(options?: Partial) : Promise { }, }; + attachComponentDescriptorHandler(rootComponentManager); + attachStreamingRenderingListener(options?.ssr, navigationEnhancementCallbacks); + if (!options?.ssr?.disableDomPreservation) { attachProgressivelyEnhancedNavigationListener(navigationEnhancementCallbacks); } - attachComponentDescriptorHandler(rootComponentManager); - attachStreamingRenderingListener(options?.ssr, navigationEnhancementCallbacks); - enableFocusOnNavigate(jsEventRegistry); // Wait until the initial page response completes before activating interactive components. diff --git a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts index 4ffd97561136..e029c77178dd 100644 --- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts +++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts @@ -37,18 +37,11 @@ const acceptHeader = 'text/html; blazor-enhanced-nav=on'; let currentEnhancedNavigationAbortController: AbortController | null; let navigationEnhancementCallbacks: NavigationEnhancementCallbacks; let performingEnhancedPageLoad: boolean; -let navigationEnhancementCallbacksPromise: Promise | null = null; -let navigationEnhancementCallbacksResolver: ((callbacks: NavigationEnhancementCallbacks) => void) | null = null; // This gets initialized to the current URL when we load. // After that, it gets updated every time we successfully complete a navigation. let currentContentUrl = location.href; -// Initialize the promise for waiting for navigation enhancement callbacks -navigationEnhancementCallbacksPromise = new Promise((resolve) => { - navigationEnhancementCallbacksResolver = resolve; -}); - export interface NavigationEnhancementCallbacks { enhancedNavigationStarted: () => void; documentUpdated: () => void; @@ -65,14 +58,6 @@ export function hasNeverStartedAnyEnhancedPageLoad() { export function attachProgressivelyEnhancedNavigationListener(callbacks: NavigationEnhancementCallbacks) { navigationEnhancementCallbacks = callbacks; - - // Resolve the promise so any waiting performEnhancedPageLoad calls can proceed - if (navigationEnhancementCallbacksResolver) { - navigationEnhancementCallbacksResolver(callbacks); - navigationEnhancementCallbacksResolver = null; - navigationEnhancementCallbacksPromise = null; - } - document.addEventListener('click', onDocumentClick); document.addEventListener('submit', onDocumentSubmit); window.addEventListener('popstate', onPopState); @@ -210,10 +195,6 @@ function onDocumentSubmit(event: SubmitEvent) { export async function performEnhancedPageLoad(internalDestinationHref: string, interceptedLink: boolean, fetchOptions?: RequestInit, treatAsRedirectionFromMethod?: 'get' | 'post', changeUrl: boolean = true) { performingEnhancedPageLoad = true; - if (!navigationEnhancementCallbacks && navigationEnhancementCallbacksPromise) { - navigationEnhancementCallbacks = await navigationEnhancementCallbacksPromise; - } - // First, stop any preceding enhanced page load currentEnhancedNavigationAbortController?.abort(); diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 6ce48dd5ee78..da6187e63993 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -227,7 +227,7 @@ private void AssertNotFoundRendered_ResponseNotStarted(bool hasCustomNotFoundPag public void NotFoundSetOnFormSubmit_ResponseNotStarted_SSR(bool hasReExecutionMiddleware, bool hasCustomNotFoundPageSet) { string reexecution = hasReExecutionMiddleware ? "/reexecution" : ""; - string testUrl = $"{ServerPathBase}{reexecution}/post-not-found-ssr?useCustomNotFoundPage={hasCustomNotFoundPageSet}"; + string testUrl = $"{ServerPathBase}{reexecution}/post-not-found-ssr-streaming?useCustomNotFoundPage={hasCustomNotFoundPageSet}"; Navigate(testUrl); Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); From 5d940b1f565fe3e1241447340761d85b3e9ca150 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:24:07 +0200 Subject: [PATCH 11/12] Update NoInteractivityTest.cs Revert fully fixes to `NotFoundSetOnFormSubmit_ResponseNotStarted_SSR` --- .../test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index da6187e63993..6f0d4ca025a9 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -231,7 +231,7 @@ public void NotFoundSetOnFormSubmit_ResponseNotStarted_SSR(bool hasReExecutionMi Navigate(testUrl); Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); - AssertNotFoundRendered_ResponseNotStarted(hasCustomNotFoundPageSet); + AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); AssertUrlNotChanged(testUrl); } From 3be7864108a698f2caff54b096bac513733386a4 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz <32700855+ilonatommy@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:45:56 +0200 Subject: [PATCH 12/12] Update NoInteractivityTest.cs --- .../test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 6f0d4ca025a9..698e26da88b4 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -231,7 +231,7 @@ public void NotFoundSetOnFormSubmit_ResponseNotStarted_SSR(bool hasReExecutionMi Navigate(testUrl); Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); - AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); + AssertNotFoundRendered_ResponseStarted(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); AssertUrlNotChanged(testUrl); }