Skip to content

Commit d16321c

Browse files
authored
Improve enhanced navigation suppression in tests (#63132)
* Suppress enhanced nav per test. * `fixture.Navigate` clears the storage, reading testId has to be done first. * NonInteractivityTests contain tests that require supression and tests that don't support session storage at the same time. Allow single tests to request id assignment. * Wait for the base page to be loaded to make sure session storage is available. * Move the id initialization to the enhanced nav suppression method. * Go back to setting test id only when suppression happens but try to clean it after each test. * Improve cleanup - supression flag can also get removed. * Fix `RefreshCanFallBackOnFullPageReload` * Cleanup - removal of storage items can be done once, on disposal. * BasicTestApp did not have h1 "Hello" that we expect on cleanup. Fix it. * Tests that closed the browser do not have to clean the session storage. * Fix `DragDrop_CanTrigger` that requires specific layout of elements on the index page. * Add logs in case of "Failed to execute script after 3 retries." error. * Avoid searching for just h1 tag, use specific ids. * Limit relying on JS execution for checking the element position + increase timeouts for landing page assert that can be too short once in every 20 runs.
1 parent 5afdc06 commit d16321c

File tree

18 files changed

+126
-53
lines changed

18 files changed

+126
-53
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@page "/"
22

3-
<h1>Hello, world!</h1>
3+
<h1 id="session-storage-anchor">Hello, world!</h1>
44

55
Welcome to your new app.

src/Components/test/E2ETest/Infrastructure/ServerTestBase.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
5+
using Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests;
56
using Microsoft.AspNetCore.E2ETesting;
67
using OpenQA.Selenium;
78
using Xunit.Abstractions;
@@ -31,6 +32,12 @@ public void Navigate(string relativeUrl)
3132
Browser.Navigate(_serverFixture.RootUri, relativeUrl);
3233
}
3334

