Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
66d2b0b
Supress enhanced nav per test.
ilonatommy Aug 5, 2025
4b1f572
Each test should have a testId.
ilonatommy Aug 5, 2025
945c9dd
Move granting test id to the base class.
ilonatommy Aug 6, 2025
e9bf1bf
Remove unused namespace.
ilonatommy Aug 6, 2025
f9601f1
Not all tests can use session storage.
ilonatommy Aug 6, 2025
2cd3a18
Improve exception message.
ilonatommy Aug 6, 2025
5fd1050
Fix build + fix multiple blank lines.
ilonatommy Aug 6, 2025
7c0bf05
redirection tests do not ned supression.
ilonatommy Aug 6, 2025
b2e8441
Merge branch 'main' into improve-enhanced-nav-supression
ilonatommy Aug 6, 2025
08c99cf
`fixture.Navigate` clears the storage, reading testId has to be done …
ilonatommy Aug 6, 2025
73abbda
NonInteractivityTests contain tests that require supression and tests…
ilonatommy Aug 6, 2025
5ef86a1
Wait for the base page to be loaded to make sure session storage is a…
ilonatommy Aug 20, 2025
357192c
Move the id initialization to the enhanced nav supression method.
ilonatommy Aug 20, 2025
92cad9a
Cleanup.
ilonatommy Aug 20, 2025
8c5c3b1
Go back to setting test id only when supression happens but try to cl…
ilonatommy Aug 21, 2025
189db63
Improve cleanup - supression flag can also get removed.
ilonatommy Aug 21, 2025
fa057a8
Fix
ilonatommy Aug 21, 2025
ee4aa34
Fix `RefreshCanFallBackOnFullPageReload`
ilonatommy Aug 21, 2025
795b665
Fix tests.
ilonatommy Aug 25, 2025
a6856fc
Cleanup - removal of storage items can be done once, on disposal.
ilonatommy Aug 25, 2025
249f03a
Feedback.
ilonatommy Aug 25, 2025
763ea9d
BasicTestApp did not have h1 "Hello" that we expect on cleanup. Fix it.
ilonatommy Aug 25, 2025
5696430
Tests that closed the browser do not have to clean the session storage.
ilonatommy Aug 25, 2025
e1b6931
Another test needs a more specific selector.
ilonatommy Aug 25, 2025
1a8d9bf
Fix `DragDrop_CanTrigger` that requires specific layout of elements o…
ilonatommy Aug 25, 2025
1339daa
Add logs in case of "Failed to execute script after 3 retries." error.
ilonatommy Aug 25, 2025
21051c2
Avoid searching for just h1 tag, use specific ids.
ilonatommy Aug 25, 2025
0bd2afc
Limit relying on JS execution for checking the element position + inc…
ilonatommy Aug 26, 2025
7ac455c
Grant ID for each test on initialization. Change BinaryHttpClientTest…
ilonatommy Aug 27, 2025
18ee068
Try on CI if tests run with small or big window (toggle bar has probl…
ilonatommy Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@page "/"

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

Welcome to your new app.
20 changes: 18 additions & 2 deletions src/Components/test/E2ETest/Infrastructure/ServerTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using OpenQA.Selenium.BiDi.Communication;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
Expand All @@ -13,28 +15,42 @@ public abstract class ServerTestBase<TServerFixture>
IClassFixture<TServerFixture>
where TServerFixture : ServerFixture
{
public string ServerPathBase => "/subdir";
private string _serverPathBase;
public string ServerPathBase => _serverPathBase;

protected readonly TServerFixture _serverFixture;

public ServerTestBase(
BrowserFixture browserFixture,
TServerFixture serverFixture,
ITestOutputHelper output)
ITestOutputHelper output,
string serverPathBase = "/subdir")
: base(browserFixture, output)
{
_serverFixture = serverFixture;
_serverPathBase = serverPathBase;
}

public void Navigate(string relativeUrl)
{
Browser.Navigate(_serverFixture.RootUri, relativeUrl);
}

public override async Task DisposeAsync()
{
EnhancedNavigationTestUtil.CleanEnhancedNavigationSuppression(this);
Copy link
Member

@oroztocil oroztocil Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naive question: Is there a world where a test crashes and the enhanced navigation suppression flag is not cleaned up and that has a flaky effect on other tests? Or is that avoided by each test appending different IDs to the flag in the session storage?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That depends on the type of failure. Disposal method should run after each test, regardless of the result.

In case the failure was connected with browser getting disconnected/crashed etc, then we loose the connection to the old instance, that is, the old session storage values are gone, we don't need to worry about them. The following tests should have a fresh instance created by selenium (with a clean storage).

It's not really a matter of test id because we don't grant test id unless the test wants to suppress enhanced nav. So it's not true that each test has their ID, only suppressing tests have them. I was initially trying an approach of granting it for every test, in InitializeAsync but it resulted in problems with browser crashing on redirecting to the safe location to set the storage. Maybe I could reconsider that approach now, I did not take detailed notes what was the blocker there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation!

await base.DisposeAsync();
}

protected override void InitializeAsyncCore()
{
// Clear logs - we check these during tests in some cases.
// Make sure each test starts clean.
((IJavaScriptExecutor)Browser).ExecuteScript("console.clear()");
}

protected override void GrantTestId()
{
EnhancedNavigationTestUtil.GrantTestId(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,11 @@

using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using System;

namespace Microsoft.AspNetCore.Components.E2ETest;

internal static class WebDriverExtensions
{
private static string GetFindPositionScript(string elementId) =>
$"return Math.round(document.getElementById('{elementId}').getBoundingClientRect().top + window.scrollY);";

public static void Navigate(this IWebDriver browser, Uri baseUri, string relativeUrl)
{
var absoluteUrl = new Uri(baseUri, relativeUrl);
Expand Down Expand Up @@ -45,27 +41,33 @@ public static void WaitForElementToBeVisible(this IWebDriver browser, By by, int

public static long GetElementPositionWithRetry(this IWebDriver browser, string elementId, int retryCount = 3, int delayBetweenRetriesMs = 100)
{
var jsExecutor = (IJavaScriptExecutor)browser;
string script = GetFindPositionScript(elementId);
browser.WaitForElementToBeVisible(By.Id(elementId));
string log = "";

for (int i = 0; i < retryCount; i++)
{
try
{
var result = jsExecutor.ExecuteScript(script);
if (result != null)
{
return (long)result;
}
browser.WaitForElementToBeVisible(By.Id(elementId));
var element = browser.FindElement(By.Id(elementId));
var elementLocation = element.Location.Y;

// Get scroll position using JavaScript (this is less likely to fail than element positioning)
var jsExecutor = (IJavaScriptExecutor)browser;
var scrollY = (long)jsExecutor.ExecuteScript("return window.scrollY");

return elementLocation + scrollY;
}
catch (OpenQA.Selenium.JavaScriptException)
catch (Exception ex)
{
// JavaScript execution failed, retry
log += $"Attempt {i + 1}: - {ex.Message}. ";
}

Thread.Sleep(delayBetweenRetriesMs);
if (i < retryCount - 1)
{
Thread.Sleep(delayBetweenRetriesMs);
}
}

throw new Exception($"Failed to execute script after {retryCount} retries.");
throw new Exception($"Failed to get position for element '{elementId}' after {retryCount} retries. Debug log: {log}");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void ComponentMethods_HaveCircuitContext_OnInitialPageLoad()
// Internal for reuse in Blazor Web tests
internal static void TestCircuitContextCore(IWebDriver browser)
{
browser.Equal("Circuit Context", () => browser.Exists(By.TagName("h1")).Text);
browser.Equal("Circuit Context", () => browser.Exists(By.Id("circuit-context-title")).Text);

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase);
Browser.MountTestComponent<GracefulTermination>();
Browser.Equal("Graceful Termination", () => Browser.Exists(By.TagName("h1")).Text);
Browser.Equal("Graceful Termination", () => Browser.Exists(By.Id("graceful-termination-title")).Text);

GracefulDisconnectCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
Sink = _serverFixture.Host.Services.GetRequiredService<TestSink>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ public void RefreshCanFallBackOnFullPageReload(string renderMode)
Browser.Navigate().Refresh();
Browser.Equal("Page with interactive components that navigate", () => Browser.Exists(By.TagName("h1")).Text);

// if we don't clean up the suppression, all subsequent navigations will be suppressed by default
EnhancedNavigationTestUtil.CleanEnhancedNavigationSuppression(this, skipNavigation: true);

// Normally, you shouldn't store references to elements because they could become stale references
// after the page re-renders. However, we want to explicitly test that the element becomes stale
// across renders to ensure that a full page reload occurs.
Expand Down Expand Up @@ -677,11 +680,10 @@ public void CanUpdateHrefOnLinkTagWithIntegrity()
}

[Theory]
[QuarantinedTest("https://github.com/dotnet/aspnetcore/issues/60875")]
// [InlineData(false, false, false)] // https://github.com/dotnet/aspnetcore/issues/60875
[InlineData(false, false, false)]
[InlineData(false, true, false)]
[InlineData(true, true, false)]
// [InlineData(true, false, false)] // https://github.com/dotnet/aspnetcore/issues/60875
[InlineData(true, false, false)]
// [InlineData(false, false, true)] programmatic navigation doesn't work without enhanced navigation
[InlineData(false, true, true)]
[InlineData(true, true, true)]
Expand All @@ -692,8 +694,8 @@ public void EnhancedNavigationScrollBehavesSameAsBrowserOnNavigation(bool enable
// or to the beginning of a fragment, regardless of the previous scroll position
string landingPageSuffix = enableStreaming ? "" : "-no-streaming";
string buttonKeyword = programmaticNavigation ? "-programmatic" : "";
EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, shouldSuppress: !useEnhancedNavigation);
Navigate($"{ServerPathBase}/nav/scroll-test{landingPageSuffix}");
EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, shouldSuppress: !useEnhancedNavigation, skipNavigation: true);

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

[Theory]
// [InlineData(false, false, false)] // https://github.com/dotnet/aspnetcore/issues/60875
[InlineData(false, false, false)]
[InlineData(false, true, false)]
[InlineData(true, true, false)]
// [InlineData(true, false, false)] // https://github.com/dotnet/aspnetcore/issues/60875
[InlineData(true, false, false)]
// [InlineData(false, false, true)] programmatic navigation doesn't work without enhanced navigation
[InlineData(false, true, true)]
[InlineData(true, true, true)]
Expand All @@ -745,8 +747,8 @@ public void EnhancedNavigationScrollBehavesSameAsBrowserOnBackwardsForwardsActio
// This test checks if the scroll position is preserved after backwards/forwards action
string landingPageSuffix = enableStreaming ? "" : "-no-streaming";
string buttonKeyword = programmaticNavigation ? "-programmatic" : "";
EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, shouldSuppress: !useEnhancedNavigation);
Navigate($"{ServerPathBase}/nav/scroll-test{landingPageSuffix}");
EnhancedNavigationTestUtil.SuppressEnhancedNavigation(this, shouldSuppress: !useEnhancedNavigation, skipNavigation: true);

// "landing" page: scroll to pos1, navigate away
AssertWeAreOnLandingPage();
Expand Down Expand Up @@ -831,6 +833,8 @@ private void AssertScrollPositionCorrect(bool useEnhancedNavigation, long previo
private void AssertEnhancedNavigation(bool useEnhancedNavigation, IWebElement elementForStalenessCheck, int retryCount = 3, int delayBetweenRetriesMs = 1000)
{
bool enhancedNavigationDetected = false;
string logging = "";
string isNavigationSuppressed = "";
for (int i = 0; i < retryCount; i++)
{
try
Expand All @@ -841,28 +845,43 @@ private void AssertEnhancedNavigation(bool useEnhancedNavigation, IWebElement el
}
catch (XunitException)
{
var logs = Browser.GetBrowserLogs(LogLevel.Warning);
logging += $"{string.Join(", ", logs.Select(l => l.Message))}\n";

var testId = ((IJavaScriptExecutor)Browser).ExecuteScript("return sessionStorage.getItem('test-id');");
logging += $" testId: {testId}\n";
if (testId is null)
{
continue;
}
var suppressKey = $"suppress-enhanced-navigation-{testId}";

var enhancedNavAttached = ((IJavaScriptExecutor)Browser).ExecuteScript("return sessionStorage.getItem('blazor-enhanced-nav-attached');");
isNavigationSuppressed = (string)((IJavaScriptExecutor)Browser).ExecuteScript($"return sessionStorage.getItem('{suppressKey}');");

logging += $" suppressKey: {suppressKey}\n";
logging += $" {suppressKey}: {isNavigationSuppressed}\n";
// Maybe the check was done too early to change the DOM ref, retry
}

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

private void AssertWeAreOnLandingPage()
{
string infoName = "test-info-1";
Browser.WaitForElementToBeVisible(By.Id(infoName), timeoutInSeconds: 20);
Browser.WaitForElementToBeVisible(By.Id(infoName), timeoutInSeconds: 30);
Browser.Equal("Scroll tests landing page", () => Browser.Exists(By.Id(infoName)).Text);
}

private void AssertWeAreOnNextPage()
{
string infoName = "test-info-2";
Browser.WaitForElementToBeVisible(By.Id(infoName), timeoutInSeconds: 20);
Browser.WaitForElementToBeVisible(By.Id(infoName), timeoutInSeconds: 30);
Browser.Equal("Scroll tests next page", () => Browser.Exists(By.Id(infoName)).Text);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,100 @@ public static void SuppressEnhancedNavigation<TServerFixture>(ServerTestBase<TSe

if (!skipNavigation)
{
// Normally we need to navigate here first otherwise the browser isn't on the correct origin to access
// localStorage. But some tests are already in the right place and need to avoid extra navigation.
fixture.Navigate($"{fixture.ServerPathBase}/");
browser.Equal("Hello", () => browser.Exists(By.TagName("h1")).Text);
NavigateToOrigin(fixture);
}
AssertSessionStorageAvailable(browser);

((IJavaScriptExecutor)browser).ExecuteScript("sessionStorage.setItem('suppress-enhanced-navigation', 'true')");
var testId = ((IJavaScriptExecutor)browser).ExecuteScript($"return sessionStorage.getItem('test-id')");
if (testId is null || string.IsNullOrEmpty(testId as string))
{
throw new InvalidOperationException("Test ID not found in sessionStorage. Make sure your test class grants it in InitializeAsync.");
}

((IJavaScriptExecutor)browser).ExecuteScript($"sessionStorage.setItem('suppress-enhanced-navigation-{testId}', 'true')");

var suppressEnhancedNavigation = ((IJavaScriptExecutor)browser).ExecuteScript($"return sessionStorage.getItem('suppress-enhanced-navigation-{testId}');");
Assert.True(suppressEnhancedNavigation is not null && (string)suppressEnhancedNavigation == "true",
"Expected 'suppress-enhanced-navigation' to be set in sessionStorage.");
}
}

public static void AssertSessionStorageAvailable(IWebDriver browser)
{
try
{
((IJavaScriptExecutor)browser).ExecuteScript("sessionStorage.length");
}
catch (Exception ex)
{
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);
}

}

public static void GrantTestId<TServerFixture>(ServerTestBase<TServerFixture> fixture)
where TServerFixture : ServerFixture
{
NavigateToOrigin(fixture);
AssertSessionStorageAvailable(fixture.Browser);
GrantTestIdCore(fixture.Browser);
}

public static void CleanEnhancedNavigationSuppression<TServerFixture>(ServerTestBase<TServerFixture> fixture, bool skipNavigation = false)
where TServerFixture : ServerFixture
{
var browser = fixture.Browser;

try
{
// First, ensure we're on the correct origin to access sessionStorage
try
{
// Check if we can access sessionStorage from current location
((IJavaScriptExecutor)browser).ExecuteScript("sessionStorage.length");
}
catch
{
if (skipNavigation)
{
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.");
}
NavigateToOrigin(fixture);
}

var testId = ((IJavaScriptExecutor)browser).ExecuteScript($"return sessionStorage.getItem('test-id')");
if (testId is null || string.IsNullOrEmpty(testId as string))
{
return;
}

((IJavaScriptExecutor)browser).ExecuteScript($"sessionStorage.removeItem('test-id')");
((IJavaScriptExecutor)browser).ExecuteScript($"sessionStorage.removeItem('suppress-enhanced-navigation-{testId}')");
}
catch (WebDriverException ex) when (ex.Message.Contains("invalid session id"))
{
// Browser session is no longer valid (e.g., browser was closed)
// Session storage is automatically cleared when browser closes, so cleanup is already done
// This is expected in some tests, so we silently return
return;
}
}

private static void NavigateToOrigin<TServerFixture>(ServerTestBase<TServerFixture> fixture)
where TServerFixture : ServerFixture
{
// Navigate to the test origin to ensure the browser is on the correct state to access sessionStorage
fixture.Navigate($"{fixture.ServerPathBase}/");
fixture.Browser.Exists(By.Id("session-storage-anchor"));
}

private static string GrantTestIdCore(IWebDriver browser)
{
var testId = Guid.NewGuid().ToString("N")[..8];
((IJavaScriptExecutor)browser).ExecuteScript($"sessionStorage.setItem('test-id', '{testId}')");
return testId;
}

public static long GetScrollY(this IWebDriver browser)
=> Convert.ToInt64(((IJavaScriptExecutor)browser).ExecuteScript("return window.scrollY"), CultureInfo.CurrentCulture);

Expand Down
8 changes: 6 additions & 2 deletions src/Components/test/E2ETest/Tests/BinaryHttpClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
// The .NET Foundation licenses this file to you under the MIT license.

using BasicTestApp.HttpClientTest;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using TestServer;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Components.E2ETest.Tests;

public class BinaryHttpClientTest : BrowserTestBase,
public class BinaryHttpClientTest : ServerTestBase<AspNetSiteServerFixture>,
IClassFixture<BasicTestAppServerSiteFixture<CorsStartup>>,
IClassFixture<BlazorWasmTestAppFixture<BasicTestApp.Program>>
{
Expand All @@ -23,11 +25,13 @@ public class BinaryHttpClientTest : BrowserTestBase,

public BinaryHttpClientTest(
BrowserFixture browserFixture,
AspNetSiteServerFixture serverFixture,
BlazorWasmTestAppFixture<BasicTestApp.Program> devHostServerFixture,
BasicTestAppServerSiteFixture<CorsStartup> apiServerFixture,
ITestOutputHelper output)
: base(browserFixture, output)
: base(browserFixture, serverFixture, output)
{
serverFixture.BuildWebHostMethod = Program.BuildWebHost<CorsStartup>;
_devHostServerFixture = devHostServerFixture;
_devHostServerFixture.PathBase = "/subdir";
_apiServerFixture = apiServerFixture;
Expand Down
Loading
Loading