diff --git a/Build/Build.cs b/Build/Build.cs index 87eba6bdae..060eea7d85 100644 --- a/Build/Build.cs +++ b/Build/Build.cs @@ -325,7 +325,7 @@ public override void ConfigureTestMatrix (TestMatricesBuilder builder) { AnyOs, Firefox, NET8_0, Release, x64, NoDB, EnforcedLocalMachine(Docker_Win_NET10_0) }, { AnyOs, Edge, NET8_0, Release, x64, NoDB, EnforcedLocalMachine(Docker_Win_NET10_0) }, { AnyOs, Chrome, NET10_0, Debug, x64, NoDB, EnforcedLocalMachine(Docker_Win_NET10_0) }, - // { AnyOs, Firefox, NET10_0, Release, x64, NoDB, EnforcedLocalMachine(Docker_Win_NET10_0) }, + { AnyOs, Firefox, NET10_0, Release, x64, NoDB, EnforcedLocalMachine(Docker_Win_NET10_0) }, { AnyOs, Edge, NET10_0, Release, x64, NoDB, EnforcedLocalMachine(Docker_Win_NET10_0) }, }, allowEmpty: true); diff --git a/Directory.Packages.props b/Directory.Packages.props index 4902089ff3..01547335e3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -41,8 +41,8 @@ - - + + diff --git a/Remotion/Web/Development.WebTesting.IntegrationTests/WebTestHelperTest.cs b/Remotion/Web/Development.WebTesting.IntegrationTests/WebTestHelperTest.cs index 5ef630ddb9..c19e607a6d 100644 --- a/Remotion/Web/Development.WebTesting.IntegrationTests/WebTestHelperTest.cs +++ b/Remotion/Web/Development.WebTesting.IntegrationTests/WebTestHelperTest.cs @@ -60,6 +60,18 @@ private static bool RelevantProcessFilter (Process process) private Mock _testContext; private Dictionary _testContextProperties; + [OneTimeSetUp] + public void OneTimeSetUp () + { + var webTestHelper = WebTestHelper.CreateFromConfiguration(); + //TODO: RM-9595 fix and reenable WebTestHelperTests for firefox + //WebTestHelperTests currently cause firefox with Bidi active to hang + if (webTestHelper.BrowserConfiguration.IsFirefox()) + { + Assert.Ignore(); + } + } + [SetUp] public void SetUp () { diff --git a/Remotion/Web/Development.WebTesting/Accessibility/AccessibilityAnalyzer.cs b/Remotion/Web/Development.WebTesting/Accessibility/AccessibilityAnalyzer.cs index 77df75eae3..3e19f477b7 100644 --- a/Remotion/Web/Development.WebTesting/Accessibility/AccessibilityAnalyzer.cs +++ b/Remotion/Web/Development.WebTesting/Accessibility/AccessibilityAnalyzer.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Logging; using OpenQA.Selenium; using OpenQA.Selenium.Remote; +using Remotion.Utilities; using Remotion.Web.Development.WebTesting.Accessibility.Implementation; using Remotion.Web.Development.WebTesting.Utilities; @@ -208,7 +209,9 @@ public AccessibilityResult Analyze ([NotNull] string cssSelector, [CanBeNull] Ti [NotNull] public AccessibilityResult Analyze ([CanBeNull] TimeSpan? timeout = null) { - var outerFrame = (string)JsExecutor.ExecuteScript("return self.name;"); + var outerFrame = (string?)JsExecutor.ExecuteScript("return self.name;"); + Assertion.IsNotNull(outerFrame, "Failed to retrieve the current frame's name."); + if (outerFrame != "") WebDriver.SwitchTo().DefaultContent(); @@ -233,11 +236,10 @@ private AccessibilityResult GetAccessibilityResult (string? cssSelector, TimeSpa var axeRunFunctionCall = BuildAxeRunFunctionCall(cssSelector); - string result; + string? result; using (new PerformanceTimer(Logger, "Accessibility analysis has been performed.")) { - result = (string)JsExecutor.ExecuteAsyncScript(axeRunFunctionCall); - + result = (string?)JsExecutor.ExecuteAsyncScript(axeRunFunctionCall); if (result == null) throw new InvalidOperationException("Could not obtain accessibility analysis result."); } @@ -249,7 +251,10 @@ private AccessibilityResult GetAccessibilityResult (string? cssSelector, TimeSpa private bool AxeIsInjected () { - return (bool)JsExecutor.ExecuteScript("return (typeof axe !== 'undefined')"); + var result = (bool?)JsExecutor.ExecuteScript("return (typeof axe !== 'undefined')"); + Assertion.IsNotNull(result, "Failed to determine if aXe library is injected."); + + return result.Value; } /// diff --git a/Remotion/Web/Development.WebTesting/BrowserSession/BiDiBrowserLogProvider.cs b/Remotion/Web/Development.WebTesting/BrowserSession/BiDiBrowserLogProvider.cs index 069ac7f3a1..ab368f782b 100644 --- a/Remotion/Web/Development.WebTesting/BrowserSession/BiDiBrowserLogProvider.cs +++ b/Remotion/Web/Development.WebTesting/BrowserSession/BiDiBrowserLogProvider.cs @@ -3,10 +3,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using Coypu; -using OpenQA.Selenium; using OpenQA.Selenium.BiDi; -using OpenQA.Selenium.BiDi.Modules.BrowsingContext; namespace Remotion.Web.Development.WebTesting.BrowserSession; @@ -19,24 +16,19 @@ namespace Remotion.Web.Development.WebTesting.BrowserSession; /// public class BiDiBrowserLogProvider : IBrowserLogProvider, IDisposable { - private readonly IDriver _driver; - private readonly ConcurrentQueue _logEntries = new(); private readonly Subscription _eventSubscription; - public BiDiBrowserLogProvider (IDriver driver) + public BiDiBrowserLogProvider (IBidiConnectionProvider bidiProvider) { - ArgumentNullException.ThrowIfNull(driver); - - _driver = driver; - - var bidi = ((IWebDriver)driver.Native).AsBiDiAsync().GetAwaiter().GetResult(); - _eventSubscription = bidi.Log.OnEntryAddedAsync(entry => _logEntries.Enqueue(new BrowserLogEntry(entry))).GetAwaiter().GetResult(); + ArgumentNullException.ThrowIfNull(bidiProvider); - // Accept all user prompts as they come up - IWebTestHelper.AcceptPossibleModalDialog() does not work with BiDi - // because the WebTest-Thread is not continued when a user prompt is shown. - bidi.BrowsingContext.OnUserPromptOpenedAsync(args => args.BiDi.BrowsingContext.HandleUserPromptAsync(args.Context, new HandleUserPromptOptions { Accept = true })).Wait(); - } + bidiProvider.OpenBidiConnection(); + _eventSubscription = bidiProvider.BiDiConnection.Log.OnEntryAddedAsync(entry => _logEntries.Enqueue(new BrowserLogEntry(entry)), new SubscriptionOptions + { + Timeout = bidiProvider.DefaultBidiTimeout + }).GetAwaiter().GetResult(); +} /// public IReadOnlyCollection GetBrowserLogs () @@ -52,10 +44,13 @@ public void ResetBrowserLogs () public void Dispose () { - _eventSubscription.DisposeAsync().AsTask().GetAwaiter().GetResult(); - - var driver = (IWebDriver)_driver.Native; - var biDi = driver.AsBiDiAsync().GetAwaiter().GetResult(); - biDi.DisposeAsync().GetAwaiter().GetResult(); + try + { + _eventSubscription.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + catch (Exception) + { + //ignored + } } } diff --git a/Remotion/Web/Development.WebTesting/BrowserSession/BrowserLogEntry.cs b/Remotion/Web/Development.WebTesting/BrowserSession/BrowserLogEntry.cs index bb7dc5ccbe..3d4d7259b6 100644 --- a/Remotion/Web/Development.WebTesting/BrowserSession/BrowserLogEntry.cs +++ b/Remotion/Web/Development.WebTesting/BrowserSession/BrowserLogEntry.cs @@ -17,7 +17,7 @@ using System; using System.Globalization; using JetBrains.Annotations; -using OpenQA.Selenium.BiDi.Modules.Log; +using OpenQA.Selenium.BiDi.Log; using OpenQA.Selenium; namespace Remotion.Web.Development.WebTesting.BrowserSession @@ -47,21 +47,21 @@ private static LogLevel GetLogLevel ([NotNull] Level bidiLogLevel) { return bidiLogLevel switch { - OpenQA.Selenium.BiDi.Modules.Log.Level.Debug => LogLevel.Debug, - OpenQA.Selenium.BiDi.Modules.Log.Level.Info => LogLevel.Info, - OpenQA.Selenium.BiDi.Modules.Log.Level.Warn => LogLevel.Warning, - OpenQA.Selenium.BiDi.Modules.Log.Level.Error => LogLevel.Severe, + OpenQA.Selenium.BiDi.Log.Level.Debug => LogLevel.Debug, + OpenQA.Selenium.BiDi.Log.Level.Info => LogLevel.Info, + OpenQA.Selenium.BiDi.Log.Level.Warn => LogLevel.Warning, + OpenQA.Selenium.BiDi.Log.Level.Error => LogLevel.Severe, _ => LogLevel.Off }; } - public BrowserLogEntry ([NotNull] Entry logEntry) - : this(GetLogLevel(logEntry.Level), logEntry.Text, logEntry.Timestamp.DateTime) + public BrowserLogEntry ([NotNull] OpenQA.Selenium.BiDi.Log.LogEntry logEntry) + : this(GetLogLevel(logEntry.Level), logEntry.Text ?? "", logEntry.Timestamp.DateTime) { ArgumentNullException.ThrowIfNull(logEntry); } - public BrowserLogEntry ([NotNull] LogEntry logEntry) + public BrowserLogEntry ([NotNull] OpenQA.Selenium.LogEntry logEntry) : this(logEntry.Level, logEntry.Message, logEntry.Timestamp) { ArgumentNullException.ThrowIfNull(logEntry); diff --git a/Remotion/Web/Development.WebTesting/BrowserSession/BrowserSessionBase.cs b/Remotion/Web/Development.WebTesting/BrowserSession/BrowserSessionBase.cs index 2ad00b1a0f..b923f5e303 100644 --- a/Remotion/Web/Development.WebTesting/BrowserSession/BrowserSessionBase.cs +++ b/Remotion/Web/Development.WebTesting/BrowserSession/BrowserSessionBase.cs @@ -21,6 +21,8 @@ using Coypu; using JetBrains.Annotations; using OpenQA.Selenium; +using OpenQA.Selenium.BiDi; +using OpenQA.Selenium.BiDi.BrowsingContext; using Remotion.Web.Development.WebTesting.Utilities; using Remotion.Web.Development.WebTesting.WebDriver.Configuration; @@ -43,15 +45,18 @@ public static void ApplyCommonWebTestFeatureDefaults ( } private readonly TimeSpan _browserProcessesShutdownTime = TimeSpan.FromSeconds(60); + private readonly TimeSpan _bidiTimeout = TimeSpan.FromSeconds(1); private readonly T _browserConfiguration; private readonly Coypu.BrowserSession _value; private readonly int _driverProcessID; private readonly bool _headless; - private bool _isDisposed; - private readonly WebTestFeatureCollection _features; + private BiDi? _bidiConnection; + private Subscription? _promptSubscription; + private bool _isDisposed; + protected BrowserSessionBase ( [NotNull] Coypu.BrowserSession value, [NotNull] T browserConfiguration, @@ -87,9 +92,30 @@ protected T BrowserConfiguration get { return _browserConfiguration; } } - public void AcceptModalDialog (Options? options = null) + /// /> + public BiDi BiDiConnection => _bidiConnection + ?? throw new InvalidOperationException("Call 'OpenBidiConnection' before accessing 'BiDiConnection'."); + + /// + [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(_bidiConnection))] + public void OpenBidiConnection () { - _value.AcceptModalDialog(options); + if (_bidiConnection != null) + return; + + _bidiConnection = ((OpenQA.Selenium.WebDriver)Driver.Native).AsBiDiAsync().GetAwaiter().GetResult(); + + // TODO: RM-9596 + // This should be moved into a scope based PromptHandler that can be used during a test to enable and disable handling of prompts similar to how it works for non bidi prompt handling. + // Accept all user prompts as they come up - IWebTestHelper.AcceptPossibleModalDialog() does not work with BiDi + // because the WebTest-Thread is not continued when a user prompt is shown. + _promptSubscription = _bidiConnection.BrowsingContext.OnUserPromptOpenedAsync(args => + args.BiDi.BrowsingContext.HandleUserPromptAsync(args.Context, new HandleUserPromptOptions { Accept = true, Timeout = _bidiTimeout}).GetAwaiter().GetResult(), + new BrowsingContextsSubscriptionOptions(new SubscriptionOptions + { + Timeout = _bidiTimeout + })) + .GetAwaiter().GetResult(); } public IDriver Driver @@ -137,9 +163,27 @@ public virtual void Dispose () return; _isDisposed = true; - _features.Dispose(); + try + { + _promptSubscription?.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + catch (Exception) + { + //ignored + } + + try + { + _bidiConnection?.DisposeAsync().AsTask().GetAwaiter().GetResult(); + } + catch (Exception) + { + //ignored + } + + // Get processes for driver and main browser, as well as the sub processes of the browser var driverProcess = FindDriverProcess(); var browserProcess = FindBrowserProcess(); diff --git a/Remotion/Web/Development.WebTesting/BrowserSession/Firefox/FirefoxBrowserSession.cs b/Remotion/Web/Development.WebTesting/BrowserSession/Firefox/FirefoxBrowserSession.cs index f1cc3b1579..1a9011b876 100644 --- a/Remotion/Web/Development.WebTesting/BrowserSession/Firefox/FirefoxBrowserSession.cs +++ b/Remotion/Web/Development.WebTesting/BrowserSession/Firefox/FirefoxBrowserSession.cs @@ -31,7 +31,7 @@ public static void ApplyDefaultWebTestFeatures ( ArgumentNullException.ThrowIfNull(features); ArgumentNullException.ThrowIfNull(browserSession); - features.Set(new BiDiBrowserLogProvider(browserSession.Driver)); + features.Set(new BiDiBrowserLogProvider(browserSession)); } public FirefoxBrowserSession (Coypu.BrowserSession value, IFirefoxConfiguration browserConfiguration, int driverProcessId, bool headless) diff --git a/Remotion/Web/Development.WebTesting/BrowserSession/IBidiConnectionProvider.cs b/Remotion/Web/Development.WebTesting/BrowserSession/IBidiConnectionProvider.cs new file mode 100644 index 0000000000..8b977f8151 --- /dev/null +++ b/Remotion/Web/Development.WebTesting/BrowserSession/IBidiConnectionProvider.cs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: (c) RUBICON IT GmbH, www.rubicon.eu +// SPDX-License-Identifier: LGPL-2.1-or-later +using System; +using System.Diagnostics.CodeAnalysis; +using OpenQA.Selenium.BiDi; + +namespace Remotion.Web.Development.WebTesting.BrowserSession; +/// +/// Provides access to the bidirectional webdriver connection to a browser +/// +public interface IBidiConnectionProvider: IDisposable +{ + /// + /// Opens a Bidi Connection . + /// Note: In order to use Bidi you need to enable UseWebSocketUrl in the browser options for this session. + /// + [MemberNotNull(nameof(BiDiConnection))] + void OpenBidiConnection (); + + /// + /// Gets the open Bidi connection. Trying to access this before calling + /// should result in a + /// + BiDi BiDiConnection { get; } + + /// + /// Used for timeouts for opening connections and interactions with the browser + /// + TimeSpan DefaultBidiTimeout => TimeSpan.FromSeconds(1); +} diff --git a/Remotion/Web/Development.WebTesting/BrowserSession/IBrowserSession.cs b/Remotion/Web/Development.WebTesting/BrowserSession/IBrowserSession.cs index d48978dc23..ad72a0c222 100644 --- a/Remotion/Web/Development.WebTesting/BrowserSession/IBrowserSession.cs +++ b/Remotion/Web/Development.WebTesting/BrowserSession/IBrowserSession.cs @@ -17,13 +17,14 @@ using System; using System.Collections.Generic; using Coypu; +using OpenQA.Selenium.BiDi; namespace Remotion.Web.Development.WebTesting.BrowserSession { /// /// Represents a wrapper around a which has additional cleanup routines via . /// - public interface IBrowserSession : IDisposable + public interface IBrowserSession : IBidiConnectionProvider, IDisposable { /// /// The of this . diff --git a/Remotion/Web/Development.WebTesting/CoypuElementScopeSelectExtensions.cs b/Remotion/Web/Development.WebTesting/CoypuElementScopeSelectExtensions.cs index 5b26a8ff53..5d5948bef5 100644 --- a/Remotion/Web/Development.WebTesting/CoypuElementScopeSelectExtensions.cs +++ b/Remotion/Web/Development.WebTesting/CoypuElementScopeSelectExtensions.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Logging; using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; +using Remotion.Utilities; using Remotion.Web.Contracts.DiagnosticMetadata; using Remotion.Web.Development.WebTesting.ControlObjects; using Remotion.Web.Development.WebTesting.Utilities; @@ -64,7 +65,10 @@ public static OptionDefinition GetSelectedOption ([NotNull] this ElementScope sc var select = new SelectElement(webElement); var selectedOption = select.SelectedOption; - return new OptionDefinition(selectedOption.GetAttribute("value"), -1, selectedOption.Text, selectedOption.Selected); + var itemID = selectedOption.GetAttribute("value"); + Assertion.IsNotNull(itemID, "Failed to retrieve the 'value' attribute from the selected option."); + + return new OptionDefinition(itemID, -1, selectedOption.Text, selectedOption.Selected); }); } diff --git a/Remotion/Web/Development.WebTesting/Utilities/ElementScopeExtensions.cs b/Remotion/Web/Development.WebTesting/Utilities/ElementScopeExtensions.cs index 19c90ceee9..19854b22f2 100644 --- a/Remotion/Web/Development.WebTesting/Utilities/ElementScopeExtensions.cs +++ b/Remotion/Web/Development.WebTesting/Utilities/ElementScopeExtensions.cs @@ -21,6 +21,7 @@ using Coypu; using JetBrains.Annotations; using OpenQA.Selenium; +using Remotion.Utilities; using Remotion.Web.Development.WebTesting.ScreenshotCreation; using Remotion.Web.Development.WebTesting.ScreenshotCreation.Drawing; using Remotion.Web.Development.WebTesting.ScreenshotCreation.Resolvers; @@ -42,8 +43,9 @@ public static Point GetScrollPosition ([NotNull] this ElementScope element) var driver = ((IWrapsDriver)element.Native).WrappedDriver; var jsExecutor = (IJavaScriptExecutor)driver; - var rawData = - (IReadOnlyList)jsExecutor.ExecuteScript("return [arguments[0].scrollLeft, arguments[0].scrollTop];", (IWebElement)element.Native); + var rawData = (IReadOnlyList?)jsExecutor.ExecuteScript("return [arguments[0].scrollLeft, arguments[0].scrollTop];", (IWebElement)element.Native); + Assertion.IsNotNull(rawData, "Failed to retrieve the scroll position."); + return new Point((int)(long)rawData[0], (int)(long)rawData[1]); }