35+
public override async Task DisposeAsync()
36+
{
37+
EnhancedNavigationTestUtil.CleanEnhancedNavigationSuppression(this);
38+
await base.DisposeAsync();
39+
}
40+
3441
protected override void InitializeAsyncCore()
3542
{
3643
// Clear logs - we check these during tests in some cases.

src/Components/test/E2ETest/Infrastructure/WebDriverExtensions/WebDriverExtensions.cs

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@
33

44
using OpenQA.Selenium;
55
using OpenQA.Selenium.Support.UI;
6-
using System;
76

87
namespace Microsoft.AspNetCore.Components.E2ETest;
98

109
internal static class WebDriverExtensions
1110
{
12-
private static string GetFindPositionScript(string elementId) =>
13-
$"return Math.round(document.getElementById('{elementId}').getBoundingClientRect().top + window.scrollY);";
14-
1511
public static void Navigate(this IWebDriver browser, Uri baseUri, string relativeUrl)
1612
{
1713
var absoluteUrl = new Uri(baseUri, relativeUrl);
@@ -45,27 +41,27 @@ public static void WaitForElementToBeVisible(this IWebDriver browser, By by, int
4541

4642
public static long GetElementPositionWithRetry(this IWebDriver browser, string elementId, int retryCount = 3, int delayBetweenRetriesMs = 100)
4743
{
48-
var jsExecutor = (IJavaScriptExecutor)browser;
49-
string script = GetFindPositionScript(elementId);
50-
browser.WaitForElementToBeVisible(By.Id(elementId));
44+
string log = "";
45+
5146
for (int i = 0; i < retryCount; i++)
5247
{
5348
try
5449
{
55-
var result = jsExecutor.ExecuteScript(script);
56-
if (result != null)
57-
{
58-
return (long)result;
59-
}
50+
browser.WaitForElementToBeVisible(By.Id(elementId));
51+
var element = browser.FindElement(By.Id(elementId));
52+
return element.Location.Y;
6053
}
61-
catch (OpenQA.Selenium.JavaScriptException)
54+
catch (Exception ex)
6255
{
63-
// JavaScript execution failed, retry
56+
log += $"Attempt {i + 1}: - {ex.Message}. ";
6457
}
6558

66-
Thread.Sleep(delayBetweenRetriesMs);
59+
if (i < retryCount - 1)
60+
{
61+
Thread.Sleep(delayBetweenRetriesMs);
62+
}
6763
}
6864

69-
throw new Exception($"Failed to execute script after {retryCount} retries.");
65+
throw new Exception($"Failed to get position for element '{elementId}' after {retryCount} retries. Debug log: {log}");
7066
}
7167
}

src/Components/test/E2ETest/ServerExecutionTests/CircuitContextTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public void ComponentMethods_HaveCircuitContext_OnInitialPageLoad()
4141
// Internal for reuse in Blazor Web tests
4242
internal static void TestCircuitContextCore(IWebDriver browser)
4343
{
44-
browser.Equal("Circuit Context", () => browser.Exists(By.TagName("h1")).Text);
44+
browser.Equal("Circuit Context", () => browser.Exists(By.Id("circuit-context-title")).Text);
4545

4646
browser.Click(By.Id("trigger-click-event-button"));
4747

src/Components/test/E2ETest/ServerExecutionTests/CircuitGracefulTerminationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ protected override void InitializeAsyncCore()
4040
{
4141
Navigate(ServerPathBase);
4242
Browser.MountTestComponent<GracefulTermination>();
43-
Browser.Equal("Graceful Termination", () => Browser.Exists(By.TagName("h1")).Text);
43+
Browser.Equal("Graceful Termination", () => Browser.Exists(By.Id("graceful-termination-title")).Text);
4444

4545
GracefulDisconnectCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
4646
Sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();

src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,9 @@ public void RefreshCanFallBackOnFullPageReload(string renderMode)
317317
Browser.Navigate().Refresh();
318318
Browser.Equal("Page with interactive components that navigate", () => Browser.Exists(By.TagName("h1")).Text);
319319

320+
// if we don't clean up the suppression, all subsequent navigations will be suppressed by default
321+
EnhancedNavigationTestUtil.CleanEnhancedNavigationSuppression(this, skipNavigation: true);
322+
320323
// Normally, you shouldn't store references to elements because they could become stale references
321324
// after the page re-renders. However, we want to explicitly test that the element becomes stale
322325
// across renders to ensure that a full page reload occurs.
@@ -677,11 +680,10 @@ public void CanUpdateHrefOnLinkTagWithIntegrity()
677680
}
678681

679682
[Theory]
680-
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/60875")]
681-
// [InlineData(false, false, false)] // https://github.com/dotnet/aspnetcore/issues/60875
683+
[InlineData(false, false, false)]
682684
[InlineData(false, true, false)]
683685
[InlineData(true, true, false)]
684-
// [InlineData(true, false, false)] // https://github.com/dotnet/aspnetcore/issues/60875
686+
[InlineData(true, false, false)]
685687
// [InlineData(false, false, true)] programmatic navigation doesn't work without enhanced navigation
686688
[InlineData(false, true, true)]
687689
[InlineData(true, true, true)]
@@ -692,8 +694,8 @@ public void EnhancedNavigationScrollBehavesSameAsBrowserOnNavigation(bool enable
692694
// or to the beginning of a fragment, regardless of the previous scroll position
693695
string landingPageSuffix = enableStreaming ? "" : "-no-streaming";
694696
string buttonKeyword = programmaticNavigation ? "-programmatic" : "";
697+
EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, shouldSuppress: !useEnhancedNavigation);
695698
Navigate($"{ServerPathBase}/nav/scroll-test{landingPageSuffix}");
696-
EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, shouldSuppress: !useEnhancedNavigation, skipNavigation: true);
697699

698700
// "landing" page: scroll maximally down and go to "next" page - we should land at the top of that page
699701
AssertWeAreOnLandingPage();
@@ -732,10 +734,10 @@ public void EnhancedNavigationScrollBehavesSameAsBrowserOnNavigation(bool enable
732734
}
733735

734736
[Theory]
735-
// [InlineData(false, false, false)] // https://github.com/dotnet/aspnetcore/issues/60875
737+
[InlineData(false, false, false)]
736738
[InlineData(false, true, false)]
737739
[InlineData(true, true, false)]
738-
// [InlineData(true, false, false)] // https://github.com/dotnet/aspnetcore/issues/60875
740+
[InlineData(true, false, false)]
739741
// [InlineData(false, false, true)] programmatic navigation doesn't work without enhanced navigation
740742
[InlineData(false, true, true)]
741743
[InlineData(true, true, true)]
@@ -745,8 +747,8 @@ public void EnhancedNavigationScrollBehavesSameAsBrowserOnBackwardsForwardsActio
745747
// This test checks if the scroll position is preserved after backwards/forwards action
746748
string landingPageSuffix = enableStreaming ? "" : "-no-streaming";
747749
string buttonKeyword = programmaticNavigation ? "-programmatic" : "";
750+
EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, shouldSuppress: !useEnhancedNavigation);
748751
Navigate($"{ServerPathBase}/nav/scroll-test{landingPageSuffix}");
749-
EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, shouldSuppress: !useEnhancedNavigation, skipNavigation: true);
750752

751753
// "landing" page: scroll to pos1, navigate away
752754
AssertWeAreOnLandingPage();
@@ -831,6 +833,8 @@ private void AssertScrollPositionCorrect(bool useEnhancedNavigation, long previo
831833
private void AssertEnhancedNavigation(bool useEnhancedNavigation, IWebElement elementForStalenessCheck, int retryCount = 3, int delayBetweenRetriesMs = 1000)
832834
{
833835
bool enhancedNavigationDetected = false;
836+
string logging = "";
837+
string isNavigationSuppressed = "";
834838
for (int i = 0; i < retryCount; i++)
835839
{
836840
try
@@ -841,28 +845,32 @@ private void AssertEnhancedNavigation(bool useEnhancedNavigation, IWebElement el
841845
}
842846
catch (XunitException)
843847
{
848+
var logs = Browser.GetBrowserLogs(LogLevel.Warning);
849+
logging += $"{string.Join(", ", logs.Select(l => l.Message))}\n";
850+
isNavigationSuppressed = (string)((IJavaScriptExecutor)Browser).ExecuteScript("return sessionStorage.getItem('suppress-enhanced-navigation');");
851+
852+
logging += $" isNavigationSuppressed: {isNavigationSuppressed}\n";
844853
// Maybe the check was done too early to change the DOM ref, retry
845854
}
846855

847856
Thread.Sleep(delayBetweenRetriesMs);
848857
}
849-
string expectedNavigation = useEnhancedNavigation ? "enhanced navigation" : "browser navigation";
858+
string expectedNavigation = useEnhancedNavigation ? "enhanced navigation" : "full page load";
850859
string isStale = enhancedNavigationDetected ? "is not stale" : "is stale";
851-
var isNavigationSupressed = (string)((IJavaScriptExecutor)Browser).ExecuteScript("return sessionStorage.getItem('suppress-enhanced-navigation');");
852-
throw new Exception($"Expected to use {expectedNavigation} because 'suppress-enhanced-navigation' is set to {isNavigationSupressed} but the element from previous path {isStale}");
860+
throw new Exception($"Expected to use {expectedNavigation} because 'suppress-enhanced-navigation' is set to {isNavigationSuppressed} but the element from previous path {isStale}. logging={logging}");
853861
}
854862

855863
private void AssertWeAreOnLandingPage()
856864
{
857865
string infoName = "test-info-1";
858-
Browser.WaitForElementToBeVisible(By.Id(infoName), timeoutInSeconds: 20);
866+
Browser.WaitForElementToBeVisible(By.Id(infoName), timeoutInSeconds: 30);
859867
Browser.Equal("Scroll tests landing page", () => Browser.Exists(By.Id(infoName)).Text);
860868
}
861869

862870
private void AssertWeAreOnNextPage()
863871
{
864872
string infoName = "test-info-2";
865-
Browser.WaitForElementToBeVisible(By.Id(infoName), timeoutInSeconds: 20);
873+
Browser.WaitForElementToBeVisible(By.Id(infoName), timeoutInSeconds: 30);
866874
Browser.Equal("Scroll tests next page", () => Browser.Exists(By.Id(infoName)).Text);
867875
}
868876

src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTestUtil.cs

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests;
1111

1212
public static class EnhancedNavigationTestUtil
1313
{
14+
private static bool _isSuppressed;
15+
1416
public static void SuppressEnhancedNavigation<TServerFixture>(ServerTestBase<TServerFixture> fixture, bool shouldSuppress, bool skipNavigation = false)
1517
where TServerFixture : ServerFixture
1618
{
@@ -20,16 +22,76 @@ public static void SuppressEnhancedNavigation<TServerFixture>(ServerTestBase<TSe
2022

2123
if (!skipNavigation)
2224
{
23-
// Normally we need to navigate here first otherwise the browser isn't on the correct origin to access
24-
// localStorage. But some tests are already in the right place and need to avoid extra navigation.
25-
fixture.Navigate($"{fixture.ServerPathBase}/");
26-
browser.Equal("Hello", () => browser.Exists(By.TagName("h1")).Text);
25+
NavigateToOrigin(fixture);
26+
}
27+
28+
try
29+
{
30+
((IJavaScriptExecutor)browser).ExecuteScript("sessionStorage.length");
31+
}
32+
catch (Exception ex)
33+
{
34+
throw new InvalidOperationException("Session storage not found. Ensure that the browser is on the correct origin by navigating to a page or by setting skipNavigation to false.", ex);
2735
}
2836

2937
((IJavaScriptExecutor)browser).ExecuteScript("sessionStorage.setItem('suppress-enhanced-navigation', 'true')");
38+
39+
var suppressEnhancedNavigation = ((IJavaScriptExecutor)browser).ExecuteScript("return sessionStorage.getItem('suppress-enhanced-navigation');");
40+
Assert.True(suppressEnhancedNavigation is not null && (string)suppressEnhancedNavigation == "true",
41+
"Expected 'suppress-enhanced-navigation' to be set in sessionStorage.");
42+
_isSuppressed = true;
3043
}
3144
}
3245

46+
public static void CleanEnhancedNavigationSuppression<TServerFixture>(ServerTestBase<TServerFixture> fixture, bool skipNavigation = false)
47+
where TServerFixture : ServerFixture
48+
{
49+
if (!_isSuppressed)
50+
{
51+
return;
52+
}
53+
54+
var browser = fixture.Browser;
55+
56+
try
57+
{
58+
// First, ensure we're on the correct origin to access sessionStorage
59+
try
60+
{
61+
// Check if we can access sessionStorage from current location
62+
((IJavaScriptExecutor)browser).ExecuteScript("sessionStorage.length");
63+
}
64+
catch
65+
{
66+
if (skipNavigation)
67+
{
68+
throw new InvalidOperationException("Session storage not found. Ensure that the browser is on the correct origin by navigating to a page or by setting skipNavigation to false.");
69+
}
70+
NavigateToOrigin(fixture);
71+
}
72+
((IJavaScriptExecutor)browser).ExecuteScript($"sessionStorage.removeItem('suppress-enhanced-navigation')");
73+
}
74+
catch (WebDriverException ex) when (ex.Message.Contains("invalid session id"))
75+
{
76+
// Browser session is no longer valid (e.g., browser was closed)
77+
// Session storage is automatically cleared when browser closes, so cleanup is already done
78+
// This is expected in some tests, so we silently return
79+
return;
80+
}
81+
finally
82+
{
83+
_isSuppressed = false;
84+
}
85+
}
86+
87+
private static void NavigateToOrigin<TServerFixture>(ServerTestBase<TServerFixture> fixture)
88+
where TServerFixture : ServerFixture
89+
{
90+
// Navigate to the test origin to ensure the browser is on the correct state to access sessionStorage
91+
fixture.Navigate($"{fixture.ServerPathBase}/");
92+
fixture.Browser.Exists(By.Id("session-storage-anchor"));
93+
}
94+
3395
public static long GetScrollY(this IWebDriver browser)
3496
=> Convert.ToInt64(((IJavaScriptExecutor)browser).ExecuteScript("return window.scrollY"), CultureInfo.CurrentCulture);
3597

src/Components/test/E2ETest/Tests/SectionsWithCascadingParametersTest.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void RenderSectionContent_CascadingParameterForSectionOutletIsDeterminedB
3535
Browser.FindElement(By.Id("render-section-outlet")).Click();
3636
Browser.FindElement(By.Id("render-second-section-content")).Click();
3737

38-
Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.TagName("p")).Text);
38+
Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.Id("text-component")).Text);
3939
}
4040

