Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(roll): roll Playwright to v1.48.0 #3029

Merged
merged 3 commits into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
Loading