Skip to content

Commit

Permalink
chore(roll): roll Playwright to v1.48.0 (#3029)
Browse files Browse the repository at this point in the history
  • Loading branch information
mxschmitt authored Oct 21, 2024
1 parent 5b76172 commit 6423c0b
Show file tree
Hide file tree
Showing 83 changed files with 2,757 additions and 841 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->129.0.6668.29<!-- GEN:stop --> ||||
| Chromium <!-- GEN:chromium-version -->130.0.6723.31<!-- GEN:stop --> ||||
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> ||||
| Firefox <!-- GEN:firefox-version -->130.0<!-- GEN:stop --> ||||
| Firefox <!-- GEN:firefox-version -->131.0<!-- GEN:stop --> ||||

Playwright for .NET is the official language port of [Playwright](https://playwright.dev), the library to automate [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) with a single API. Playwright is built to enable cross-browser web automation that is **ever-green**, **capable**, **reliable** and **fast**.

Expand Down
2 changes: 1 addition & 1 deletion src/Common/Version.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyVersion>1.47.0</AssemblyVersion>
<PackageVersion>$(AssemblyVersion)</PackageVersion>
<DriverVersion>1.47.0-beta-1726138322000</DriverVersion>
<DriverVersion>1.48.1</DriverVersion>
<ReleaseVersion>$(AssemblyVersion)</ReleaseVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<NoDefaultExcludes>true</NoDefaultExcludes>
Expand Down
128 changes: 24 additions & 104 deletions src/Playwright.Tests.TestServer/SimpleServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,16 @@ namespace Microsoft.Playwright.Tests.TestServer;

public class SimpleServer
{
const int MaxMessageSize = 256 * 1024;

private readonly IDictionary<string, Action<HttpContext>> _requestWaits;
private readonly IList<Func<WebSocket, HttpContext, Task>> _waitForWebSocketConnectionRequestsWaits;
private readonly IDictionary<string, Action<HttpContext>> _requestSubscribers;
private readonly ConcurrentBag<Func<WebSocketWithEvents, HttpContext, Task>> _webSocketSubscribers;
private readonly IDictionary<string, Func<HttpContext, Task>> _routes;
private readonly IDictionary<string, (string username, string password)> _auths;
private readonly IDictionary<string, string> _csp;
private readonly IList<string> _gzipRoutes;
private readonly string _contentRoot;


private ArraySegment<byte> _onWebSocketConnectionData;
private readonly IWebHost _webHost;
private static int _counter;
private readonly Dictionary<int, WebSocket> _clients = new();

public int Port { get; }
public string Prefix { get; }
Expand All @@ -84,8 +79,8 @@ public SimpleServer(int port, string contentRoot, bool isHttps)
EmptyPage = $"{Prefix}/empty.html";

var currentExecutionContext = TestExecutionContext.CurrentContext;
_requestWaits = new ConcurrentDictionary<string, Action<HttpContext>>();
_waitForWebSocketConnectionRequestsWaits = [];
_requestSubscribers = new ConcurrentDictionary<string, Action<HttpContext>>();
_webSocketSubscribers = [];
_routes = new ConcurrentDictionary<string, Func<HttpContext, Task>>();
_auths = new ConcurrentDictionary<string, (string username, string password)>();
_csp = new ConcurrentDictionary<string, string>();
Expand All @@ -108,30 +103,23 @@ public SimpleServer(int port, string contentRoot, bool isHttps)
var currentContext = typeof(TestExecutionContext).GetField("AsyncLocalCurrentContext", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null) as AsyncLocal<TestExecutionContext>;
currentContext.Value = currentExecutionContext;
}
if (context.Request.Path == "/ws")
if (context.WebSockets.IsWebSocketRequest && context.Request.Path == "/ws")
{
if (context.WebSockets.IsWebSocketRequest)
var webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
var testWebSocket = new WebSocketWithEvents(webSocket, context.Request);
if (_onWebSocketConnectionData != null)
{
var webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
foreach (var wait in _waitForWebSocketConnectionRequestsWaits)
{
_waitForWebSocketConnectionRequestsWaits.Remove(wait);
await wait(webSocket, context).ConfigureAwait(false);
}
if (_onWebSocketConnectionData != null)
{
await webSocket.SendAsync(_onWebSocketConnectionData, WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);
}
await ReceiveLoopAsync(webSocket, context.Request.Headers["User-Agent"].ToString().Contains("Firefox"), CancellationToken.None).ConfigureAwait(false);
await webSocket.SendAsync(_onWebSocketConnectionData, WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false);
}
else if (!context.Response.HasStarted)
foreach (var wait in _webSocketSubscribers)
{
context.Response.StatusCode = 400;
await wait(testWebSocket, context).ConfigureAwait(false);
}
await testWebSocket.RunReceiveLoop().ConfigureAwait(false);
return;
}
if (_requestWaits.TryGetValue(context.Request.Path, out var requestWait))
if (_requestSubscribers.TryGetValue(context.Request.Path, out var requestWait))
{
requestWait(context);
}
Expand Down Expand Up @@ -264,9 +252,10 @@ public void Reset()
_routes.Clear();
_auths.Clear();
_csp.Clear();
_requestWaits.Clear();
_requestSubscribers.Clear();
_gzipRoutes.Clear();
_onWebSocketConnectionData = null;
_webSocketSubscribers.Clear();
}

public void EnableGzip(string path) => _gzipRoutes.Add(path);
Expand All @@ -290,33 +279,33 @@ public void SetRedirect(string from, string to) => SetRoute(from, context =>
public async Task<T> WaitForRequest<T>(string path, Func<HttpRequest, T> selector)
{
var taskCompletion = new TaskCompletionSource<T>();
_requestWaits.Add(path, context =>
_requestSubscribers.Add(path, context =>
{
taskCompletion.SetResult(selector(context.Request));
});

var request = await taskCompletion.Task.ConfigureAwait(false);
_requestWaits.Remove(path);
_requestSubscribers.Remove(path);

return request;
}

public Task WaitForRequest(string path) => WaitForRequest(path, _ => true);

public async Task<(WebSocket, HttpRequest)> WaitForWebSocketConnectionRequest()
public Task<WebSocketWithEvents> WaitForWebSocketAsync()
{
var taskCompletion = new TaskCompletionSource<(WebSocket, HttpRequest)>();
OnceWebSocketConnection((WebSocket ws, HttpContext context) =>
var tcs = new TaskCompletionSource<WebSocketWithEvents>();
OnceWebSocketConnection((ws, _) =>
{
taskCompletion.SetResult((ws, context.Request));
tcs.SetResult(ws);
return Task.CompletedTask;
});
return await taskCompletion.Task.ConfigureAwait(false);
return tcs.Task;
}

public void OnceWebSocketConnection(Func<WebSocket, HttpContext, Task> handler)
public void OnceWebSocketConnection(Func<WebSocketWithEvents, HttpContext, Task> handler)
{
_waitForWebSocketConnectionRequestsWaits.Add(handler);
_webSocketSubscribers.Add(handler);
}

private static bool Authenticate(string username, string password, HttpContext context)
Expand All @@ -332,73 +321,4 @@ private static bool Authenticate(string username, string password, HttpContext c
}
return false;
}

private async Task ReceiveLoopAsync(WebSocket webSocket, bool sendCloseMessage, CancellationToken token)
{
int connectionId = NextConnectionId();
_clients.Add(connectionId, webSocket);

byte[] buffer = new byte[MaxMessageSize];

try
{
while (true)
{
var result = await webSocket.ReceiveAsync(new(buffer), token).ConfigureAwait(false);

if (result.MessageType == WebSocketMessageType.Close)
{
if (sendCloseMessage)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Close", CancellationToken.None).ConfigureAwait(false);
}
break;
}

var data = await ReadFrames(result, webSocket, buffer, token).ConfigureAwait(false);

if (data.Count == 0)
{
break;
}
}
}
finally
{
_clients.Remove(connectionId);
}
}

private async Task<ArraySegment<byte>> ReadFrames(WebSocketReceiveResult result, WebSocket webSocket, byte[] buffer, CancellationToken token)
{
int count = result.Count;

while (!result.EndOfMessage)
{
if (count >= MaxMessageSize)
{
string closeMessage = $"Maximum message size: {MaxMessageSize} bytes.";
await webSocket.CloseAsync(WebSocketCloseStatus.MessageTooBig, closeMessage, token).ConfigureAwait(false);
return new();
}

result = await webSocket.ReceiveAsync(new(buffer, count, MaxMessageSize - count), token).ConfigureAwait(false);
count += result.Count;

}
return new(buffer, 0, count);
}


private static int NextConnectionId()
{
int id = Interlocked.Increment(ref _counter);

if (id == int.MaxValue)
{
throw new("connection id limit reached: " + id);
}

return id;
}
}
113 changes: 113 additions & 0 deletions src/Playwright.Tests.TestServer/WebSocketWithEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* MIT License
*
* Copyright (c) Microsoft Corporation.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace Microsoft.Playwright.Tests.TestServer;

public class WebSocketWithEvents : IDisposable
{
public readonly WebSocket ws;
public readonly HttpRequest request;
private readonly CancellationTokenSource _cts = new();

public WebSocketWithEvents(WebSocket ws, HttpRequest request)
{
this.ws = ws;
this.request = request;
}

public event EventHandler<string> MessageReceived;
public event EventHandler<(WebSocketCloseStatus? Code, string Reason)> Closed;

internal async Task RunReceiveLoop()
{
var buffer = new byte[1024 * 4];
try
{
while (ws.State == WebSocketState.Open && !_cts.IsCancellationRequested)
{
var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), _cts.Token).ConfigureAwait(false);
if (result.MessageType == WebSocketMessageType.Text)
{
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
MessageReceived?.Invoke(this, message);
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await CloseAsync().ConfigureAwait(false);
break;
}
}
}
catch (OperationCanceledException)
{
// Normal cancellation, do nothing
}
finally
{
if (ws.State == WebSocketState.CloseReceived)
{
Closed?.Invoke(this, (ws.CloseStatus, ws.CloseStatusDescription));
}
await CloseAsync().ConfigureAwait(false);
}
}