4141
[Fact]
@@ -46,7 +46,7 @@ public void ChangeCascadingValueForSectionContent_CascadingValueForSectionOutlet
4646

4747
Browser.FindElement(By.Id("change-cascading-value")).Click();
4848

49-
Browser.Equal("First Section with additional text for second section", () => Browser.Exists(By.TagName("p")).Text);
49+
Browser.Equal("First Section with additional text for second section", () => Browser.Exists(By.Id("text-component")).Text);
5050
}
5151

5252
[Fact]
@@ -56,7 +56,7 @@ public void RenderTwoSectionContentsWithSameId_CascadingParameterForSectionOutle
5656
Browser.FindElement(By.Id("render-first-section-content")).Click();
5757
Browser.FindElement(By.Id("render-section-outlet")).Click();
5858

59-
Browser.Equal("First Section with additional text for first section", () => Browser.Exists(By.TagName("p")).Text);
59+
Browser.Equal("First Section with additional text for first section", () => Browser.Exists(By.Id("text-component")).Text);
6060
}
6161

6262
[Fact]
@@ -68,7 +68,7 @@ public void SecondSectionContentIdChanged_CascadingParameterForSectionOutletIsDe
6868

6969
Browser.FindElement(By.Id("change-second-section-content-id")).Click();
7070

