diff --git a/src/Playwright.Tests/InterceptionTests.cs b/src/Playwright.Tests/InterceptionTests.cs index 3dd3c4b2f..9220f9800 100644 --- a/src/Playwright.Tests/InterceptionTests.cs +++ b/src/Playwright.Tests/InterceptionTests.cs @@ -134,6 +134,18 @@ bool URLMatches(string baseURL, string url, string glob) Assert.True(URLMatches("http://playwright.dev/", "file://path/to/file", "file://**")); } + // See https://github.com/microsoft/playwright-dotnet/issues/3433 + [PlaywrightTest] + public void ShouldReplaceOnlyTheSchemeOfAWebSocketBaseURL() + { + Assert.AreEqual("ws://app/proxy?to=http://api", URLMatch.ToWebSocketBaseURL("http://app/proxy?to=http://api")); + Assert.AreEqual("wss://app/proxy?to=https://api", URLMatch.ToWebSocketBaseURL("https://app/proxy?to=https://api")); + + // A base URL that has another URL in its path still matches a relative glob. + var matcher = new URLMatch() { baseURL = "http://app/proxy/http://api/", glob = "./socket", isWebSocketUrl = true }; + Assert.True(matcher.Match("ws://app/proxy/http://api/socket")); + } + [PlaywrightTest("interception.spec.ts", "should intercept by glob")] public async Task ShouldInterceptByGlob() { diff --git a/src/Playwright.Tests/PageEvaluateTests.cs b/src/Playwright.Tests/PageEvaluateTests.cs index e61b55de2..b1814f5c5 100644 --- a/src/Playwright.Tests/PageEvaluateTests.cs +++ b/src/Playwright.Tests/PageEvaluateTests.cs @@ -847,4 +847,18 @@ public async Task ShouldParsePublicFields() Assert.AreEqual(400, result.Height); Assert.AreEqual(0, ShapeWithFields.Count); } + + // See https://github.com/microsoft/playwright-dotnet/issues/3437 + [PlaywrightTest] + public async Task ShouldTransferMessagesLargerThanTheReadBuffer() + { + // 3 MB of text that differs at every position, so that a chunk lost, repeated or put + // out of order while the message is read from the driver would show. Its size is not + // a multiple of the read buffer, and it goes both ways. + var expected = string.Join("|", Enumerable.Range(0, 300_001).Select(i => i.ToString("D9", System.Globalization.CultureInfo.InvariantCulture))); + var fromPage = await Page.EvaluateAsync("() => Array.from({ length: 300001 }, (_, i) => String(i).padStart(9, '0')).join('|')"); + Assert.AreEqual(expected, fromPage); + var echoed = await Page.EvaluateAsync("s => s", expected); + Assert.AreEqual(expected, echoed); + } } diff --git a/src/Playwright/Core/Locator.cs b/src/Playwright/Core/Locator.cs index ca28cc7ff..d7a2bbacd 100644 --- a/src/Playwright/Core/Locator.cs +++ b/src/Playwright/Core/Locator.cs @@ -37,6 +37,7 @@ namespace Microsoft.Playwright.Core; +[DebuggerDisplay("Locator@{_selector,nq}")] internal class Locator : ILocator { internal readonly Frame _frame; diff --git a/src/Playwright/Helpers/Driver.cs b/src/Playwright/Helpers/Driver.cs index d245eeaf2..2960d0c3f 100644 --- a/src/Playwright/Helpers/Driver.cs +++ b/src/Playwright/Helpers/Driver.cs @@ -141,8 +141,9 @@ string getArgs(string? args) { return !string.IsNullOrEmpty(args) ? $"\"{cliEntrypoint}\" {args}" : cliEntrypoint; } + var nodePath = Environment.GetEnvironmentVariable("PLAYWRIGHT_NODEJS_PATH"); return ( - Environment.GetEnvironmentVariable("PLAYWRIGHT_NODEJS_PATH") ?? Path.GetFullPath(Path.Combine(driversPath, ".playwright", "node", platformId, nodeExecutable)), + !string.IsNullOrEmpty(nodePath) ? nodePath : Path.GetFullPath(Path.Combine(driversPath, ".playwright", "node", platformId, nodeExecutable)), getArgs); } } diff --git a/src/Playwright/Helpers/URLMatch.cs b/src/Playwright/Helpers/URLMatch.cs index 588267d37..5f0931175 100644 --- a/src/Playwright/Helpers/URLMatch.cs +++ b/src/Playwright/Helpers/URLMatch.cs @@ -188,14 +188,15 @@ internal static string ConstructURLBasedOnBaseURL(string? baseUrl, string url) { return baseURL; } - // Allow http(s) baseURL to match ws(s) urls. + // Allow http(s) baseURL to match ws(s) urls. Only the scheme changes: the rest of the + // URL may contain another URL, in its path or in its query. if (baseURL.StartsWith("http://")) { - return baseURL.Replace("http://", "ws://"); + return "ws://" + baseURL.Substring("http://".Length); } if (baseURL.StartsWith("https://")) { - return baseURL.Replace("https://", "wss://"); + return "wss://" + baseURL.Substring("https://".Length); } return baseURL; } diff --git a/src/Playwright/Transport/Connection.cs b/src/Playwright/Transport/Connection.cs index ba8ab3745..02602a62f 100644 --- a/src/Playwright/Transport/Connection.cs +++ b/src/Playwright/Transport/Connection.cs @@ -48,6 +48,9 @@ internal class Connection : IDisposable internal const string ErrorDetailsDataKey = "playwright.errorDetails"; internal const string LogDataKey = "playwright.log"; + // The value of the DEBUG environment variable when the last connection was created. + private static volatile string? _debugLogLevels; + private readonly ConcurrentDictionary _callbacks = new(); private readonly Root _rootObject; private readonly TaskQueue _queue = new(); @@ -59,6 +62,8 @@ internal class Connection : IDisposable public Connection(LocalUtils? localUtils = null) { + // Read here and not for every message. The driver also reads DEBUG once, when it starts. + _debugLogLevels = Environment.GetEnvironmentVariable("DEBUG"); _rootObject = new(null, this, string.Empty); LocalUtils = localUtils; @@ -513,12 +518,12 @@ private void Dispose(bool disposing) internal static void TraceMessage(string logLevel, byte[] rawMessage) { - string actualLogLevel = Environment.GetEnvironmentVariable("DEBUG"); + string? actualLogLevel = _debugLogLevels; if (string.IsNullOrEmpty(actualLogLevel)) { return; } - if (!actualLogLevel.Contains(logLevel)) + if (!actualLogLevel!.Contains(logLevel)) { return; } diff --git a/src/Playwright/Transport/StdIOTransport.cs b/src/Playwright/Transport/StdIOTransport.cs index ef15b6c22..f31b20de3 100644 --- a/src/Playwright/Transport/StdIOTransport.cs +++ b/src/Playwright/Transport/StdIOTransport.cs @@ -34,7 +34,9 @@ namespace Microsoft.Playwright.Transport; internal class StdIOTransport : IDisposable { - private const int DefaultBufferSize = 1024; // Byte buffer size + // One read takes what the pipe holds, up to this size. A pipe holds 64 KB on Linux and macOS, + // so a message of 10 MB arrives in about 160 reads. + private const int DefaultBufferSize = 64 * 1024; private readonly Process _process; private readonly CancellationTokenSource _readerCancellationSource = new(); private readonly Task _getResponseTask;