From 1d7a1be095033e31d83df1ab432b9e84c0caf467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Kondratiuk?= Date: Tue, 6 Oct 2026 03:49:01 -0300 Subject: [PATCH 1/2] Skip frames without a live context when decorating screenshots A stalled subframe navigation leaves the iframe without an execution context. Style injection, caret hiding and their cleanup each waited up to 5 s for that context, so a screenshot took ~11 s and on loaded Windows runners ran into the target being closed. Upstream uses nonStallingEvaluateInExistingContext for these steps; match that by skipping child frames that have no existing context. Co-Authored-By: Claude Opus 5.5 --- .../Helpers/ScreenshotDecorations.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/PlaywrightNative/Helpers/ScreenshotDecorations.cs b/src/PlaywrightNative/Helpers/ScreenshotDecorations.cs index 629a3821..39dba40f 100644 --- a/src/PlaywrightNative/Helpers/ScreenshotDecorations.cs +++ b/src/PlaywrightNative/Helpers/ScreenshotDecorations.cs @@ -20,6 +20,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Playwright; +using PlaywrightNative.Chromium; using PlaywrightNative.WebKit; namespace PlaywrightNative.Helpers @@ -404,7 +405,7 @@ private static async Task InjectStyleAsync(IPage page, string css, List + /// Upstream decorates frames with nonStallingEvaluateInExistingContext: + /// a Chromium child frame whose document has no live execution context yet + /// (a stalled subframe navigation) is skipped instead of waiting for one. + /// + /// The frame to decorate. + /// when evaluating would wait for a new context. + private static bool LacksExistingContext(IFrame frame) + => frame is ChromiumFrame chromium + && (chromium.Frame.ExecutionContext == null || chromium.Frame.ExecutionContext.Destroyed.IsCompleted); + private static Task FinishAnimationsAsync(IPage page) { // Must share EvaluateInFramesAsync with RestoreAnimationsJs so From 20e1296e95c16ad46b9b73043f2f7fa7c511fb60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Kondratiuk?= Date: Tue, 6 Oct 2026 05:28:09 -0300 Subject: [PATCH 2/2] Reject the TLS hang fixture after the ClientHello, like upstream The SNI-reject server set both ServerCertificate and ServerCertificateSelectionCallback. SslStream rejects that combination with InvalidOperationException before it reads anything, so the server closed every connection right after accept instead of failing the handshake from the SNI callback the way the upstream tls.createServer fixture does. Drop the certificate so the throwing callback runs after the ClientHello arrives. Co-Authored-By: Claude Opus 5.5 --- .../OfficialClientCertificateExtras.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/PlaywrightNative.Tests/OfficialClientCertificateExtras.cs b/src/PlaywrightNative.Tests/OfficialClientCertificateExtras.cs index dc56a1b9..f849c440 100644 --- a/src/PlaywrightNative.Tests/OfficialClientCertificateExtras.cs +++ b/src/PlaywrightNative.Tests/OfficialClientCertificateExtras.cs @@ -113,15 +113,13 @@ public async ValueTask DisposeAsync() internal sealed class OfficialTlsSniRejectServer : IAsyncDisposable { private readonly TcpListener _listener; - private readonly X509Certificate2 _cert; private readonly CancellationTokenSource _cts = new(); private readonly Task _acceptLoop; private readonly SslProtocols _protocol; - private OfficialTlsSniRejectServer(TcpListener listener, X509Certificate2 cert, SslProtocols protocol) + private OfficialTlsSniRejectServer(TcpListener listener, SslProtocols protocol) { _listener = listener; - _cert = cert; _protocol = protocol; Port = ((IPEndPoint)listener.LocalEndpoint).Port; _acceptLoop = AcceptLoopAsync(); @@ -133,12 +131,9 @@ private OfficialTlsSniRejectServer(TcpListener listener, X509Certificate2 cert, internal static OfficialTlsSniRejectServer Start(SslProtocols protocol) { - X509Certificate2 cert = OfficialClientCertificateServer.LoadPem( - OfficialClientCertificateServer.Asset("client-certificates/server/server_cert.pem"), - OfficialClientCertificateServer.Asset("client-certificates/server/server_key.pem")); TcpListener listener = new(IPAddress.Loopback, 0); listener.Start(); - return new OfficialTlsSniRejectServer(listener, cert, protocol); + return new OfficialTlsSniRejectServer(listener, protocol); } public async ValueTask DisposeAsync() @@ -161,7 +156,6 @@ public async ValueTask DisposeAsync() } _cts.Dispose(); - _cert.Dispose(); } private async Task AcceptLoopAsync() @@ -198,9 +192,12 @@ private async Task HandleAsync(TcpClient client) await using (NetworkStream stream = client.GetStream()) await using (SslStream ssl = new(stream, leaveInnerStreamOpen: false)) { + // Upstream rejects from SNICallback, after the ClientHello + // arrives. SslStream only runs the selection callback when no + // ServerCertificate is set; setting both throws before the + // handshake reads anything. SslServerAuthenticationOptions options = new() { - ServerCertificate = _cert, ClientCertificateRequired = true, EnabledSslProtocols = _protocol, CertificateRevocationCheckMode = X509RevocationMode.NoCheck,