From 3702136a0575eb55907cfc5ccbd537085999ca28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:35:51 +0000 Subject: [PATCH 1/5] Initial plan From 20dd741b4b8f1db93415546271134676d3e5f414 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:48:52 +0000 Subject: [PATCH 2/5] Fix NotFoundSetOnFormSubmit_ResponseNotStarted_SSR test URL and enhance navigation timing Co-authored-by: ilonatommy <32700855+ilonatommy@users.noreply.github.com> --- src/Components/Web.JS/src/Boot.Web.ts | 7 ++++--- .../src/Services/NavigationEnhancement.ts | 19 ++++++++++++++----- .../NoInteractivityTest.cs | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Components/Web.JS/src/Boot.Web.ts b/src/Components/Web.JS/src/Boot.Web.ts index df605ceebc52..d8b71af66a98 100644 --- a/src/Components/Web.JS/src/Boot.Web.ts +++ b/src/Components/Web.JS/src/Boot.Web.ts @@ -65,13 +65,14 @@ function boot(options?: Partial) : Promise { }, }; - attachComponentDescriptorHandler(rootComponentManager); - attachStreamingRenderingListener(options?.ssr, navigationEnhancementCallbacks); - + // Initialize enhanced navigation as early as possible to prevent timing issues 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..b0af6dac09b9 100644 --- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts +++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts @@ -35,9 +35,15 @@ different bundles that only contain minimal content. const acceptHeader = 'text/html; blazor-enhanced-nav=on'; let currentEnhancedNavigationAbortController: AbortController | null; -let navigationEnhancementCallbacks: NavigationEnhancementCallbacks; +let navigationEnhancementCallbacks: Promise; +let navigationEnhancementCallbacksResolver: (callbacks: NavigationEnhancementCallbacks) => void; let performingEnhancedPageLoad: boolean; +// Initialize the promise that will be resolved when callbacks are set +navigationEnhancementCallbacks = new Promise((resolve) => { + navigationEnhancementCallbacksResolver = resolve; +}); + // 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; @@ -57,7 +63,7 @@ export function hasNeverStartedAnyEnhancedPageLoad() { } export function attachProgressivelyEnhancedNavigationListener(callbacks: NavigationEnhancementCallbacks) { - navigationEnhancementCallbacks = callbacks; + navigationEnhancementCallbacksResolver(callbacks); document.addEventListener('click', onDocumentClick); document.addEventListener('submit', onDocumentSubmit); window.addEventListener('popstate', onPopState); @@ -201,8 +207,11 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i // Notify any interactive runtimes that an enhanced navigation is starting notifyEnhancedNavigationListeners(internalDestinationHref, interceptedLink); + // Wait for navigation enhancement callbacks to be initialized before proceeding + const callbacks = await navigationEnhancementCallbacks; + // Notify handlers that enhanced navigation is starting - navigationEnhancementCallbacks.enhancedNavigationStarted(); + callbacks.enhancedNavigationStarted(); // Now request the new page via fetch, and a special header that tells the server we want it to inject // framing boundaries to distinguish the initial document and each subsequent streaming SSR update. @@ -305,7 +314,7 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i // For HTML responses, regardless of the status code, display it const parsedHtml = new DOMParser().parseFromString(initialContent, 'text/html'); synchronizeDomContent(document, parsedHtml); - navigationEnhancementCallbacks.documentUpdated(); + callbacks.documentUpdated(); } else if (responseContentType?.startsWith('text/') && initialContent) { // For any other text-based content, we'll just display it, because that's what // would happen if this was a non-enhanced request. @@ -344,7 +353,7 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i } performingEnhancedPageLoad = false; - navigationEnhancementCallbacks.enhancedNavigationCompleted(); + callbacks.enhancedNavigationCompleted(); // For non-GET requests, the destination has to be the same URL you're already on, or result in a redirection // (post/redirect/get). You're not allowed to POST to a different URL without redirecting, because then back/forwards diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index bbeb1ad4d9bb..80ee146b94af 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -205,7 +205,7 @@ 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(); From 08fc8003340b407635e778ad7950f77ff50bcb96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:07:47 +0000 Subject: [PATCH 3/5] Replace AssertNotFoundRendered_ResponseStarted_Or_POST with direct if/else logic in NotFoundSetOnFormSubmit_ResponseNotStarted_SSR test Co-authored-by: ilonatommy <32700855+ilonatommy@users.noreply.github.com> --- src/Components/Web.JS/src/Boot.Web.ts | 7 +++---- .../src/Services/NavigationEnhancement.ts | 19 +++++-------------- .../NoInteractivityTest.cs | 9 ++++++++- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/Components/Web.JS/src/Boot.Web.ts b/src/Components/Web.JS/src/Boot.Web.ts index d8b71af66a98..df605ceebc52 100644 --- a/src/Components/Web.JS/src/Boot.Web.ts +++ b/src/Components/Web.JS/src/Boot.Web.ts @@ -65,14 +65,13 @@ function boot(options?: Partial) : Promise { }, }; - // Initialize enhanced navigation as early as possible to prevent timing issues + 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 b0af6dac09b9..e029c77178dd 100644 --- a/src/Components/Web.JS/src/Services/NavigationEnhancement.ts +++ b/src/Components/Web.JS/src/Services/NavigationEnhancement.ts @@ -35,15 +35,9 @@ different bundles that only contain minimal content. const acceptHeader = 'text/html; blazor-enhanced-nav=on'; let currentEnhancedNavigationAbortController: AbortController | null; -let navigationEnhancementCallbacks: Promise; -let navigationEnhancementCallbacksResolver: (callbacks: NavigationEnhancementCallbacks) => void; +let navigationEnhancementCallbacks: NavigationEnhancementCallbacks; let performingEnhancedPageLoad: boolean; -// Initialize the promise that will be resolved when callbacks are set -navigationEnhancementCallbacks = new Promise((resolve) => { - navigationEnhancementCallbacksResolver = resolve; -}); - // 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; @@ -63,7 +57,7 @@ export function hasNeverStartedAnyEnhancedPageLoad() { } export function attachProgressivelyEnhancedNavigationListener(callbacks: NavigationEnhancementCallbacks) { - navigationEnhancementCallbacksResolver(callbacks); + navigationEnhancementCallbacks = callbacks; document.addEventListener('click', onDocumentClick); document.addEventListener('submit', onDocumentSubmit); window.addEventListener('popstate', onPopState); @@ -207,11 +201,8 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i // Notify any interactive runtimes that an enhanced navigation is starting notifyEnhancedNavigationListeners(internalDestinationHref, interceptedLink); - // Wait for navigation enhancement callbacks to be initialized before proceeding - const callbacks = await navigationEnhancementCallbacks; - // Notify handlers that enhanced navigation is starting - callbacks.enhancedNavigationStarted(); + navigationEnhancementCallbacks.enhancedNavigationStarted(); // Now request the new page via fetch, and a special header that tells the server we want it to inject // framing boundaries to distinguish the initial document and each subsequent streaming SSR update. @@ -314,7 +305,7 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i // For HTML responses, regardless of the status code, display it const parsedHtml = new DOMParser().parseFromString(initialContent, 'text/html'); synchronizeDomContent(document, parsedHtml); - callbacks.documentUpdated(); + navigationEnhancementCallbacks.documentUpdated(); } else if (responseContentType?.startsWith('text/') && initialContent) { // For any other text-based content, we'll just display it, because that's what // would happen if this was a non-enhanced request. @@ -353,7 +344,7 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i } performingEnhancedPageLoad = false; - callbacks.enhancedNavigationCompleted(); + navigationEnhancementCallbacks.enhancedNavigationCompleted(); // For non-GET requests, the destination has to be the same URL you're already on, or result in a redirection // (post/redirect/get). You're not allowed to POST to a different URL without redirecting, because then back/forwards diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index 80ee146b94af..a611f09904e9 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -209,7 +209,14 @@ 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); + if (hasCustomNotFoundPageSet) + { + AssertNotFoundPageRendered(); + } + else + { + AssertNotFoundFragmentRendered(); + } AssertUrlNotChanged(testUrl); } From a3dc99709a0bd7de2477e4686f6a0f39a94d6812 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 2 Jul 2025 16:11:41 +0200 Subject: [PATCH 4/5] FIx for the corrected test. --- 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 cf72016144ae13aff6cb773683d2f0bd44d442a8 Mon Sep 17 00:00:00 2001 From: Ilona Tomkowicz Date: Wed, 2 Jul 2025 18:36:05 +0200 Subject: [PATCH 5/5] Fix the test expectation. --- .../E2ETest/ServerRenderingTests/NoInteractivityTest.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs index a611f09904e9..80ee146b94af 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/NoInteractivityTest.cs @@ -209,14 +209,7 @@ public void NotFoundSetOnFormSubmit_ResponseNotStarted_SSR(bool hasReExecutionMi Navigate(testUrl); Browser.FindElement(By.Id("not-found-form")).FindElement(By.TagName("button")).Click(); - if (hasCustomNotFoundPageSet) - { - AssertNotFoundPageRendered(); - } - else - { - AssertNotFoundFragmentRendered(); - } + AssertNotFoundRendered_ResponseStarted_Or_POST(hasReExecutionMiddleware, hasCustomNotFoundPageSet, testUrl); AssertUrlNotChanged(testUrl); }