From 667d4553c66402748db68ac013aa1287f1a660e8 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 28 Oct 2026 23:19:16 +0000 Subject: [PATCH] test: retry E2E first navigation on ERR_NETWORK_CHANGED CanDrawWithTouch intermittently fails its very first navigation with 'net::ERR_NETWORK_CHANGED at http://localhost:/'. That is Chromium aborting an in-flight request because it observed a host network-state change - Docker creating/removing container bridge interfaces while Aspire starts resources produces exactly that. It is not an app error. Evidence (run 40782982599, Test-ubuntu-latest): - Failed case died in 114ms versus 1-3s for the three passing viewports, so it aborted before the page rendered anything. - Rerunning the failed jobs passed all four viewports. - Same signature documented in microsoft/playwright#13062 (container, retry clears it) and Grizzly-Endeavors/residuum#592 (Docker bridge churn explicitly named as the cause). Fix: add GotoWithNetworkChangeRetryAsync, which retries only when the exception message contains ERR_NETWORK_CHANGED, up to 3 attempts with backoff. Any other PlaywrightException propagates unchanged, so real failures are still real failures. The abort happens before the page has rendered, so retrying that specific navigation is side-effect free. Applied to the first navigation in both E2E interaction tests - the mouse test has the identical unguarded pattern and the same exposure. Scope is deliberately minimal: this does not address the separate 5s locator timeout for a[href='NewPaint'] seen on dev in August, nor the unguarded _localActorHandle.Tell in Paint.razor:268. --- .../DrawingMouseInteractionTests.cs | 6 ++-- .../DrawingTouchInteractionTests.cs | 8 +++-- .../Util/DrawTogetherPlaywrightHelpers.cs | 36 +++++++++++++++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/DrawTogether.End2End.Tests/DrawingMouseInteractionTests.cs b/tests/DrawTogether.End2End.Tests/DrawingMouseInteractionTests.cs index 1bd28ed..6d478d9 100644 --- a/tests/DrawTogether.End2End.Tests/DrawingMouseInteractionTests.cs +++ b/tests/DrawTogether.End2End.Tests/DrawingMouseInteractionTests.cs @@ -49,8 +49,10 @@ public async Task CanDrawWithMouse() _output.WriteLine($"Navigating to {endpoint}"); - // Navigate to the app - await page.GotoAsync(endpoint.ToString()); + // Navigate to the app. Retry on ERR_NETWORK_CHANGED: a freshly launched + // Chromium can abort its first navigation when Aspire's container network + // is still settling. + await GotoWithNetworkChangeRetryAsync(page, endpoint.ToString(), output: _output); // Handle authentication if needed - example for form-based auth await HandleNewUserRegistration(page, endpoint, _output); diff --git a/tests/DrawTogether.End2End.Tests/DrawingTouchInteractionTests.cs b/tests/DrawTogether.End2End.Tests/DrawingTouchInteractionTests.cs index d39c8a6..6f5cef8 100644 --- a/tests/DrawTogether.End2End.Tests/DrawingTouchInteractionTests.cs +++ b/tests/DrawTogether.End2End.Tests/DrawingTouchInteractionTests.cs @@ -1,4 +1,4 @@ -using DrawTogether.End2End.Tests.Util; +using DrawTogether.End2End.Tests.Util; using Microsoft.Playwright; using Xunit.Abstractions; using static DrawTogether.End2End.Tests.Util.DrawTogetherPlaywrightHelpers; @@ -63,8 +63,10 @@ public async Task CanDrawWithTouch(int viewPortX, int viewPortY) _output.WriteLine($"Navigating to {endpoint}"); - // Navigate to the app - await page.GotoAsync(endpoint.ToString()); + // Navigate to the app. Retry on ERR_NETWORK_CHANGED: a freshly launched + // Chromium can abort its first navigation when Aspire's container network + // is still settling. + await GotoWithNetworkChangeRetryAsync(page, endpoint.ToString(), output: _output); // Handle authentication if needed - example for form-based auth await HandleNewUserRegistration(page, endpoint, _output); diff --git a/tests/DrawTogether.End2End.Tests/Util/DrawTogetherPlaywrightHelpers.cs b/tests/DrawTogether.End2End.Tests/Util/DrawTogetherPlaywrightHelpers.cs index 1ff262f..c42bc8e 100644 --- a/tests/DrawTogether.End2End.Tests/Util/DrawTogetherPlaywrightHelpers.cs +++ b/tests/DrawTogether.End2End.Tests/Util/DrawTogetherPlaywrightHelpers.cs @@ -5,6 +5,42 @@ namespace DrawTogether.End2End.Tests.Util; public static class DrawTogetherPlaywrightHelpers { + /// + /// The first navigation from a freshly launched Chromium can be aborted with + /// net::ERR_NETWORK_CHANGED: the browser tears down in-flight requests when it + /// observes a host network-state change, and Docker creating/removing container bridge + /// interfaces while Aspire starts resources produces exactly that. + /// See https://github.com/microsoft/playwright/issues/13062 (same signature, retry clears it) + /// and https://github.com/Grizzly-Endeavors/residuum/issues/592 (Docker bridge churn). + /// The abort happens before the page renders anything, so retrying is side-effect free. + /// + private const string NetworkChangedError = "ERR_NETWORK_CHANGED"; + + public static async Task GotoWithNetworkChangeRetryAsync( + IPage page, + string url, + PageGotoOptions? options = null, + ITestOutputHelper? output = null, + int maxAttempts = 3) + { + for (var attempt = 1; ; attempt++) + { + try + { + return await page.GotoAsync(url, options); + } + catch (PlaywrightException ex) when ( + attempt < maxAttempts && + ex.Message.Contains(NetworkChangedError, StringComparison.Ordinal)) + { + output?.WriteLine( + $"Navigation to {url} aborted by {NetworkChangedError} " + + $"(attempt {attempt}/{maxAttempts}); retrying."); + await Task.Delay(TimeSpan.FromMilliseconds(250 * attempt)); + } + } + } + public static async Task LocateDrawingBox(IPage page, ITestOutputHelper output) { // Wait for the canvas to be present