-
Notifications
You must be signed in to change notification settings - Fork 4.5k
/
BrowserRunner.cs
155 lines (129 loc) · 5.51 KB
/
BrowserRunner.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System;
using System.IO;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Playwright;
using Wasm.Tests.Internal;
using Xunit.Abstractions;
namespace Wasm.Build.Tests;
internal class BrowserRunner : IAsyncDisposable
{
private static Regex s_blazorUrlRegex = new Regex("Now listening on: (?<url>https?://.*$)");
private static Regex s_appHostUrlRegex = new Regex("^App url: (?<url>https?://.*$)");
private static Regex s_exitRegex = new Regex("WASM EXIT (?<exitCode>[0-9]+)$");
private static readonly Lazy<string> s_chromePath = new(() =>
{
string artifactsBinDir = Path.Combine(Path.GetDirectoryName(typeof(BuildTestBase).Assembly.Location)!, "..", "..", "..", "..");
return BrowserLocator.FindChrome(artifactsBinDir, "BROWSER_PATH_FOR_TESTS");
});
public IPlaywright? Playwright { get; private set; }
public IBrowser? Browser { get; private set; }
public Task<CommandResult>? RunTask { get; private set; }
public IList<string> OutputLines { get; private set; } = new List<string>();
private TaskCompletionSource<int> _exited = new();
private readonly ITestOutputHelper _testOutput;
public BrowserRunner(ITestOutputHelper testOutput) => _testOutput = testOutput;
// FIXME: options
public async Task<IPage> RunAsync(
ToolCommand cmd,
string args,
bool headless = true,
Action<IConsoleMessage>? onConsoleMessage = null,
Action<string>? onError = null,
Func<string, string>? modifyBrowserUrl = null)
{
TaskCompletionSource<string> urlAvailable = new();
Action<string?> outputHandler = msg =>
{
if (string.IsNullOrEmpty(msg))
return;
OutputLines.Add(msg);
Match m = s_appHostUrlRegex.Match(msg);
if (!m.Success)
m = s_blazorUrlRegex.Match(msg);
if (m.Success)
{
string url = m.Groups["url"].Value;
if (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
urlAvailable.TrySetResult(m.Groups["url"].Value);
return;
}
m = s_exitRegex.Match(msg);
if (m.Success)
{
_exited.SetResult(int.Parse(m.Groups["exitCode"].Value));
return;
}
};
cmd.WithErrorDataReceived(outputHandler).WithOutputDataReceived(outputHandler);
var runTask = cmd.ExecuteAsync(args);
await Task.WhenAny(runTask, urlAvailable.Task, Task.Delay(TimeSpan.FromSeconds(30)));
if (runTask.IsCompleted)
{
var res = await runTask;
res.EnsureSuccessful();
throw new Exception($"Process ended before the url was found");
}
if (!urlAvailable.Task.IsCompleted)
throw new Exception("Timed out waiting for the web server url");
var url = new Uri(urlAvailable.Task.Result);
Playwright = await Microsoft.Playwright.Playwright.CreateAsync();
string[] chromeArgs = new[] { $"--explicitly-allowed-ports={url.Port}" };
_testOutput.WriteLine($"Launching chrome ('{s_chromePath.Value}') via playwright with args = {string.Join(',', chromeArgs)}");
Browser = await Playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions{
ExecutablePath = s_chromePath.Value,
Headless = headless,
Args = chromeArgs
});
string browserUrl = urlAvailable.Task.Result;
if (modifyBrowserUrl != null)
browserUrl = modifyBrowserUrl(browserUrl);
IPage page = await Browser.NewPageAsync();
if (onConsoleMessage is not null)
page.Console += (_, msg) => onConsoleMessage(msg);
onError ??= _testOutput.WriteLine;
if (onError is not null)
{
page.PageError += (_, msg) => onError($"PageError: {msg}");
page.Crash += (_, msg) => onError($"Crash: {msg}");
page.FrameDetached += (_, msg) => onError($"FrameDetached: {msg}");
}
await page.GotoAsync(browserUrl);
RunTask = runTask;
return page;
}
public async Task WaitForExitMessageAsync(TimeSpan timeout)
{
if (RunTask is null || RunTask.IsCompleted)
throw new Exception($"No run task, or already completed");
await Task.WhenAny(RunTask!, _exited.Task, Task.Delay(timeout));
if (_exited.Task.IsCompleted)
{
_testOutput.WriteLine ($"Exited with {await _exited.Task}");
return;
}
throw new Exception($"Timed out after {timeout.TotalSeconds}s waiting for 'WASM EXIT' message");
}
public async Task WaitForProcessExitAsync(TimeSpan timeout)
{
if (RunTask is null || RunTask.IsCompleted)
throw new Exception($"No run task, or already completed");
await Task.WhenAny(RunTask!, _exited.Task, Task.Delay(timeout));
if (RunTask.IsCanceled)
{
_testOutput.WriteLine ($"Exited with {(await RunTask).ExitCode}");
return;
}
throw new Exception($"Timed out after {timeout.TotalSeconds}s waiting for process to exit");
}
public async ValueTask DisposeAsync()
{
if (Browser is not null)
await Browser.DisposeAsync();
Playwright?.Dispose();
}
}