diff --git a/Directory.Packages.props b/Directory.Packages.props
index eadc39680..8c5d1dda9 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -12,7 +12,7 @@
-
+
diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Argument.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Argument.cs
index 740a9ae22..a421e56c3 100644
--- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Argument.cs
+++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Argument.cs
@@ -6,7 +6,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using OpenQA.Selenium;
namespace Microsoft.DotNet.XHarness.CLI.CommandArguments;
@@ -284,32 +283,6 @@ public override void Action(string argumentValue)
public override string ToString() => Value ? "true" : "false";
}
-public abstract class EnumPageLoadStrategyArgument : Argument
-{
- private readonly PageLoadStrategy _defaultValue;
-
- public EnumPageLoadStrategyArgument(string prototype, string description, PageLoadStrategy defaultValue)
- : base(prototype, description, defaultValue)
- {
- _defaultValue = defaultValue;
- }
-
- public override void Action(string argumentValue)
- {
- if (string.IsNullOrEmpty(argumentValue))
- {
- Value = _defaultValue;
- }
- else
- {
- Value = argumentValue.Equals("none", StringComparison.OrdinalIgnoreCase) ? PageLoadStrategy.None :
- argumentValue.Equals("eager", StringComparison.OrdinalIgnoreCase) ? PageLoadStrategy.Eager :
- argumentValue.Equals("normal", StringComparison.OrdinalIgnoreCase) ? PageLoadStrategy.Normal :
- _defaultValue;
- }
- }
-}
-
public abstract class RepeatableArgument : Argument>
{
private readonly List _values = new();
diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/Arguments/PageLoadStrategyArgument.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/Arguments/PageLoadStrategyArgument.cs
deleted file mode 100644
index 8a7a29356..000000000
--- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/Arguments/PageLoadStrategyArgument.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-namespace Microsoft.DotNet.XHarness.CLI.CommandArguments.Wasm;
-
-using OpenQA.Selenium;
-using System;
-
-internal class PageLoadStrategyArgument : EnumPageLoadStrategyArgument
-{
- private const string HelpMessage =
- $@"Decides how long WebDriver will hold off on completing a navigation method.
- NORMAL (default): Does not block WebDriver at all. Ready state: complete.
- EAGER: DOM access is ready, but other resources like images may still be loading. Ready state: interactive.
- NONE: Does not block WebDriver at all. Ready state: any.";
-
- public PageLoadStrategyArgument(PageLoadStrategy defaultValue)
- : base("pageLoadStrategy=", HelpMessage, defaultValue)
- {}
-}
diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs
index 1d980c118..bc3f4d92a 100644
--- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs
+++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestBrowserCommandArguments.cs
@@ -26,7 +26,6 @@ internal class WasmTestBrowserCommandArguments : XHarnessCommandArguments, IWebS
public NoQuitArgument NoQuit { get; } = new();
public BackgroundThrottlingArgument BackgroundThrottling { get; } = new();
public LocaleArgument Locale { get; } = new("en-US");
- public PageLoadStrategyArgument PageLoadStrategy { get; } = new(OpenQA.Selenium.PageLoadStrategy.Normal);
public SymbolMapFileArgument SymbolMapFileArgument { get; } = new();
public SymbolicatePatternsFileArgument SymbolicatePatternsFileArgument { get; } = new();
@@ -58,7 +57,6 @@ internal class WasmTestBrowserCommandArguments : XHarnessCommandArguments, IWebS
NoQuit,
BackgroundThrottling,
Locale,
- PageLoadStrategy,
SymbolMapFileArgument,
SymbolicatePatternsFileArgument,
SymbolicatorArgument,
@@ -75,16 +73,16 @@ public override void Validate()
{
base.Validate();
- if (!string.IsNullOrEmpty(BrowserLocation))
+ if (!string.IsNullOrEmpty(BrowserLocation.Value))
{
if (Browser == Wasm.Browser.Safari)
{
throw new ArgumentException("Safari driver doesn't support custom browser path");
}
- if (!File.Exists(BrowserLocation))
+ if (!File.Exists(BrowserLocation.Value))
{
- throw new ArgumentException($"Could not find browser at {BrowserLocation}");
+ throw new ArgumentException($"Could not find browser at {BrowserLocation.Value}");
}
}
diff --git a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs
index 20675e172..d25a027b5 100644
--- a/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs
+++ b/src/Microsoft.DotNet.XHarness.CLI/CommandArguments/WASM/WasmTestCommandArguments.cs
@@ -18,7 +18,6 @@ internal class WasmTestCommandArguments : XHarnessCommandArguments, IWebServerAr
public OutputDirectoryArgument OutputDirectory { get; } = new();
public TimeoutArgument Timeout { get; } = new(TimeSpan.FromMinutes(15));
public LocaleArgument Locale { get; } = new("en-US");
- public PageLoadStrategyArgument PageLoadStrategy { get; } = new(OpenQA.Selenium.PageLoadStrategy.Normal);
public SymbolMapFileArgument SymbolMapFileArgument { get; } = new();
public SymbolicatePatternsFileArgument SymbolicatePatternsFileArgument { get; } = new();
@@ -44,7 +43,6 @@ internal class WasmTestCommandArguments : XHarnessCommandArguments, IWebServerAr
Timeout,
ExpectedExitCode,
Locale,
- PageLoadStrategy,
SymbolMapFileArgument,
SymbolicatePatternsFileArgument,
SymbolicatorArgument,
diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/PlaywrightBrowserWrapper.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/PlaywrightBrowserWrapper.cs
new file mode 100644
index 000000000..4b6134550
--- /dev/null
+++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/PlaywrightBrowserWrapper.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Playwright;
+
+namespace Microsoft.DotNet.XHarness.CLI.Commands.Wasm;
+
+///
+/// Wrapper around Playwright IPage to provide Selenium-like interface for easier migration
+///
+internal class PlaywrightBrowserWrapper : IDisposable
+{
+ private readonly IPage _page;
+ private readonly IBrowser _browser;
+ private readonly IPlaywright _playwright;
+
+ public PlaywrightBrowserWrapper(IPage page, IBrowser browser, IPlaywright playwright)
+ {
+ _page = page;
+ _browser = browser;
+ _playwright = playwright;
+ }
+
+ public IPage Page => _page;
+
+ public async Task NavigateToUrlAsync(string url)
+ {
+ await _page.GotoAsync(url);
+ }
+
+ public async Task FindElementTextAsync(string selector)
+ {
+ var element = await _page.WaitForSelectorAsync(selector, new PageWaitForSelectorOptions
+ {
+ Timeout = 30000
+ });
+ return await element!.InnerTextAsync();
+ }
+
+ public void Dispose()
+ {
+ _page?.CloseAsync().Wait();
+ _browser?.CloseAsync().Wait();
+ _playwright?.Dispose();
+ }
+}
+
+///
+/// Service wrapper to mimic Selenium DriverService behavior
+///
+internal class PlaywrightServiceWrapper : IDisposable
+{
+ private readonly IBrowser _browser;
+ public bool IsRunning => !_browser.IsConnected || _browser.IsConnected;
+
+ public PlaywrightServiceWrapper(IBrowser browser)
+ {
+ _browser = browser;
+ }
+
+ public void Dispose()
+ {
+ _browser?.CloseAsync().Wait();
+ }
+}
diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmBrowserTestRunner.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmBrowserTestRunner.cs
index fce046a6b..5a6cddfbe 100644
--- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmBrowserTestRunner.cs
+++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmBrowserTestRunner.cs
@@ -17,11 +17,7 @@
using Microsoft.DotNet.XHarness.CLI.CommandArguments.Wasm;
using Microsoft.DotNet.XHarness.Common.CLI;
using Microsoft.Extensions.Logging;
-
-using OpenQA.Selenium;
-using OpenQA.Selenium.Support.UI;
-
-using SeleniumLogLevel = OpenQA.Selenium.LogLevel;
+using Microsoft.Playwright;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace Microsoft.DotNet.XHarness.CLI.Commands.Wasm;
@@ -33,10 +29,6 @@ internal class WasmBrowserTestRunner
private readonly IEnumerable _passThroughArguments;
private readonly WasmTestMessagesProcessor _messagesProcessor;
- // Messages from selenium prepend the url, and location where the message originated
- // Eg. `foo` becomes `http://localhost:8000/xyz.js 0:12 "foo"
- static readonly Regex s_consoleLogRegex = new(@"^\s*[a-z]*://[^\s]+\s+\d+:\d+\s+""(.*)""\s*$", RegexOptions.Compiled);
-
public WasmBrowserTestRunner(WasmTestBrowserCommandArguments arguments, IEnumerable passThroughArguments,
WasmTestMessagesProcessor messagesProcessor, ILogger logger)
{
@@ -46,7 +38,7 @@ public WasmBrowserTestRunner(WasmTestBrowserCommandArguments arguments, IEnumera
_messagesProcessor = messagesProcessor;
}
- public async Task RunTestsWithWebDriver(DriverService driverService, IWebDriver driver)
+ public async Task RunTestsWithPlaywrightAsync(PlaywrightServiceWrapper driverService, PlaywrightBrowserWrapper driver)
{
var htmlFilePath = Path.Combine(_arguments.AppPackagePath, _arguments.HTMLFile.Value);
if (!File.Exists(htmlFilePath))
@@ -71,18 +63,18 @@ public async Task RunTestsWithWebDriver(DriverService driverService, I
string testUrl = BuildUrl(serverURLs);
- var seleniumLogMessageTask = Task.Run(() => RunSeleniumLogMessagePump(driver, cts.Token), cts.Token);
+ var playwrightLogMessageTask = Task.Run(() => RunPlaywrightLogMessagePump(driver.Page, cts.Token), cts.Token);
cts.CancelAfter(_arguments.Timeout);
_logger.LogDebug($"Opening in browser: {testUrl}");
- driver.Navigate().GoToUrl(testUrl);
+ await driver.NavigateToUrlAsync(testUrl);
TaskCompletionSource wasmExitReceivedTcs = _messagesProcessor.WasmExitReceivedTcs;
var tasks = new Task[]
{
wasmExitReceivedTcs.Task,
consolePumpTcs.Task,
- seleniumLogMessageTask,
+ playwrightLogMessageTask,
logProcessorTask,
Task.Delay(_arguments.Timeout)
};
@@ -90,7 +82,7 @@ public async Task RunTestsWithWebDriver(DriverService driverService, I
if (_arguments.BackgroundThrottling)
{
// throttling only happens when the page is not visible
- driver.Manage().Window.Minimize();
+ await driver.Page.EvaluateAsync("() => { Object.defineProperty(document, 'visibilityState', { value: 'hidden', writable: false }); }");
}
var task = await Task.WhenAny(tasks).ConfigureAwait(false);
@@ -103,14 +95,9 @@ public async Task RunTestsWithWebDriver(DriverService driverService, I
{
if (driverService.IsRunning)
{
- // Selenium isn't able to kill chrome in this case :/
- int pid = driverService.ProcessId;
- var p = Process.GetProcessById(pid);
- if (p != null)
- {
- _logger.LogError($"Tests timed out. Killing driver service pid {pid}");
- p.Kill(true);
- }
+ // Playwright handles browser lifecycle more gracefully than Selenium
+ _logger.LogError($"Tests timed out. Closing browser gracefully");
+ driver.Dispose(); // This will properly close the browser
}
// timed out
@@ -122,10 +109,8 @@ public async Task RunTestsWithWebDriver(DriverService driverService, I
if (task == wasmExitReceivedTcs.Task && wasmExitReceivedTcs.Task.IsCompletedSuccessfully)
{
_logger.LogTrace($"Looking for `tests_done` element, to get the exit code");
- var testsDoneElement = new WebDriverWait(driver, TimeSpan.FromSeconds(30))
- .Until(e => e.FindElement(By.Id("tests_done")));
-
- if (int.TryParse(testsDoneElement.Text, out var code))
+ var testsDoneElementText = await driver.FindElementTextAsync("#tests_done");
+ if (int.TryParse(testsDoneElementText, out var code))
{
var appExitCode = (ExitCode)Enum.ToObject(typeof(ExitCode), code);
if (logProcessorExitCode != ExitCode.SUCCESS)
@@ -204,50 +189,51 @@ private async Task RunConsoleMessagesPump(WebSocket socket, CancellationToken to
}
}
- // This listens for any `console.log` messages.
- // Since we pipe messages from managed code, and console.* to the websocket,
+ // This listens for any console messages from Playwright's native console event handling.
+ // Since we still pipe messages from managed code and console.* to the websocket,
// this wouldn't normally get much. But listening on this to catch any messages
// that we miss piping to the websocket.
- private void RunSeleniumLogMessagePump(IWebDriver driver, CancellationToken token)
+ private void RunPlaywrightLogMessagePump(IPage page, CancellationToken token)
{
try
{
- ILogs logs = driver.Manage().Logs;
- while (!token.IsCancellationRequested)
+ // Playwright provides structured console events, no need to poll like Selenium
+ page.Console += (_, consoleMessage) =>
{
- foreach (var logType in logs.AvailableLogTypes)
+ if (token.IsCancellationRequested) return;
+
+ if (consoleMessage.Type == "error")
{
- foreach (var logEntry in logs.GetLog(logType))
- {
- if (logEntry.Level == SeleniumLogLevel.Severe)
- {
- // These are errors from the browser, some of which might be
- // thrown as part of tests. So, we can't differentiate when
- // it is an error that we can ignore, vs one that should stop
- // the execution completely.
- //
- // Note: these could be received out-of-order as compared to
- // console messages via the websocket.
- //
- // (see commit message for more info)
- _logger.LogError($"[out of order message from the {logType}]: {logEntry.Message}");
- continue;
- }
-
- var match = s_consoleLogRegex.Match(Regex.Unescape(logEntry.Message));
- string msg = match.Success ? match.Groups[1].Value : logEntry.Message;
- _messagesProcessor.Invoke(msg);
- }
+ // These are errors from the browser, some of which might be
+ // thrown as part of tests. So, we can't differentiate when
+ // it is an error that we can ignore, vs one that should stop
+ // the execution completely.
+ //
+ // Note: these could be received out-of-order as compared to
+ // console messages via the websocket.
+ //
+ // (see commit message for more info)
+ _logger.LogError($"[out of order message from the browser console]: {consoleMessage.Text}");
+ return;
}
+
+ // Process the message through our existing message processor
+ _messagesProcessor.Invoke(consoleMessage.Text);
+ };
+
+ // Keep the task alive while not cancelled
+ while (!token.IsCancellationRequested)
+ {
+ Task.Delay(1000, token).Wait(token);
}
}
- catch (WebDriverException wde) when (wde.Message.Contains("timed out after"))
+ catch (OperationCanceledException)
{
- _logger.LogDebug(wde.Message);
+ _logger.LogDebug($"RunPlaywrightLogMessagePump cancelled");
}
catch (Exception ex)
{
- _logger.LogDebug($"Failed trying to read log messages via selenium: {ex}");
+ _logger.LogDebug($"Failed trying to read log messages via playwright: {ex}");
throw;
}
}
diff --git a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmTestBrowserCommand.cs b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmTestBrowserCommand.cs
index e1db5fd5a..1b0ae4a9e 100644
--- a/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmTestBrowserCommand.cs
+++ b/src/Microsoft.DotNet.XHarness.CLI/Commands/WASM/Browser/WasmTestBrowserCommand.cs
@@ -13,13 +13,7 @@
using Microsoft.DotNet.XHarness.Common.CLI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-using OpenQA.Selenium;
-using OpenQA.Selenium.Chrome;
-using OpenQA.Selenium.Chromium;
-using OpenQA.Selenium.Edge;
-using OpenQA.Selenium.Firefox;
-using OpenQA.Selenium.Safari;
-using SeleniumLogLevel = OpenQA.Selenium.LogLevel;
+using Microsoft.Playwright;
namespace Microsoft.DotNet.XHarness.CLI.Commands.Wasm;
@@ -65,12 +59,12 @@ protected override async Task InvokeInternal(ILogger logger)
logger);
diagnosticsData.Target = Arguments.Browser.Value.ToString();
- (DriverService driverService, IWebDriver driver) = Arguments.Browser.Value switch
+ (PlaywrightServiceWrapper driverService, PlaywrightBrowserWrapper driver) = Arguments.Browser.Value switch
{
- Browser.Chrome => GetChromeDriver(Arguments.Locale, logger),
- Browser.Safari => GetSafariDriver(logger),
- Browser.Firefox => GetFirefoxDriver(logger),
- Browser.Edge => GetEdgeDriver(Arguments.Locale, logger),
+ Browser.Chrome => await GetChromeDriverAsync(Arguments.Locale, logger),
+ Browser.Safari => await GetSafariDriverAsync(logger),
+ Browser.Firefox => await GetFirefoxDriverAsync(logger),
+ Browser.Edge => await GetEdgeDriverAsync(Arguments.Locale, logger),
// shouldn't reach here
_ => throw new ArgumentException($"Unknown browser : {Arguments.Browser}")
@@ -78,7 +72,7 @@ protected override async Task InvokeInternal(ILogger logger)
try
{
- var exitCode = await runner.RunTestsWithWebDriver(driverService, driver);
+ var exitCode = await runner.RunTestsWithPlaywrightAsync(driverService, driver);
if ((int)exitCode != Arguments.ExpectedExitCode)
{
logger.LogError($"Application has finished with exit code {exitCode} but {Arguments.ExpectedExitCode} was expected");
@@ -103,40 +97,16 @@ protected override async Task InvokeInternal(ILogger logger)
token.WaitHandle.WaitOne();
}
- // close all tabs before quit is a workaround for broken Selenium - GeckoDriver communication in Firefox
- // https://github.com/dotnet/runtime/issues/101617
+ // Playwright handles browser cleanup automatically, but we still do graceful shutdown
var cts = new CancellationTokenSource();
cts.CancelAfter(10000);
try
{
- logger.LogInformation($"Closing {driver.WindowHandles.Count} browser tabs before setting the main tab to config page and quitting.");
- while (driver.WindowHandles.Count > 1 && driverService.IsRunning)
- {
- if (cts.IsCancellationRequested)
- {
- logger.LogInformation($"Timeout while trying to close tabs, {driver.WindowHandles.Count} is left open before quitting.");
- break;
- }
- driver.Navigate().GoToUrl("about:config");
- driver.Navigate().GoToUrl("about:blank");
- driver.Close(); //Close Tab
-
- var lastWindowHandle = driver.WindowHandles.LastOrDefault();
- if (lastWindowHandle != null)
- {
- driver.SwitchTo().Window(lastWindowHandle);
- }
- }
- await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
+ logger.LogInformation($"Closing browser gracefully.");
if (driverService.IsRunning)
{
- if (!cts.IsCancellationRequested && driver.WindowHandles.Count != 0)
- {
- driver.Navigate().GoToUrl("about:config");
- driver.Navigate().GoToUrl("about:blank");
- }
- driver.Quit(); // Firefox driver hangs if Quit is not issued.
- driver.Dispose();
+ await Task.Delay(TimeSpan.FromSeconds(1), cts.Token);
+ driver.Dispose(); // This will close page, browser, and playwright
driverService.Dispose();
}
}
@@ -147,104 +117,105 @@ protected override async Task InvokeInternal(ILogger logger)
}
}
- private (DriverService, IWebDriver) GetSafariDriver(ILogger logger)
+ private async Task<(PlaywrightServiceWrapper, PlaywrightBrowserWrapper)> GetSafariDriverAsync(ILogger logger)
{
- var options = new SafariOptions();
- options.SetLoggingPreference(LogType.Browser, SeleniumLogLevel.All);
-
logger.LogInformation("Starting Safari");
- return CreateWebDriver(
- () => SafariDriverService.CreateDefaultService(),
- driverService => new SafariDriver(driverService, options, Arguments.Timeout));
+ var playwright = await Microsoft.Playwright.Playwright.CreateAsync();
+ var browser = await playwright.Webkit.LaunchAsync(new BrowserTypeLaunchOptions
+ {
+ Headless = !Arguments.NoHeadless,
+ Args = Arguments.BrowserArgs.Value.ToList(),
+ Timeout = (float)Arguments.Timeout.Value.TotalMilliseconds,
+ ExecutablePath = !string.IsNullOrEmpty(Arguments.BrowserLocation.Value) ? Arguments.BrowserLocation.Value : null
+ });
+
+ var page = await browser.NewPageAsync();
+
+ // Setup console logging to match Selenium behavior
+ page.Console += (_, e) => logger.LogDebug($"[Browser Console] {e.Text}");
+
+ return (new PlaywrightServiceWrapper(browser), new PlaywrightBrowserWrapper(page, browser, playwright));
}
- private (DriverService, IWebDriver) GetFirefoxDriver(ILogger logger)
+ private async Task<(PlaywrightServiceWrapper, PlaywrightBrowserWrapper)> GetFirefoxDriverAsync(ILogger logger)
{
- var options = new FirefoxOptions();
- options.SetLoggingPreference(LogType.Browser, SeleniumLogLevel.All);
-
- if (!string.IsNullOrEmpty(Arguments.BrowserLocation))
+ if (!string.IsNullOrEmpty(Arguments.BrowserLocation.Value))
{
- options.BrowserExecutableLocation = Arguments.BrowserLocation;
- logger.LogInformation($"Using Firefox from {Arguments.BrowserLocation}");
+ logger.LogInformation($"Using Firefox from {Arguments.BrowserLocation.Value}");
}
- options.AddArguments(Arguments.BrowserArgs.Value);
+ var args = Arguments.BrowserArgs.Value.ToList();
if (!Arguments.NoHeadless)
- options.AddArguments("--headless");
+ args.Add("--headless");
if (!Arguments.NoIncognito)
- options.AddArguments("-private-window");
+ args.Add("-private-window");
+
+ logger.LogInformation($"Starting Firefox with args: {string.Join(' ', args)}");
- options.PageLoadStrategy = Arguments.PageLoadStrategy.Value;
+ var playwright = await Microsoft.Playwright.Playwright.CreateAsync();
+ var browser = await playwright.Firefox.LaunchAsync(new BrowserTypeLaunchOptions
+ {
+ Headless = !Arguments.NoHeadless,
+ Args = args,
+ Timeout = (float)Arguments.Timeout.Value.TotalMilliseconds,
+ ExecutablePath = !string.IsNullOrEmpty(Arguments.BrowserLocation.Value) ? Arguments.BrowserLocation.Value : null
+ });
- logger.LogInformation($"Starting Firefox with args: {string.Join(' ', options.ToCapabilities())} and load strategy: {Arguments.PageLoadStrategy.Value}");
+ var page = await browser.NewPageAsync();
+
+ // Setup console logging
+ page.Console += (_, e) => logger.LogDebug($"[Browser Console] {e.Text}");
- return CreateWebDriver(
- () => FirefoxDriverService.CreateDefaultService(),
- (driverService) => new FirefoxDriver(driverService, options, Arguments.Timeout));
+ return (new PlaywrightServiceWrapper(browser), new PlaywrightBrowserWrapper(page, browser, playwright));
}
- private (DriverService, IWebDriver) GetChromeDriver(string sessionLanguage, ILogger logger)
- => GetChromiumDriver(
+ private async Task<(PlaywrightServiceWrapper, PlaywrightBrowserWrapper)> GetChromeDriverAsync(string sessionLanguage, ILogger logger)
+ => await GetChromiumDriverAsync(
"chromedriver",
sessionLanguage,
- options => ChromeDriverService.CreateDefaultService(),
+ (playwright) => playwright.Chromium,
logger);
- private (DriverService, IWebDriver) GetEdgeDriver(string sessionLanguage, ILogger logger)
- => GetChromiumDriver(
+ private async Task<(PlaywrightServiceWrapper, PlaywrightBrowserWrapper)> GetEdgeDriverAsync(string sessionLanguage, ILogger logger)
+ => await GetChromiumDriverAsync(
"edgedriver",
sessionLanguage,
- options =>
- {
- options.UseChromium = true;
- return EdgeDriverService.CreateDefaultServiceFromOptions(options);
- }, logger);
-
- private (DriverService, IWebDriver) GetChromiumDriver(
- string driverName, string sessionLanguage, Func getDriverService, ILogger logger)
- where TDriver : ChromiumDriver
- where TDriverOptions : ChromiumOptions
- where TDriverService : ChromiumDriverService
- {
- var options = Activator.CreateInstance();
- options.SetLoggingPreference(LogType.Browser, SeleniumLogLevel.All);
-
- if (!string.IsNullOrEmpty(Arguments.BrowserLocation))
- {
- options.BinaryLocation = Arguments.BrowserLocation;
- logger.LogInformation($"Using Chrome from {Arguments.BrowserLocation}");
- }
+ (playwright) => playwright.Chromium, // Edge uses Chromium engine
+ logger);
- options.AddArguments(Arguments.BrowserArgs.Value);
+ private async Task<(PlaywrightServiceWrapper, PlaywrightBrowserWrapper)> GetChromiumDriverAsync(
+ string driverName, string sessionLanguage, Func getBrowserType, ILogger logger)
+ where TDriverOptions : BrowserTypeLaunchOptions, new()
+ {
+ var args = Arguments.BrowserArgs.Value.ToList();
if (!Arguments.NoHeadless && !Arguments.BackgroundThrottling)
- options.AddArguments("--headless");
+ args.Add("--headless");
if (Arguments.DebuggerPort.Value != null)
- options.AddArguments($"--remote-debugging-port={Arguments.DebuggerPort}");
+ args.Add($"--remote-debugging-port={Arguments.DebuggerPort.Value}");
if (!Arguments.NoIncognito)
- options.AddArguments("--incognito");
+ args.Add("--incognito");
if (!Arguments.BackgroundThrottling)
{
- options.AddArguments(new[]
+ args.AddRange(new[]
{
- "--disable-background-timer-throttling",
- "--disable-backgrounding-occluded-windows",
- "--disable-renderer-backgrounding",
- "--enable-features=NetworkService,NetworkServiceInProcess",
- });
+ "--disable-background-timer-throttling",
+ "--disable-backgrounding-occluded-windows",
+ "--disable-renderer-backgrounding",
+ "--enable-features=NetworkService,NetworkServiceInProcess",
+ });
}
else
{
- options.AddArguments(@"--enable-features=IntensiveWakeUpThrottling:grace_period_seconds/1");
+ args.Add(@"--enable-features=IntensiveWakeUpThrottling:grace_period_seconds/1");
}
- options.AddArguments(new[]
+ args.AddRange(new[]
{
// added based on https://github.com/puppeteer/puppeteer/blob/main/src/node/Launcher.ts#L159-L181
"--allow-insecure-localhost",
@@ -261,35 +232,18 @@ protected override async Task InvokeInternal(ILogger logger)
if (File.Exists("/.dockerenv"))
{
// Use --no-sandbox for containers, and codespaces
- options.AddArguments("--no-sandbox");
+ args.Add("--no-sandbox");
}
- if (Arguments.NoQuit)
- options.LeaveBrowserRunning = true;
-
- if (options is ChromeOptions chromeOptions)
- chromeOptions.PageLoadStrategy = Arguments.PageLoadStrategy.Value;
- if (options is EdgeOptions edgeOptions)
- edgeOptions.PageLoadStrategy = Arguments.PageLoadStrategy.Value;
-
- logger.LogInformation($"Starting {driverName} with args: {string.Join(' ', options.Arguments)} and load strategy: {Arguments.PageLoadStrategy.Value}");
-
- // We want to explicitly specify a timeout here. This is for for the
- // driver commands, like getLog. The default is 60s, which ends up
- // timing out when getLog() is waiting, and doesn't receive anything
- // for 60s.
- //
- // Since, we almost all the output gets written via the websocket now,
- // getLog() might not see anything for long durations!
- //
- // So -> use a larger timeout!
+ logger.LogInformation($"Starting {driverName} with args: {string.Join(' ', args)}");
+ // Retry logic preserved from original Selenium implementation
string[] err_snippets = new[]
{
- "exited abnormally",
- "Cannot start the driver service",
- "failed to start"
- };
+ "exited abnormally",
+ "Cannot start the driver service",
+ "failed to start"
+ };
foreach (var file in Directory.EnumerateFiles(Arguments.OutputDirectory, $"{driverName}-*.log"))
File.Delete(file);
@@ -298,64 +252,60 @@ protected override async Task InvokeInternal(ILogger logger)
int retry_num = 0;
while (true)
{
- TDriverService? driverService = null;
+ IPlaywright? playwright = null;
+ IBrowser? browser = null;
try
{
- driverService = getDriverService(options);
- driverService.DriverProcessStarting += (object? sender, DriverProcessStartingEventArgs e) =>
+ playwright = await Microsoft.Playwright.Playwright.CreateAsync();
+ var browserType = getBrowserType(playwright);
+
+ // Set environment for browser launch
+ var env = new System.Collections.Generic.Dictionary();
+ if (!string.IsNullOrEmpty(sessionLanguage))
{
- // Browser respects LANGUAGE in the first place, only if empty it checks LANG
- e.DriverServiceProcessStartInfo.EnvironmentVariables["LANGUAGE"] = sessionLanguage;
- };
+ env["LANGUAGE"] = sessionLanguage;
+ }
- driverService.EnableAppendLog = false;
- driverService.EnableVerboseLogging = true;
- driverService.LogPath = Path.Combine(Arguments.OutputDirectory, $"{driverName}-{retry_num}.log");
+ // Determine channel for branded browsers
+ string? channel = null;
+ if (driverName == "edgedriver")
+ channel = "msedge";
+ else if (driverName == "chromedriver" && !string.IsNullOrEmpty(Arguments.BrowserLocation.Value))
+ channel = "chrome";
- if (Activator.CreateInstance(typeof(TDriver), driverService, options, Arguments.Timeout.Value) is not TDriver driver)
+ browser = await browserType.LaunchAsync(new BrowserTypeLaunchOptions
{
- throw new ArgumentException($"Failed to create instance of {typeof(TDriver)}");
- }
+ Headless = !Arguments.NoHeadless,
+ Args = args,
+ Timeout = (float)Arguments.Timeout.Value.TotalMilliseconds,
+ ExecutablePath = !string.IsNullOrEmpty(Arguments.BrowserLocation.Value) ? Arguments.BrowserLocation.Value : null,
+ Channel = channel,
+ Env = env.Count > 0 ? env : null
+ });
- return (driverService, driver);
+ var page = await browser.NewPageAsync();
+
+ // Setup console logging
+ page.Console += (_, e) => logger.LogDebug($"[Browser Console] {e.Text}");
+
+ return (new PlaywrightServiceWrapper(browser), new PlaywrightBrowserWrapper(page, browser, playwright));
}
- catch (TargetInvocationException tie) when
- (tie.InnerException is WebDriverException wde
- && err_snippets.Any(s => wde.ToString().Contains(s)) && retry_num < max_retries - 1)
+ catch (Exception ex) when (err_snippets.Any(s => ex.ToString().Contains(s)) && retry_num < max_retries - 1)
{
- // chrome can sometimes crash on startup when launching from chromedriver.
- // As a *workaround*, let's retry that a few times
- // Example error seen:
- // [12:41:07] crit: OpenQA.Selenium.WebDriverException: unknown error: Chrome failed to start: exited abnormally.
- // (chrome not reachable)
-
- // Log on max-1 tries, and rethrow on the last one
- logger.LogWarning($"Failed to start the browser, attempt #{retry_num}: {wde}");
+ // Preserve retry logic from Selenium implementation
+ logger.LogWarning($"Failed to start the browser, attempt #{retry_num}: {ex}");
- driverService?.Dispose();
+ browser?.CloseAsync().Wait();
+ playwright?.Dispose();
}
catch
{
- driverService?.Dispose();
+ browser?.CloseAsync().Wait();
+ playwright?.Dispose();
throw;
}
retry_num++;
}
}
-
- private static (DriverService, IWebDriver) CreateWebDriver(Func getDriverService, Func getDriver)
- where TDriverService : DriverService
- {
- var driverService = getDriverService();
- try
- {
- return (driverService, getDriver(driverService));
- }
- catch
- {
- driverService?.Dispose();
- throw;
- }
- }
}
diff --git a/src/Microsoft.DotNet.XHarness.CLI/Microsoft.DotNet.XHarness.CLI.csproj b/src/Microsoft.DotNet.XHarness.CLI/Microsoft.DotNet.XHarness.CLI.csproj
index aa5132fcd..f2674ab20 100644
--- a/src/Microsoft.DotNet.XHarness.CLI/Microsoft.DotNet.XHarness.CLI.csproj
+++ b/src/Microsoft.DotNet.XHarness.CLI/Microsoft.DotNet.XHarness.CLI.csproj
@@ -29,7 +29,7 @@
-
+