public async Task SendAsync(string message)
{
if (ws.State != WebSocketState.Open)
throw new InvalidOperationException("WebSocket is not open.");

var buffer = Encoding.UTF8.GetBytes(message);
await ws.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, _cts.Token).ConfigureAwait(false);
}

public async Task CloseAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.NormalClosure, string statusDescription = "Closing")
{
if (ws.State == WebSocketState.Open)
{
try
{
await ws.CloseAsync(closeStatus, statusDescription, CancellationToken.None).ConfigureAwait(false);
}
catch (WebSocketException)
{
// The WebSocket might have been closed by the server, ignore the exception
}
}
}

public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
ws.Dispose();
}
}
16 changes: 8 additions & 8 deletions src/Playwright.Tests/BrowserTypeConnectOverCDPTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ public async Task ShouldConnectToAnExistingCDPSession()
[Skip(SkipAttribute.Targets.Firefox, SkipAttribute.Targets.Webkit)]
public async Task ShouldSendExtraHeadersWithConnectRequest()
{
var waitForRequest = Server.WaitForWebSocketConnectionRequest();
var webSocketTask = Server.WaitForWebSocketAsync();
BrowserType.ConnectOverCDPAsync($"ws://127.0.0.1:{Server.Port}/ws", new()
{
Headers = new Dictionary<string, string> {
{ "x-foo-bar", "fookek" }
},
{ "x-foo-bar", "fookek" }
},
}).IgnoreException();
(_, var req) = await waitForRequest;
Assert.AreEqual("fookek", req.Headers["x-foo-bar"]);
StringAssert.Contains("Playwright", req.Headers["user-agent"]);
var webSocket = await webSocketTask;
Assert.AreEqual("fookek", webSocket.request.Headers["x-foo-bar"]);
StringAssert.Contains("Playwright", webSocket.request.Headers["user-agent"]);
}

[PlaywrightTest("chromium/chromium.spec.ts", "should report all pages in an existing browser")]
Expand Down Expand Up @@ -102,8 +102,8 @@ public async Task ShouldPrintCustomWsCloseError()
{
Server.OnceWebSocketConnection(async (ws, request) =>
{
await ws.ReceiveAsync(new byte[1024], CancellationToken.None);
await ws.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, "Oh my!", CancellationToken.None);
await ws.ws.ReceiveAsync(new byte[1024], CancellationToken.None);
await ws.ws.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, "Oh my!", CancellationToken.None);
});
var error = await PlaywrightAssert.ThrowsAsync<PlaywrightException>(() => BrowserType.ConnectOverCDPAsync($"ws://localhost:{Server.Port}/ws"));
StringAssert.Contains("Browser logs:\n\nOh my!\n", error.Message);
Expand Down
Loading

0 comments on commit 6423c0b

Please sign in to comment.