Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
<PackageVersion Include="Mono.Options" Version="6.12.0.148" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Selenium.WebDriver" Version="4.0.0-alpha05" />
<PackageVersion Include="Microsoft.Playwright" Version="1.54.0" />
<PackageVersion Include="Microsoft.Tools.Mlaunch" Version="1.1.72" />
<PackageVersion Include="NUnit" Version="3.13.0" />
<PackageVersion Include="NUnit.Engine" Version="3.13.0" />
Expand Down
27 changes: 0 additions & 27 deletions src/Microsoft.DotNet.XHarness.CLI/CommandArguments/Argument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenQA.Selenium;

namespace Microsoft.DotNet.XHarness.CLI.CommandArguments;

Expand Down Expand Up @@ -284,32 +283,6 @@ public override void Action(string argumentValue)
public override string ToString() => Value ? "true" : "false";
}

public abstract class EnumPageLoadStrategyArgument : Argument<PageLoadStrategy>
{
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<IEnumerable<string>>
{
private readonly List<string> _values = new();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -58,7 +57,6 @@ internal class WasmTestBrowserCommandArguments : XHarnessCommandArguments, IWebS
NoQuit,
BackgroundThrottling,
Locale,
PageLoadStrategy,
SymbolMapFileArgument,
SymbolicatePatternsFileArgument,
SymbolicatorArgument,
Expand All @@ -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}");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -44,7 +43,6 @@ internal class WasmTestCommandArguments : XHarnessCommandArguments, IWebServerAr
Timeout,
ExpectedExitCode,
Locale,
PageLoadStrategy,
SymbolMapFileArgument,
SymbolicatePatternsFileArgument,
SymbolicatorArgument,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Wrapper around Playwright IPage to provide Selenium-like interface for easier migration
/// </summary>
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<string> 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();
}
}

/// <summary>
/// Service wrapper to mimic Selenium DriverService behavior
/// </summary>
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,10 +29,6 @@ internal class WasmBrowserTestRunner
private readonly IEnumerable<string> _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<string> passThroughArguments,
WasmTestMessagesProcessor messagesProcessor, ILogger logger)
{
Expand All @@ -46,7 +38,7 @@ public WasmBrowserTestRunner(WasmTestBrowserCommandArguments arguments, IEnumera
_messagesProcessor = messagesProcessor;
}

public async Task<ExitCode> RunTestsWithWebDriver(DriverService driverService, IWebDriver driver)
public async Task<ExitCode> RunTestsWithPlaywrightAsync(PlaywrightServiceWrapper driverService, PlaywrightBrowserWrapper driver)
{
var htmlFilePath = Path.Combine(_arguments.AppPackagePath, _arguments.HTMLFile.Value);
if (!File.Exists(htmlFilePath))
Expand All @@ -71,26 +63,26 @@ public async Task<ExitCode> 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)
};

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);
Expand All @@ -103,14 +95,9 @@ public async Task<ExitCode> 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
Expand All @@ -122,10 +109,8 @@ public async Task<ExitCode> 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)
Expand Down Expand Up @@ -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;
}
}
Expand Down
Loading
Loading