71-
Browser.Equal("First Section with additional text for first section", () => Browser.Exists(By.TagName("p")).Text);
71+
Browser.Equal("First Section with additional text for first section", () => Browser.Exists(By.Id("text-component")).Text);
7272
}
7373

7474
[Fact]
@@ -80,7 +80,7 @@ public void SecondSectionContentDisposed_CascadingParameterForSectionOutletIsDet
8080

8181
Browser.FindElement(By.Id("dispose-second-section-content")).Click();
8282

83-
Browser.Equal("First Section with additional text for first section", () => Browser.Exists(By.TagName("p")).Text);
83+
Browser.Equal("First Section with additional text for first section", () => Browser.Exists(By.Id("text-component")).Text);
8484
}
8585

8686
[Fact]
@@ -92,7 +92,7 @@ public void FirstSectionContentDisposedThenRenderSecondSectionContent_CascadingP
9292
Browser.FindElement(By.Id("dispose-first-section-content")).Click();
9393
Browser.FindElement(By.Id("render-second-section-content")).Click();
9494

95-
Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.TagName("p")).Text);
95+
Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.Id("text-component")).Text);
9696
}
9797

9898
[Fact]
@@ -105,6 +105,6 @@ public void SectionOutletIdChanged_CascadingParameterForSectionOutletIsDetermine
105105

