From 0567481fa1b8aeaee4dc1e1dc99b50a01220eea2 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Thu, 10 Sep 2026 09:58:36 +0100 Subject: [PATCH 1/2] Stabilize Edge UI integration test navigation --- .github/workflows/nightlybuild.yml | 2 - .../Common/BrowserNavigation.cs | 44 +++++++++++++++++++ .../Common/BrowserNavigationTests.cs | 30 +++++++++++++ .../Common/Hooks.cs | 3 ++ .../UserLogin/UserLoginSteps.cs | 11 ++++- 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigation.cs create mode 100644 SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigationTests.cs diff --git a/.github/workflows/nightlybuild.yml b/.github/workflows/nightlybuild.yml index a9bb789..0269e92 100644 --- a/.github/workflows/nightlybuild.yml +++ b/.github/workflows/nightlybuild.yml @@ -292,8 +292,6 @@ jobs: - name: Run Integration Tests (UI Edge) env: Browser: Edge - DriverPath: C:\\SeleniumWebDrivers\\EdgeDriver\\ - DriverExe: msedgedriver.exe run: | dotnet test "SecurityService.OpenIdConnect.IntegrationTests\SecurityService.OpenIdConnect.IntegrationTests.csproj" --filter Category=PRTest diff --git a/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigation.cs b/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigation.cs new file mode 100644 index 0000000..8dd6b9b --- /dev/null +++ b/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigation.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading.Tasks; +using OpenQA.Selenium; + +namespace SecurityService.IntergrationTests.Common; + +public static class BrowserNavigation +{ + public static async Task NavigateWithRetryAsync( + Func navigate, + Action log = null, + Int32 maxAttempts = 2, + TimeSpan retryDelay = default) + { + if (maxAttempts < 1) + { + throw new ArgumentOutOfRangeException(nameof(maxAttempts)); + } + + Exception lastException = null; + TimeSpan delay = retryDelay == default ? TimeSpan.FromSeconds(1) : retryDelay; + + for (Int32 attempt = 1; attempt <= maxAttempts; attempt++) + { + try + { + await navigate(); + return; + } + catch (WebDriverException exception) + { + lastException = exception; + log?.Invoke($"Browser navigation attempt {attempt} of {maxAttempts} failed: {exception.Message}"); + + if (attempt < maxAttempts) + { + await Task.Delay(delay); + } + } + } + + throw new WebDriverException($"Browser navigation failed after {maxAttempts} attempts.", lastException); + } +} diff --git a/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigationTests.cs b/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigationTests.cs new file mode 100644 index 0000000..ae49c26 --- /dev/null +++ b/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigationTests.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using NUnit.Framework; +using OpenQA.Selenium; + +namespace SecurityService.IntergrationTests.Common; + +[TestFixture] +public class BrowserNavigationTests +{ + [Test] + public async Task NavigateWithRetryAsync_RetriesAfterTransientNavigationFailure() + { + Int32 attempts = 0; + + await BrowserNavigation.NavigateWithRetryAsync( + () => + { + attempts++; + if (attempts == 1) + { + throw new WebDriverException("transient navigation failure"); + } + + return Task.CompletedTask; + }); + + Assert.That(attempts, Is.EqualTo(2)); + } +} diff --git a/SecurityService.OpenIdConnect.IntegrationTests/Common/Hooks.cs b/SecurityService.OpenIdConnect.IntegrationTests/Common/Hooks.cs index 488dd19..e088ce4 100644 --- a/SecurityService.OpenIdConnect.IntegrationTests/Common/Hooks.cs +++ b/SecurityService.OpenIdConnect.IntegrationTests/Common/Hooks.cs @@ -104,15 +104,18 @@ await Retry.For(async () => { EdgeOptions options = new EdgeOptions(); options.AcceptInsecureCertificates = true; + options.PageLoadStrategy = PageLoadStrategy.Eager; options.AddArguments("--headless=new"); options.AddArguments("--window-size=1280x1024"); options.AddArguments("--no-sandbox"); options.AddArguments("--disable-dev-shm-usage"); options.AddArguments("--disable-gpu"); + options.AddArguments("--host-resolver-rules=MAP identity-server 127.0.0.1"); await Retry.For(async () => { this.WebDriver = new EdgeDriver(options); }, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(60)); + this.WebDriver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(45); } this.ObjectContainer.RegisterInstanceAs(this.WebDriver); diff --git a/SecurityService.OpenIdConnect.IntegrationTests/UserLogin/UserLoginSteps.cs b/SecurityService.OpenIdConnect.IntegrationTests/UserLogin/UserLoginSteps.cs index bb78ea3..07cf02b 100644 --- a/SecurityService.OpenIdConnect.IntegrationTests/UserLogin/UserLoginSteps.cs +++ b/SecurityService.OpenIdConnect.IntegrationTests/UserLogin/UserLoginSteps.cs @@ -162,8 +162,15 @@ public async Task ThenIGetAWelcomeEmailWithMyLoginDetails() [When(@"I navigate to the confirm email address")] - public void WhenINavigateToTheConfirmEmailAddress() { - this.WebDriver.Navigate().GoToUrl(this.TestingContext.ConfirmEmailAddressLink); + public async Task WhenINavigateToTheConfirmEmailAddress() + { + await BrowserNavigation.NavigateWithRetryAsync( + () => + { + this.WebDriver.Navigate().GoToUrl(this.TestingContext.ConfirmEmailAddressLink); + return Task.CompletedTask; + }, + message => this.TestingContext.DockerHelper.Logger.LogInformation(message)); } [Then(@"I am presented with the confirm email address successful screen")] From 432fd977bd72cb65a2112a402e3a656a438974fe Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Thu, 10 Sep 2026 10:18:58 +0100 Subject: [PATCH 2/2] Avoid retrying state-changing browser navigation --- .../Common/BrowserNavigation.cs | 35 ++++--------------- .../Common/BrowserNavigationTests.cs | 20 +++++------ .../UserLogin/UserLoginSteps.cs | 2 +- 3 files changed, 17 insertions(+), 40 deletions(-) diff --git a/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigation.cs b/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigation.cs index 8dd6b9b..f3aa95b 100644 --- a/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigation.cs +++ b/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigation.cs @@ -6,39 +6,18 @@ namespace SecurityService.IntergrationTests.Common; public static class BrowserNavigation { - public static async Task NavigateWithRetryAsync( + public static async Task NavigateWithDiagnosticsAsync( Func navigate, - Action log = null, - Int32 maxAttempts = 2, - TimeSpan retryDelay = default) + Action log = null) { - if (maxAttempts < 1) + try { - throw new ArgumentOutOfRangeException(nameof(maxAttempts)); + await navigate(); } - - Exception lastException = null; - TimeSpan delay = retryDelay == default ? TimeSpan.FromSeconds(1) : retryDelay; - - for (Int32 attempt = 1; attempt <= maxAttempts; attempt++) + catch (WebDriverException exception) { - try - { - await navigate(); - return; - } - catch (WebDriverException exception) - { - lastException = exception; - log?.Invoke($"Browser navigation attempt {attempt} of {maxAttempts} failed: {exception.Message}"); - - if (attempt < maxAttempts) - { - await Task.Delay(delay); - } - } + log?.Invoke($"Browser navigation failed without retry because the URL may be state-changing: {exception.Message}"); + throw; } - - throw new WebDriverException($"Browser navigation failed after {maxAttempts} attempts.", lastException); } } diff --git a/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigationTests.cs b/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigationTests.cs index ae49c26..a6e2d23 100644 --- a/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigationTests.cs +++ b/SecurityService.OpenIdConnect.IntegrationTests/Common/BrowserNavigationTests.cs @@ -9,22 +9,20 @@ namespace SecurityService.IntergrationTests.Common; public class BrowserNavigationTests { [Test] - public async Task NavigateWithRetryAsync_RetriesAfterTransientNavigationFailure() + public async Task NavigateWithDiagnosticsAsync_DoesNotRetryAfterNavigationFailure() { Int32 attempts = 0; + String logMessage = String.Empty; - await BrowserNavigation.NavigateWithRetryAsync( - () => + Assert.ThrowsAsync(async () => await BrowserNavigation.NavigateWithDiagnosticsAsync( + async () => { attempts++; - if (attempts == 1) - { - throw new WebDriverException("transient navigation failure"); - } + throw new WebDriverException("navigation failure"); + }, + message => logMessage = message)); - return Task.CompletedTask; - }); - - Assert.That(attempts, Is.EqualTo(2)); + Assert.That(attempts, Is.EqualTo(1)); + Assert.That(logMessage, Does.Contain("navigation failure")); } } diff --git a/SecurityService.OpenIdConnect.IntegrationTests/UserLogin/UserLoginSteps.cs b/SecurityService.OpenIdConnect.IntegrationTests/UserLogin/UserLoginSteps.cs index 07cf02b..861b274 100644 --- a/SecurityService.OpenIdConnect.IntegrationTests/UserLogin/UserLoginSteps.cs +++ b/SecurityService.OpenIdConnect.IntegrationTests/UserLogin/UserLoginSteps.cs @@ -164,7 +164,7 @@ public async Task ThenIGetAWelcomeEmailWithMyLoginDetails() [When(@"I navigate to the confirm email address")] public async Task WhenINavigateToTheConfirmEmailAddress() { - await BrowserNavigation.NavigateWithRetryAsync( + await BrowserNavigation.NavigateWithDiagnosticsAsync( () => { this.WebDriver.Navigate().GoToUrl(this.TestingContext.ConfirmEmailAddressLink);