106106
Browser.FindElement(By.Id("change-section-outlet-id")).Click();
107107

108-
Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.TagName("p")).Text);
108+
Browser.Equal("Second Section with additional text for second section", () => Browser.Exists(By.Id("text-component")).Text);
109109
}
110110
}

src/Components/test/E2ETest/Tests/SectionsWithErrorBoundaryTest.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public void RenderSectionContent_ErrorBoundaryForSectionOutletContentIsDetermine
3737

3838
Browser.FindElement(By.Id("error-button")).Click();
3939

40-
Browser.Equal("Sorry!", () => Browser.Exists(By.TagName("p")).Text);
40+
Browser.Equal("Sorry!", () => Browser.Exists(By.Id("error-content")).Text);
4141
}
4242

4343
[Fact]
@@ -88,7 +88,7 @@ public void FirstSectionContentDisposedThenRenderSecondSectionContent_ErrorBound
8888
Browser.FindElement(By.Id("render-second-section-content")).Click();
8989
Browser.FindElement(By.Id("error-button")).Click();
9090

91-
Browser.Equal("Sorry!", () => Browser.Exists(By.TagName("p")).Text);
91+
Browser.Equal("Sorry!", () => Browser.Exists(By.Id("error-content")).Text);
9292
}
9393

9494
[Fact]
@@ -102,6 +102,6 @@ public void SectionOutletIdChanged_ErrorBoundaryForSectionOutletIsDeterminedByMa
102102
Browser.FindElement(By.Id("change-section-outlet-id")).Click();
103103
Browser.FindElement(By.Id("error-button")).Click();
104104

105-
Browser.Equal("Sorry!", () => Browser.Exists(By.TagName("p")).Text);
105+
Browser.Equal("Sorry!", () => Browser.Exists(By.Id("error-content")).Text);
106106
}
107107
}

src/Components/test/testassets/BasicTestApp/GracefulTermination.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@inject NavigationManager navigationManager
22

3-
<h1>Graceful Termination</h1>
3+
<h1 id="graceful-termination-title">Graceful Termination</h1>
44

55
<a href="mailto:test@example.com" id="mailto-link">Send Email</a>
66
<a href="download" download id="download-href">Download Link</a>

0 commit comments

Comments
 (0)