From 86ad6c744350f97b01eba68dbb3b4c59efc9caf4 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 24 Jun 2024 22:15:19 +0200 Subject: [PATCH] chore(roll): roll to Playwright v1.45 (#2949) --- .github/workflows/tests.yml | 2 +- README.md | 4 +- src/Common/Dependencies.props | 6 +- src/Common/Version.props | 2 +- src/Playwright.MSTest/PlaywrightTest.cs | 2 +- .../SimpleServer.cs | 9 +- .../assets/input/folderupload.html | 12 + src/Playwright.Tests/BaseTests/PageTestEx.cs | 15 + .../BaseTests/PlaywrightTestEx.cs | 15 + .../BrowserContextAddCookiesTests.cs | 2 +- .../BrowserContextCookiesTests.cs | 12 +- .../BrowserContextFetchTests.cs | 32 ++ .../BrowserContextStorageStateTests.cs | 8 - .../BrowserTypeConnectTests.cs | 92 +++ .../DefaultBrowserContext1Tests.cs | 4 +- src/Playwright.Tests/GlobalFetchTests.cs | 26 +- src/Playwright.Tests/PageClockTests.cs | 542 ++++++++++++++++++ .../PageNetworkRequestTest.cs | 1 + .../PageRequestContinueTests.cs | 51 ++ .../PageSetInputFilesTests.cs | 78 ++- .../PageWaitForSelector1Tests.cs | 2 +- .../Generated/Enums/HttpCredentialsSend.cs | 39 ++ .../API/Generated/IBrowserContext.cs | 24 +- src/Playwright/API/Generated/IClock.cs | 290 ++++++++++ .../API/Generated/IConsoleMessage.cs | 4 +- .../API/Generated/IElementHandle.cs | 16 +- src/Playwright/API/Generated/ILocator.cs | 36 +- .../API/Generated/ILocatorAssertions.cs | 14 +- src/Playwright/API/Generated/IMouse.cs | 8 +- src/Playwright/API/Generated/IPage.cs | 27 +- .../API/Generated/IPageAssertions.cs | 14 +- .../API/Generated/IPlaywrightAssertions.cs | 11 +- .../BrowserContextUnrouteAllOptions.cs | 2 +- .../Generated/Options/ClockInstallOptions.cs | 66 +++ .../Options/PageUnrouteAllOptions.cs | 2 +- .../API/Generated/Types/HttpCredentials.cs | 12 + src/Playwright/Core/BrowserContext.cs | 10 +- src/Playwright/Core/Clock.cs | 107 ++++ src/Playwright/Core/ElementHandle.cs | 2 + src/Playwright/Core/Frame.cs | 2 + src/Playwright/Core/Page.cs | 2 + src/Playwright/Core/PlaywrightImpl.cs | 6 +- src/Playwright/Core/Request.cs | 7 +- src/Playwright/Core/Stream.cs | 5 +- src/Playwright/Core/WritableStream.cs | 8 +- .../Helpers/SetInputFilesHelpers.cs | 92 ++- .../Transport/Protocol/Generated/Metadata.cs | 4 +- .../Transport/Protocol/SetInputFilesFiles.cs | 4 + 48 files changed, 1611 insertions(+), 120 deletions(-) create mode 100644 src/Playwright.Tests.TestServer/assets/input/folderupload.html create mode 100644 src/Playwright.Tests/PageClockTests.cs create mode 100644 src/Playwright/API/Generated/Enums/HttpCredentialsSend.cs create mode 100644 src/Playwright/API/Generated/IClock.cs create mode 100644 src/Playwright/API/Generated/Options/ClockInstallOptions.cs create mode 100644 src/Playwright/Core/Clock.cs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3589606cac..f7bf84eebc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: browser: [chromium, firefox, webkit] - os: [windows-latest, ubuntu-latest, macos-12] + os: [windows-latest, ubuntu-latest, macos-13] steps: - uses: actions/checkout@v3 - name: Setup .NET Core diff --git a/README.md b/README.md index 04f7c5845b..e56df88d03 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 125.0.6422.26 | ✅ | ✅ | ✅ | +| Chromium 127.0.6533.5 | ✅ | ✅ | ✅ | | WebKit 17.4 | ✅ | ✅ | ✅ | -| Firefox 125.0.1 | ✅ | ✅ | ✅ | +| Firefox 127.0 | ✅ | ✅ | ✅ | 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**. diff --git a/src/Common/Dependencies.props b/src/Common/Dependencies.props index cdd871f086..ba44be21a3 100644 --- a/src/Common/Dependencies.props +++ b/src/Common/Dependencies.props @@ -10,11 +10,11 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -22,7 +22,7 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers diff --git a/src/Common/Version.props b/src/Common/Version.props index cdc24e8e7d..005c0e7713 100644 --- a/src/Common/Version.props +++ b/src/Common/Version.props @@ -2,7 +2,7 @@ 1.44.0 $(AssemblyVersion) - 1.44.0-beta-1715802478000 + 1.45.0-beta-1718782041000 $(AssemblyVersion) $(AssemblyVersion) true diff --git a/src/Playwright.MSTest/PlaywrightTest.cs b/src/Playwright.MSTest/PlaywrightTest.cs index 8c7ede3c25..c0304f9821 100644 --- a/src/Playwright.MSTest/PlaywrightTest.cs +++ b/src/Playwright.MSTest/PlaywrightTest.cs @@ -47,7 +47,7 @@ public class PlaywrightTest public IBrowserType BrowserType { get; private set; } = null!; - public int WorkerIndex { get => _currentWorker!.WorkerIndex; } + public int WorkerIndex => _currentWorker!.WorkerIndex; [TestInitialize] public async Task Setup() diff --git a/src/Playwright.Tests.TestServer/SimpleServer.cs b/src/Playwright.Tests.TestServer/SimpleServer.cs index 5239d0e681..9c309bb3e1 100644 --- a/src/Playwright.Tests.TestServer/SimpleServer.cs +++ b/src/Playwright.Tests.TestServer/SimpleServer.cs @@ -117,6 +117,11 @@ public SimpleServer(int port, string contentRoot, bool isHttps) return; } + if (_requestWaits.TryGetValue(context.Request.Path, out var requestWait)) + { + requestWait(context); + } + if (_auths.TryGetValue(context.Request.Path, out var auth) && !Authenticate(auth.username, auth.password, context)) { context.Response.Headers.Append("WWW-Authenticate", "Basic realm=\"Secure Area\""); @@ -128,10 +133,6 @@ public SimpleServer(int port, string contentRoot, bool isHttps) await context.Response.WriteAsync("HTTP Error 401 Unauthorized: Access is denied").ConfigureAwait(false); return; } - if (_requestWaits.TryGetValue(context.Request.Path, out var requestWait)) - { - requestWait(context); - } if (_routes.TryGetValue(context.Request.Path + context.Request.QueryString, out var handler)) { await handler(context).ConfigureAwait(false); diff --git a/src/Playwright.Tests.TestServer/assets/input/folderupload.html b/src/Playwright.Tests.TestServer/assets/input/folderupload.html new file mode 100644 index 0000000000..16c7e2c3e9 --- /dev/null +++ b/src/Playwright.Tests.TestServer/assets/input/folderupload.html @@ -0,0 +1,12 @@ + + + + Folder upload test + + +
+ + +
+ + \ No newline at end of file diff --git a/src/Playwright.Tests/BaseTests/PageTestEx.cs b/src/Playwright.Tests/BaseTests/PageTestEx.cs index 63c854826c..70d7f82813 100644 --- a/src/Playwright.Tests/BaseTests/PageTestEx.cs +++ b/src/Playwright.Tests/BaseTests/PageTestEx.cs @@ -32,6 +32,21 @@ public class PageTestEx : PageTest public SimpleServer Server { get; internal set; } public SimpleServer HttpsServer { get; internal set; } public int BrowserMajorVersion { get; internal set; } + public SameSiteAttribute DefaultSameSiteCookieValue + { + get + { + if (TestConstants.IsChromium) + { + return SameSiteAttribute.Lax; + } + if (TestConstants.IsWebKit && TestConstants.IsLinux) + { + return SameSiteAttribute.Lax; + } + return SameSiteAttribute.None; + } + } [SetUp] public async Task HttpSetup() diff --git a/src/Playwright.Tests/BaseTests/PlaywrightTestEx.cs b/src/Playwright.Tests/BaseTests/PlaywrightTestEx.cs index 821a3cbb47..ebbcd17482 100644 --- a/src/Playwright.Tests/BaseTests/PlaywrightTestEx.cs +++ b/src/Playwright.Tests/BaseTests/PlaywrightTestEx.cs @@ -30,6 +30,21 @@ public class PlaywrightTestEx : PlaywrightTest { public SimpleServer Server { get; internal set; } public SimpleServer HttpsServer { get; internal set; } + public SameSiteAttribute DefaultSameSiteCookieValue + { + get + { + if (TestConstants.IsChromium) + { + return SameSiteAttribute.Lax; + } + if (TestConstants.IsWebKit && TestConstants.IsLinux) + { + return SameSiteAttribute.Lax; + } + return SameSiteAttribute.None; + } + } [SetUp] public async Task HttpSetup() diff --git a/src/Playwright.Tests/BrowserContextAddCookiesTests.cs b/src/Playwright.Tests/BrowserContextAddCookiesTests.cs index a2a414ef8f..3b4ce0bc76 100644 --- a/src/Playwright.Tests/BrowserContextAddCookiesTests.cs +++ b/src/Playwright.Tests/BrowserContextAddCookiesTests.cs @@ -306,7 +306,7 @@ await Context.AddCookiesAsync(new[] Assert.AreEqual(-1, cookie.Expires); Assert.IsFalse(cookie.HttpOnly); Assert.IsFalse(cookie.Secure); - Assert.AreEqual(TestConstants.IsChromium ? SameSiteAttribute.Lax : SameSiteAttribute.None, cookie.SameSite); + Assert.AreEqual(DefaultSameSiteCookieValue, cookie.SameSite); } [PlaywrightTest("browsercontext-add-cookies.spec.ts", "should set a cookie with a path")] diff --git a/src/Playwright.Tests/BrowserContextCookiesTests.cs b/src/Playwright.Tests/BrowserContextCookiesTests.cs index 05130f38c1..5dd4fab0bf 100644 --- a/src/Playwright.Tests/BrowserContextCookiesTests.cs +++ b/src/Playwright.Tests/BrowserContextCookiesTests.cs @@ -48,7 +48,7 @@ public async Task ShouldGetACookie() Assert.AreEqual(-1, cookie.Expires); Assert.IsFalse(cookie.HttpOnly); Assert.IsFalse(cookie.Secure); - Assert.AreEqual(TestConstants.IsChromium ? SameSiteAttribute.Lax : SameSiteAttribute.None, cookie.SameSite); + Assert.AreEqual(DefaultSameSiteCookieValue, cookie.SameSite); } [PlaywrightTest("browsercontext-cookies.spec.ts", "should get a non-session cookie")] @@ -70,7 +70,7 @@ public async Task ShouldGetANonSessionCookie() Assert.NotNull(cookie.Expires); Assert.IsFalse(cookie.HttpOnly); Assert.IsFalse(cookie.Secure); - Assert.AreEqual(TestConstants.IsChromium ? SameSiteAttribute.Lax : SameSiteAttribute.None, cookie.SameSite); + Assert.AreEqual(DefaultSameSiteCookieValue, cookie.SameSite); } [PlaywrightTest("browsercontext-cookies.spec.ts", "should properly report httpOnly cookie")] @@ -138,7 +138,7 @@ public async Task ShouldGetMultipleCookies() Assert.AreEqual(cookie.Expires, -1); Assert.IsFalse(cookie.HttpOnly); Assert.IsFalse(cookie.Secure); - Assert.AreEqual(TestConstants.IsChromium ? SameSiteAttribute.Lax : SameSiteAttribute.None, cookie.SameSite); + Assert.AreEqual(DefaultSameSiteCookieValue, cookie.SameSite); cookie = cookies[1]; Assert.AreEqual("username", cookie.Name); @@ -148,7 +148,7 @@ public async Task ShouldGetMultipleCookies() Assert.AreEqual(cookie.Expires, -1); Assert.IsFalse(cookie.HttpOnly); Assert.IsFalse(cookie.Secure); - Assert.AreEqual(TestConstants.IsChromium ? SameSiteAttribute.Lax : SameSiteAttribute.None, cookie.SameSite); + Assert.AreEqual(DefaultSameSiteCookieValue, cookie.SameSite); } [PlaywrightTest("browsercontext-cookies.spec.ts", "should get cookies from multiple urls")] @@ -187,7 +187,7 @@ await Context.AddCookiesAsync(new[] Assert.AreEqual(cookie.Expires, -1); Assert.IsFalse(cookie.HttpOnly); Assert.IsTrue(cookie.Secure); - Assert.AreEqual(TestConstants.IsChromium ? SameSiteAttribute.Lax : SameSiteAttribute.None, cookie.SameSite); + Assert.AreEqual(DefaultSameSiteCookieValue, cookie.SameSite); cookie = cookies[1]; Assert.AreEqual("doggo", cookie.Name); @@ -197,6 +197,6 @@ await Context.AddCookiesAsync(new[] Assert.AreEqual(cookie.Expires, -1); Assert.IsFalse(cookie.HttpOnly); Assert.IsTrue(cookie.Secure); - Assert.AreEqual(TestConstants.IsChromium ? SameSiteAttribute.Lax : SameSiteAttribute.None, cookie.SameSite); + Assert.AreEqual(DefaultSameSiteCookieValue, cookie.SameSite); } } diff --git a/src/Playwright.Tests/BrowserContextFetchTests.cs b/src/Playwright.Tests/BrowserContextFetchTests.cs index fa2dfe5576..400c636a84 100644 --- a/src/Playwright.Tests/BrowserContextFetchTests.cs +++ b/src/Playwright.Tests/BrowserContextFetchTests.cs @@ -312,6 +312,38 @@ public async Task ShouldWorkWithHttpCredentials() Assert.AreEqual("/empty.html", requestURL); } + [PlaywrightTest("browsercontext-fetch.spec.ts", "should support HTTPCredentials.send")] + public async Task ShouldSupportHttpCredentialsSend() + { + var context = await Browser.NewContextAsync(new() + { + HttpCredentials = new() + { + Username = "user", + Password = "pass", + Origin = Server.Prefix.ToUpperInvariant(), + Send = HttpCredentialsSend.Always + } + }); + { + var (requestHeaders, response) = await TaskUtils.WhenAll( + Server.WaitForRequest("/empty.html", request => request.Headers.ToDictionary(header => header.Key, header => header.Value)), + context.APIRequest.GetAsync(Server.EmptyPage) + ); + Assert.AreEqual(requestHeaders["Authorization"], "Basic " + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("user:pass"))); + Assert.AreEqual(200, response.Status); + } + { + var (requestHeaders, response) = await TaskUtils.WhenAll( + Server.WaitForRequest("/empty.html", request => request.Headers.ToDictionary(header => header.Key, header => header.Value)), + context.APIRequest.GetAsync(Server.CrossProcessPrefix + "/empty.html") + ); + Assert.AreEqual(200, response.Status); + // Not sent to another origin. + Assert.AreEqual(false, requestHeaders.ContainsKey("Authorization")); + } + } + [PlaywrightTest("browsercontext-fetch.spec.ts", "delete should support post data")] public async Task DeleteShouldSupportPostData() { diff --git a/src/Playwright.Tests/BrowserContextStorageStateTests.cs b/src/Playwright.Tests/BrowserContextStorageStateTests.cs index d7b61d69cd..3ecee2ee5f 100644 --- a/src/Playwright.Tests/BrowserContextStorageStateTests.cs +++ b/src/Playwright.Tests/BrowserContextStorageStateTests.cs @@ -124,14 +124,6 @@ public async Task ShouldCaptureCookies() var storageState = await Context.StorageStateAsync(); StringAssert.Contains(@"""name"":""a"",""value"":""b""", storageState); StringAssert.Contains(@"""name"":""empty"",""value"":""""", storageState); - if (TestConstants.IsWebKit || TestConstants.IsFirefox) - { - StringAssert.Contains(@"""sameSite"":""None""", storageState); - } - else - { - StringAssert.Contains(@"""sameSite"":""Lax""", storageState); - } StringAssert.DoesNotContain(@"""url"":null", storageState); await using var context2 = await Browser.NewContextAsync(new() { StorageState = storageState }); diff --git a/src/Playwright.Tests/BrowserTypeConnectTests.cs b/src/Playwright.Tests/BrowserTypeConnectTests.cs index 13964e7dcd..f41561b093 100644 --- a/src/Playwright.Tests/BrowserTypeConnectTests.cs +++ b/src/Playwright.Tests/BrowserTypeConnectTests.cs @@ -511,6 +511,98 @@ public async Task SetInputFilesShouldPreserveLastModifiedTimestamp() Assert.LessOrEqual(Math.Abs(timestamps[i] - expectedTimestamps[i]), 1000); } + [PlaywrightTest("page-set-input-files.spec.ts", "should upload a folder")] + public async Task ShouldUploadAFolder() + { + var browser = await BrowserType.ConnectAsync(_remoteServer.WSEndpoint); + var context = await browser.NewContextAsync(); + var page = await context.NewPageAsync(); + + await page.GotoAsync(Server.Prefix + "/input/folderupload.html"); + var input = await page.QuerySelectorAsync("input"); + using var tmpDir = new TempDirectory(); + var dir = Path.Combine(tmpDir.Path, "file-upload-test"); + { + Directory.CreateDirectory(dir); + File.WriteAllText(Path.Combine(dir, "file1.txt"), "file1 content"); + File.WriteAllText(Path.Combine(dir, "file2"), "file2 content"); + Directory.CreateDirectory(Path.Combine(dir, "sub-dir")); + File.WriteAllText(Path.Combine(dir, "sub-dir", "really.txt"), "sub-dir file content"); + } + await input.SetInputFilesAsync(dir); + var webkitRelativePaths = await input.EvaluateAsync("e => [...e.files].map(f => f.webkitRelativePath)"); + Assert.True(new HashSet { "file-upload-test/sub-dir/really.txt", "file-upload-test/file1.txt", "file-upload-test/file2" }.SetEquals(new HashSet(webkitRelativePaths))); + for (var i = 0; i < webkitRelativePaths.Length; i++) + { + var content = await input.EvaluateAsync(@"(e, i) => { + const reader = new FileReader(); + const promise = new Promise(fulfill => reader.onload = fulfill); + reader.readAsText(e.files[i]); + return promise.then(() => reader.result); + }", i); + Assert.AreEqual(File.ReadAllText(Path.Combine(dir, "..", webkitRelativePaths[i])), content); + } + } + + [PlaywrightTest("page-set-input-files.spec.ts", "should upload a folder and throw for multiple directories")] + public async Task ShouldUploadAFolderAndThrowForMultipleDirectories() + { + var browser = await BrowserType.ConnectAsync(_remoteServer.WSEndpoint); + var context = await browser.NewContextAsync(); + var page = await context.NewPageAsync(); + + await page.GotoAsync(Server.Prefix + "/input/folderupload.html"); + var input = await page.QuerySelectorAsync("input"); + using var tmpDir = new TempDirectory(); + var dir = Path.Combine(tmpDir.Path, "file-upload-test"); + { + Directory.CreateDirectory(Path.Combine(dir, "folder1")); + File.WriteAllText(Path.Combine(dir, "folder1", "file1.txt"), "file1 content"); + Directory.CreateDirectory(Path.Combine(dir, "folder2")); + File.WriteAllText(Path.Combine(dir, "folder2", "file2.txt"), "file2 content"); + } + var ex = await PlaywrightAssert.ThrowsAsync(() => input.SetInputFilesAsync(new string[] { Path.Combine(dir, "folder1"), Path.Combine(dir, "folder2") })); + Assert.AreEqual("Multiple directories are not supported", ex.Message); + } + + [PlaywrightTest("page-set-input-files.spec.ts", "should throw if a directory and files are passed")] + public async Task ShouldThrowIfADirectoryAndFilesArePassed() + { + var browser = await BrowserType.ConnectAsync(_remoteServer.WSEndpoint); + var context = await browser.NewContextAsync(); + var page = await context.NewPageAsync(); + + await page.GotoAsync(Server.Prefix + "/input/folderupload.html"); + var input = await page.QuerySelectorAsync("input"); + using var tmpDir = new TempDirectory(); + var dir = Path.Combine(tmpDir.Path, "file-upload-test"); + { + Directory.CreateDirectory(Path.Combine(dir, "folder1")); + File.WriteAllText(Path.Combine(dir, "folder1", "file1.txt"), "file1 content"); + } + var ex = await PlaywrightAssert.ThrowsAsync(() => input.SetInputFilesAsync(new string[] { Path.Combine(dir, "folder1"), Path.Combine(dir, "folder1", "file1.txt") })); + Assert.AreEqual("File paths must be all files or a single directory", ex.Message); + } + + [PlaywrightTest("page-set-input-files.spec.ts", "should throw when uploading a folder in a normal file upload input")] + public async Task ShouldThrowWhenUploadingAFolderInANormalFileUploadInput() + { + var browser = await BrowserType.ConnectAsync(_remoteServer.WSEndpoint); + var context = await browser.NewContextAsync(); + var page = await context.NewPageAsync(); + + await page.GotoAsync(Server.Prefix + "/input/fileupload.html"); + var input = await page.QuerySelectorAsync("input"); + using var tmpDir = new TempDirectory(); + var dir = Path.Combine(tmpDir.Path, "file-upload-test"); + { + Directory.CreateDirectory(dir); + File.WriteAllText(Path.Combine(dir, "file1.txt"), "file1 content"); + } + var ex = await PlaywrightAssert.ThrowsAsync(() => input.SetInputFilesAsync(dir)); + Assert.AreEqual("Error: File input does not support directories, pass individual files instead", ex.Message); + } + [PlaywrightTest("browsertype-connect.spec.ts", "should print custom ws close error")] public async Task ShouldPrintCustomWsCloseError() { diff --git a/src/Playwright.Tests/DefaultBrowserContext1Tests.cs b/src/Playwright.Tests/DefaultBrowserContext1Tests.cs index 404ed3dcb3..3ae3558c29 100644 --- a/src/Playwright.Tests/DefaultBrowserContext1Tests.cs +++ b/src/Playwright.Tests/DefaultBrowserContext1Tests.cs @@ -50,7 +50,7 @@ public async Task ContextCookiesShouldWork() Assert.AreEqual(-1, cookie.Expires); Assert.IsFalse(cookie.HttpOnly); Assert.IsFalse(cookie.Secure); - Assert.AreEqual(TestConstants.IsChromium ? SameSiteAttribute.Lax : SameSiteAttribute.None, cookie.SameSite); + Assert.AreEqual(DefaultSameSiteCookieValue, cookie.SameSite); await context.DisposeAsync(); tmp.Dispose(); @@ -82,7 +82,7 @@ await context.AddCookiesAsync(new[] Assert.AreEqual(-1, cookie.Expires); Assert.IsFalse(cookie.HttpOnly); Assert.IsFalse(cookie.Secure); - Assert.AreEqual(TestConstants.IsChromium ? SameSiteAttribute.Lax : SameSiteAttribute.None, cookie.SameSite); + Assert.AreEqual(DefaultSameSiteCookieValue, cookie.SameSite); await context.DisposeAsync(); tmp.Dispose(); diff --git a/src/Playwright.Tests/GlobalFetchTests.cs b/src/Playwright.Tests/GlobalFetchTests.cs index aac9c0d7a0..26386abaef 100644 --- a/src/Playwright.Tests/GlobalFetchTests.cs +++ b/src/Playwright.Tests/GlobalFetchTests.cs @@ -181,7 +181,6 @@ public async Task ShouldReturnErrorWithCorrectCredentialsAndMismatchingPort() await request.DisposeAsync(); } - [PlaywrightTest("global-fetch.spec.ts", "should use proxy")] [Ignore("Fetch API is using the CONNECT Http proxy server method all the time")] public async Task ShouldUseProxy() @@ -193,6 +192,31 @@ public async Task ShouldUseProxy() await request.DisposeAsync(); } + [PlaywrightTest("global-fetch.spec.ts", "should support HTTPCredentials.send")] + public async Task ShouldSupportHTTPCredentialsSend() + { + var request = await Playwright.APIRequest.NewContextAsync(new() { HttpCredentials = new() { Username = "user", Password = "pass", Origin = Server.Prefix.ToUpperInvariant(), Send = HttpCredentialsSend.Always } }); + { + var (requestHeaders, response) = await TaskUtils.WhenAll( + Server.WaitForRequest("/empty.html", request => request.Headers.ToDictionary(header => header.Key, header => header.Value)), + request.GetAsync(Server.EmptyPage) + ); + Assert.AreEqual(requestHeaders["Authorization"], "Basic " + Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("user:pass"))); + Assert.AreEqual(200, response.Status); + } + { + var (requestHeaders, response) = await TaskUtils.WhenAll( + Server.WaitForRequest("/empty.html", request => request.Headers.ToDictionary(header => header.Key, header => header.Value)), + request.GetAsync(Server.CrossProcessPrefix + "/empty.html") + ); + Assert.AreEqual(200, response.Status); + // Not sent to another origin. + Assert.AreEqual(false, requestHeaders.ContainsKey("Authorization")); + } + await request.DisposeAsync(); + } + + [PlaywrightTest("global-fetch.spec.ts", "should support global ignoreHTTPSErrors option")] public async Task ShouldSupportGlobalIgnoreHTTPSErrorsOption() { diff --git a/src/Playwright.Tests/PageClockTests.cs b/src/Playwright.Tests/PageClockTests.cs new file mode 100644 index 0000000000..9256fe7860 --- /dev/null +++ b/src/Playwright.Tests/PageClockTests.cs @@ -0,0 +1,542 @@ +/* + * 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.Text.Json; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.Playwright.Tests; + +public class PageClockTests : PageTestEx +{ + private readonly List _calls = []; + + [SetUp] + public async Task SetUp() + { + _calls.Clear(); + await Page.ExposeFunctionAsync("stub", () => + { + _calls.Add([]); + }); + await Page.ExposeFunctionAsync("stubWithNumberValue", (int value) => + { + _calls.Add([value]); + }); + await Page.ExposeFunctionAsync("stubWithStringValue", (string value) => + { + _calls.Add([value]); + }); + } + + public class RunForTests : PageClockTests + { + [SetUp] + public async Task RunForSetUp() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.Clock.PauseAtAsync(1000); + } + + [PlaywrightTest("page-clock.spec.ts", "triggers immediately without specified delay")] + public async Task RunForTriggersImmediatelyWithoutSpecifiedDelay() + { + await Page.EvaluateAsync("() => { setTimeout(window.stub); }"); + + await Page.Clock.RunForAsync(0); + Assert.AreEqual(1, _calls.Count); + } + + [PlaywrightTest("page-clock.spec.ts", "does not trigger without sufficient delay")] + public async Task DoesNotTriggerWithoutSufficientDelay() + { + await Page.EvaluateAsync("() => { setTimeout(window.stub, 100); }"); + await Page.Clock.RunForAsync(10); + Assert.IsEmpty(_calls); + } + + [PlaywrightTest("page-clock.spec.ts", "triggers after sufficient delay")] + public async Task TriggersAfterSufficientDelay() + { + await Page.EvaluateAsync("() => { setTimeout(window.stub, 100); }"); + await Page.Clock.RunForAsync(100); + Assert.AreEqual(1, _calls.Count); + } + + [PlaywrightTest("page-clock.spec.ts", "triggers simultaneous timers")] + public async Task TriggersSimultaneousTimers() + { + await Page.EvaluateAsync("() => { setTimeout(window.stub, 100); setTimeout(window.stub, 100); }"); + await Page.Clock.RunForAsync(100); + Assert.AreEqual(2, _calls.Count); + } + + [PlaywrightTest("page-clock.spec.ts", "triggers multiple simultaneous timers")] + public async Task TriggersMultipleSimultaneousTimers() + { + await Page.EvaluateAsync("() => { setTimeout(window.stub, 100); setTimeout(window.stub, 100); setTimeout(window.stub, 99); setTimeout(window.stub, 100); }"); + await Page.Clock.RunForAsync(100); + Assert.AreEqual(4, _calls.Count); + } + + [PlaywrightTest("page-clock.spec.ts", "waits after setTimeout was called")] + public async Task WaitsAfterSetTimeoutWasCalled() + { + await Page.EvaluateAsync("() => { setTimeout(window.stub, 150); }"); + await Page.Clock.RunForAsync(50); + Assert.IsEmpty(_calls); + await Page.Clock.RunForAsync(100); + Assert.AreEqual(1, _calls.Count); + } + + [PlaywrightTest("page-clock.spec.ts", "triggers when some throw")] + public async Task TriggersWhenSomeThrow() + { + await Page.EvaluateAsync("() => { setTimeout(() => { throw new Error(); }, 100); setTimeout(window.stub, 120); }"); + await PlaywrightAssert.ThrowsAsync(() => Page.Clock.RunForAsync(120)); + Assert.AreEqual(1, _calls.Count); + } + + [PlaywrightTest("page-clock.spec.ts", "creates updated Date while ticking")] + public async Task CreatesUpdatedDateWhileTicking() + { + await Page.Clock.SetSystemTimeAsync(0); + await Page.EvaluateAsync("() => { setInterval(() => { window.stubWithNumberValue(new Date().getTime()); }, 10); }"); + await Page.Clock.RunForAsync(100); + Assert.AreEqual(new[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }, _calls.Select(c => c[0])); + } + + [PlaywrightTest("page-clock.spec.ts", "passes 8 seconds")] + public async Task Passes8Seconds() + { + await Page.EvaluateAsync("() => { setInterval(window.stub, 4000); }"); + await Page.Clock.RunForAsync("08"); + Assert.AreEqual(2, _calls.Count); + } + + [PlaywrightTest("page-clock.spec.ts", "passes 1 minute")] + public async Task Passes1Minute() + { + await Page.EvaluateAsync("() => { setInterval(window.stub, 6000); }"); + await Page.Clock.RunForAsync("01:00"); + Assert.AreEqual(10, _calls.Count); + } + + [PlaywrightTest("page-clock.spec.ts", "passes 2 hours, 34 minutes and 10 seconds")] + public async Task Passes2Hours34MinutesAnd10Seconds() + { + await Page.EvaluateAsync("() => { setInterval(window.stub, 10000); }"); + await Page.Clock.RunForAsync("02:34:10"); + Assert.AreEqual(925, _calls.Count); + } + + [PlaywrightTest("page-clock.spec.ts", "throws for invalid format")] + public async Task ThrowsForInvalidFormat() + { + await Page.EvaluateAsync("() => { setInterval(window.stub, 10000); }"); + await PlaywrightAssert.ThrowsAsync(() => Page.Clock.RunForAsync("12:02:34:10")); + Assert.IsEmpty(_calls); + } + + [PlaywrightTest("page-clock.spec.ts", "returns the current now value")] + public async Task ReturnsTheCurrentNowValue() + { + await Page.Clock.SetSystemTimeAsync(0); + var value = 200; + await Page.Clock.RunForAsync(value); + Assert.AreEqual(value, await Page.EvaluateAsync("Date.now()")); + } + } + + public class FastForwardTests : PageClockTests + { + [SetUp] + public async Task FastForwardSetUp() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.Clock.PauseAtAsync(1000); + } + + [PlaywrightTest("page-clock.spec.ts", "ignores timers which wouldn't be run")] + public async Task IgnoresTimersWhichWouldntBeRun() + { + await Page.EvaluateAsync("() => { setTimeout(() => { window.stubWithStringValue('should not be logged'); }, 1000); }"); + await Page.Clock.FastForwardAsync(500); + Assert.IsEmpty(_calls); + } + + [PlaywrightTest("page-clock.spec.ts", "pushes back execution time for skipped timers")] + public async Task PushesBackExecutionTimeForSkippedTimers() + { + await Page.EvaluateAsync("() => { setTimeout(() => { window.stubWithNumberValue(Date.now()); }, 1000); }"); + await Page.Clock.FastForwardAsync(2000); + Assert.AreEqual(1, _calls.Count); + Assert.AreEqual(1000 + 2000, _calls[0][0]); + } + + [PlaywrightTest("page-clock.spec.ts", "supports string time arguments")] + public async Task SupportsStringTimeArguments() + { + await Page.EvaluateAsync("() => { setTimeout(() => { window.stubWithNumberValue(Date.now()); }, 100000); }"); // 100000 = 1:40 + await Page.Clock.FastForwardAsync("01:50"); + Assert.AreEqual(1, _calls.Count); + Assert.AreEqual(111000, _calls[0][0]); + } + } + + public class StubTimersTests : PageClockTests + { + [SetUp] + public async Task StubTimersSetUp() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.Clock.PauseAtAsync(1000); + } + + [PlaywrightTest("page-clock.spec.ts", "sets initial timestamp")] + public async Task SetsInitialTimestamp() + { + await Page.Clock.SetSystemTimeAsync(1400); + Assert.AreEqual(1400, await Page.EvaluateAsync("Date.now()")); + } + + [PlaywrightTest("page-clock.spec.ts", "should throw for invalid date")] + public async Task ShouldThrowForInvalidDate() + { + await PlaywrightAssert.ThrowsAsync(() => Page.Clock.SetSystemTimeAsync("invalid")); + } + + [PlaywrightTest("page-clock.spec.ts", "replaces global setTimeout")] + public async Task ReplacesGlobalSetTimeout() + { + await Page.EvaluateAsync("() => { setTimeout(window.stub, 1000); }"); + await Page.Clock.RunForAsync(1000); + Assert.AreEqual(1, _calls.Count); + } + + [PlaywrightTest("page-clock.spec.ts", "global fake setTimeout should return id")] + public async Task GlobalFakeSetTimeoutShouldReturnId() + { + var id = await Page.EvaluateAsync("() => setTimeout(window.stub, 1000)"); + Assert.IsTrue(id > 0); + } + + [PlaywrightTest("page-clock.spec.ts", "replaces global clearTimeout")] + public async Task ReplacesGlobalClearTimeout() + { + await Page.EvaluateAsync("() => { const id = setTimeout(window.stub, 1000); clearTimeout(id); }"); + await Page.Clock.RunForAsync(1000); + Assert.IsEmpty(_calls); + } + + [PlaywrightTest("page-clock.spec.ts", "replaces global setInterval")] + public async Task ReplacesGlobalSetInterval() + { + await Page.EvaluateAsync("() => { setInterval(window.stub, 500); }"); + await Page.Clock.RunForAsync(1000); + Assert.AreEqual(2, _calls.Count); + } + + [PlaywrightTest("page-clock.spec.ts", "replaces global clearInterval")] + public async Task ReplacesGlobalClearInterval() + { + await Page.EvaluateAsync("() => { const id = setInterval(window.stub, 500); clearInterval(id); }"); + await Page.Clock.RunForAsync(1000); + Assert.IsEmpty(_calls); + } + + [PlaywrightTest("page-clock.spec.ts", "replaces global performance.now")] + public async Task ReplacesGlobalPerformanceNow() + { + var task = Page.EvaluateAsync("async () => { const prev = performance.now(); await new Promise(f => setTimeout(f, 1000)); const next = performance.now(); return { prev, next }; }"); + await Page.Clock.RunForAsync(1000); + var result = await task; + Assert.AreEqual(1000, result.GetProperty("prev").GetInt32()); + Assert.AreEqual(2000, result.GetProperty("next").GetInt32()); + } + + [PlaywrightTest("page-clock.spec.ts", "fakes Date constructor")] + public async Task FakesDateConstructor() + { + var now = await Page.EvaluateAsync("() => new Date().getTime()"); + Assert.AreEqual(1000, now); + } + } + + public class StubTimerTests : PageClockTests + { + [PlaywrightTest("page-clock.spec.ts", "replaces global performance.timeOrigin")] + public async Task ReplacesGlobalPerformanceTimeOrigin() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 1000 }); + await Page.Clock.PauseAtAsync(2000); + var promise = Page.EvaluateAsync("async () => { const prev = performance.now(); await new Promise(f => setTimeout(f, 1000)); const next = performance.now(); return { prev, next }; }"); + await Page.Clock.RunForAsync(1000); + Assert.AreEqual(1000, await Page.EvaluateAsync("performance.timeOrigin")); + var result = await promise; + Assert.AreEqual(1000, result.GetProperty("prev").GetInt32()); + Assert.AreEqual(2000, result.GetProperty("next").GetInt32()); + } + } + + public class PopupTests : PageClockTests + { + [PlaywrightTest("page-clock.spec.ts", "should tick after popup")] + public async Task ShouldTickAfterPopup() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + var now = new DateTime(2015, 9, 25); + await Page.Clock.PauseAtAsync(now); + var popupTask = Page.WaitForPopupAsync(); + await Page.EvaluateAsync("() => window.open('about:blank')"); + var popup = await popupTask; + var popupTime = await popup.EvaluateAsync("() => Date.now()"); + Assert.AreEqual(((DateTimeOffset)now).ToUnixTimeMilliseconds(), popupTime); + await Page.Clock.RunForAsync(1000); + var popupTimeAfter = await popup.EvaluateAsync("() => Date.now()"); + Assert.AreEqual(((DateTimeOffset)now).ToUnixTimeMilliseconds() + 1000, popupTimeAfter); + } + + [PlaywrightTest("page-clock.spec.ts", "should tick before popup")] + public async Task ShouldTickBeforePopup() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + var now = new DateTime(2015, 9, 25); + await Page.Clock.PauseAtAsync(now); + await Page.Clock.RunForAsync(1000); + var popupTask = Page.WaitForPopupAsync(); + await Page.EvaluateAsync("() => window.open('about:blank')"); + var popup = await popupTask; + var popupTime = await popup.EvaluateAsync("() => Date.now()"); + Assert.AreEqual(((DateTimeOffset)now).ToUnixTimeMilliseconds() + 1000, popupTime); + } + + [PlaywrightTest("page-clock.spec.ts", "should run time before popup")] + public async Task ShouldRunTimeBeforePopup() + { + Server.SetRoute("/popup.html", context => + { + context.Response.Headers["Content-Type"] = "text/html"; + return context.Response.WriteAsync(""); + }); + await Page.GotoAsync(Server.EmptyPage); + // Wait for 2 seconds in real life to check that it is past in popup. + await Page.WaitForTimeoutAsync(2000); + var popupTask = Page.WaitForPopupAsync(); + await Page.EvaluateAsync("url => window.open(url)", Server.Prefix + "/popup.html"); + var popup = await popupTask; + var popupTime = await popup.EvaluateAsync("window.time"); + Assert.GreaterOrEqual(popupTime, 2000); + } + + [PlaywrightTest("page-clock.spec.ts", "should not run time before popup on pause")] + public async Task ShouldNotRunTimeBeforePopupOnPause() + { + Server.SetRoute("/popup.html", context => + { + context.Response.Headers["Content-Type"] = "text/html"; + return context.Response.WriteAsync(""); + }); + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.Clock.PauseAtAsync(1000); + await Page.GotoAsync(Server.EmptyPage); + // Wait for 2 seconds in real life to check that it is past in popup. + await Page.WaitForTimeoutAsync(2000); + var popupTask = Page.WaitForPopupAsync(); + await Page.EvaluateAsync("url => window.open(url)", Server.Prefix + "/popup.html"); + var popup = await popupTask; + var popupTime = await popup.EvaluateAsync("window.time"); + Assert.AreEqual(1000, popupTime); + } + } + + public class SetFixedTimeTests : PageClockTests + { + [PlaywrightTest("page-clock.spec.ts", "does not fake methods")] + public async Task DoesNotFakeMethods() + { + await Page.Clock.SetFixedTimeAsync(0); + // Should not stall. + await Page.EvaluateAsync("() => new Promise(f => setTimeout(f, 1))"); + } + + [PlaywrightTest("page-clock.spec.ts", "allows setting time multiple times")] + public async Task AllowsSettingTimeMultipleTimes() + { + await Page.Clock.SetFixedTimeAsync(100); + Assert.AreEqual(100, await Page.EvaluateAsync("Date.now()")); + await Page.Clock.SetFixedTimeAsync(200); + Assert.AreEqual(200, await Page.EvaluateAsync("Date.now()")); + } + + [PlaywrightTest("page-clock.spec.ts", "fixed time is not affected by clock manipulation")] + public async Task FixedTimeIsNotAffectedByClockManipulation() + { + await Page.Clock.SetFixedTimeAsync(100); + Assert.AreEqual(100, await Page.EvaluateAsync("Date.now()")); + await Page.Clock.FastForwardAsync(20); + Assert.AreEqual(100, await Page.EvaluateAsync("Date.now()")); + } + + [PlaywrightTest("page-clock.spec.ts", "allows installing fake timers after setting time")] + public async Task AllowsInstallingFakeTimersAfterSettingTime() + { + await Page.Clock.SetFixedTimeAsync(100); + Assert.AreEqual(100, await Page.EvaluateAsync("Date.now()")); + await Page.Clock.SetFixedTimeAsync(200); + await Page.EvaluateAsync("() => { setTimeout(() => window.stubWithNumberValue(Date.now()), 0); }"); + await Page.Clock.RunForAsync(0); + Assert.AreEqual(1, _calls.Count); + Assert.AreEqual(200, _calls[0][0]); + } + } + + public class WhileRunningTests : PageClockTests + { + [PlaywrightTest("page-clock.spec.ts", "should progress time")] + public async Task ShouldProgressTime() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.GotoAsync("data:text/html,"); + await Page.WaitForTimeoutAsync(1000); + var now = await Page.EvaluateAsync("Date.now()"); + Assert.GreaterOrEqual(now, 1000); + Assert.LessOrEqual(now, 2000); + } + + [PlaywrightTest("page-clock.spec.ts", "should runFor")] + public async Task ShouldRunFor() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.GotoAsync("data:text/html,"); + await Page.Clock.RunForAsync(10000); + var now = await Page.EvaluateAsync("Date.now()"); + Assert.GreaterOrEqual(now, 10000); + Assert.LessOrEqual(now, 11000); + } + + [PlaywrightTest("page-clock.spec.ts", "should fastForward")] + public async Task ShouldFastForward() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.GotoAsync("data:text/html,"); + await Page.Clock.FastForwardAsync(10000); + var now = await Page.EvaluateAsync("Date.now()"); + Assert.GreaterOrEqual(now, 10000); + Assert.LessOrEqual(now, 11000); + } + + [PlaywrightTest("page-clock.spec.ts", "should fastForwardTo")] + public async Task ShouldFastForwardTo() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.GotoAsync("data:text/html,"); + await Page.Clock.FastForwardAsync(10000); + var now = await Page.EvaluateAsync("Date.now()"); + Assert.GreaterOrEqual(now, 10000); + Assert.LessOrEqual(now, 11000); + } + + [PlaywrightTest("page-clock.spec.ts", "should pause")] + public async Task ShouldPause() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.GotoAsync("data:text/html,"); + await Page.Clock.PauseAtAsync(1000); + await Page.WaitForTimeoutAsync(1000); + await Page.Clock.ResumeAsync(); + var now = await Page.EvaluateAsync("Date.now()"); + Assert.GreaterOrEqual(now, 0); + Assert.LessOrEqual(now, 1000); + } + + [PlaywrightTest("page-clock.spec.ts", "should pause and fastForward")] + public async Task ShouldPauseAndFastForward() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.GotoAsync("data:text/html,"); + await Page.Clock.PauseAtAsync(1000); + await Page.Clock.FastForwardAsync(1000); + var now = await Page.EvaluateAsync("Date.now()"); + Assert.AreEqual(2000, now); + } + + [PlaywrightTest("page-clock.spec.ts", "should set system time on pause")] + public async Task ShouldSetSystemTimeOnPause() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.GotoAsync("data:text/html,"); + await Page.Clock.PauseAtAsync(1000); + var now = await Page.EvaluateAsync("Date.now()"); + Assert.AreEqual(1000, now); + } + } + + public class WhileOnPauseTests : PageClockTests + { + [PlaywrightTest("page-clock.spec.ts", "fastForward should not run nested immediate")] + public async Task FastForwardShouldNotRunNestedImmediate() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.GotoAsync("data:text/html,"); + await Page.Clock.PauseAtAsync(1000); + await Page.EvaluateAsync("() => { setTimeout(() => { window.stubWithStringValue('outer'); setTimeout(() => window.stubWithStringValue('inner'), 0); }, 1000); }"); + await Page.Clock.FastForwardAsync(1000); + Assert.AreEqual(1, _calls.Count); + Assert.AreEqual("outer", _calls[0][0]); + await Page.Clock.FastForwardAsync(1); + Assert.AreEqual(2, _calls.Count); + Assert.AreEqual("inner", _calls[1][0]); + } + + [PlaywrightTest("page-clock.spec.ts", "runFor should not run nested immediate")] + public async Task RunForShouldNotRunNestedImmediate() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.GotoAsync("data:text/html,"); + await Page.Clock.PauseAtAsync(1000); + await Page.EvaluateAsync("() => { setTimeout(() => { window.stubWithStringValue('outer'); setTimeout(() => window.stubWithStringValue('inner'), 0); }, 1000); }"); + await Page.Clock.RunForAsync(1000); + Assert.AreEqual(1, _calls.Count); + Assert.AreEqual("outer", _calls[0][0]); + await Page.Clock.RunForAsync(1); + Assert.AreEqual(2, _calls.Count); + Assert.AreEqual("inner", _calls[1][0]); + } + + [PlaywrightTest("page-clock.spec.ts", "runFor should not run nested immediate from microtask")] + public async Task RunForShouldNotRunNestedImmediateFromMicrotask() + { + await Page.Clock.InstallAsync(new() { TimeInt64 = 0 }); + await Page.GotoAsync("data:text/html,"); + await Page.Clock.PauseAtAsync(1000); + await Page.EvaluateAsync("() => { setTimeout(() => { window.stubWithStringValue('outer'); Promise.resolve().then(() => setTimeout(() => window.stubWithStringValue('inner'), 0)); }, 1000); }"); + await Page.Clock.RunForAsync(1000); + Assert.AreEqual(1, _calls.Count); + Assert.AreEqual("outer", _calls[0][0]); + await Page.Clock.RunForAsync(1); + Assert.AreEqual(2, _calls.Count); + Assert.AreEqual("inner", _calls[1][0]); + } + } +} diff --git a/src/Playwright.Tests/PageNetworkRequestTest.cs b/src/Playwright.Tests/PageNetworkRequestTest.cs index 57909db048..51eb435ce1 100644 --- a/src/Playwright.Tests/PageNetworkRequestTest.cs +++ b/src/Playwright.Tests/PageNetworkRequestTest.cs @@ -345,6 +345,7 @@ public async Task ShouldReportRawHeaders() return new Header { Name = e.Name, Value = values[0] }; }).ToList(); } + expectedHeaders = expectedHeaders.Where(h => h.Name.ToLowerInvariant() != "priority").ToList(); await ctx.Response.CompleteAsync(); }); diff --git a/src/Playwright.Tests/PageRequestContinueTests.cs b/src/Playwright.Tests/PageRequestContinueTests.cs index f849411427..78da8103ef 100644 --- a/src/Playwright.Tests/PageRequestContinueTests.cs +++ b/src/Playwright.Tests/PageRequestContinueTests.cs @@ -141,4 +141,55 @@ await Page.RouteAsync("**/empty.html", async (route) => Assert.AreEqual("New URL must have same protocol as overridden URL", exception.Message); await PlaywrightAssert.ThrowsAsync(() => gotoTask); } + + [PlaywrightTest("page-request-continue.spec.ts", "continue should not change multipart/form-data body")] + public async Task ContinueShouldNotChangeMultipartFormDataBody() + { + await Page.GotoAsync(Server.EmptyPage); + Server.SetRoute("/upload", context => + { + context.Response.Headers["Content-Type"] = "text/plain"; + return Task.CompletedTask; + }); + + async Task SendFormData() + { + var requestPostBody = Server.WaitForRequest("/upload", request => + { + using StreamReader reader = new(request.Body); + return reader.ReadToEndAsync().GetAwaiter().GetResult(); + }); + var status = await Page.EvaluateAsync(@"async () => { + const newFile = new File(['file content'], 'file.txt'); + const formData = new FormData(); + formData.append('file', newFile); + const response = await fetch('/upload', { + method: 'POST', + credentials: 'include', + body: formData, + }); + return response.status; + }"); + Assert.AreEqual(200, status); + return await requestPostBody; + } + + var reqBefore = await SendFormData(); + await Page.RouteAsync("**/*", async (route) => + { + await route.ContinueAsync(); + }); + var reqAfter = await SendFormData(); + var fileContent = string.Join("\r\n", new[] + { + "Content-Disposition: form-data; name=\"file\"; filename=\"file.txt\"", + "Content-Type: application/octet-stream", + "", + "file content", + "------" + }); + StringAssert.Contains(fileContent, reqBefore); + StringAssert.Contains(fileContent, reqAfter); + } } + diff --git a/src/Playwright.Tests/PageSetInputFilesTests.cs b/src/Playwright.Tests/PageSetInputFilesTests.cs index dae72e453b..173e58150f 100644 --- a/src/Playwright.Tests/PageSetInputFilesTests.cs +++ b/src/Playwright.Tests/PageSetInputFilesTests.cs @@ -424,7 +424,7 @@ public async Task ShouldUploadMultipleLargeFiles() } } - [PlaywrightTest("browsertype-connect.spec.ts", "should preserve lastModified timestamp")] + [PlaywrightTest("page-set-input-files.spec.ts", "should preserve lastModified timestamp")] public async Task ShouldPreserveLastModifiedTimestamp() { await Page.SetContentAsync(""); @@ -440,4 +440,80 @@ public async Task ShouldPreserveLastModifiedTimestamp() for (var i = 0; i < timestamps.Length; i++) Assert.LessOrEqual(Math.Abs(timestamps[i] - expectedTimestamps[i]), 1000); } + + [PlaywrightTest("page-set-input-files.spec.ts", "should upload a folder")] + public async Task ShouldUploadAFolder() + { + await Page.GotoAsync(Server.Prefix + "/input/folderupload.html"); + var input = await Page.QuerySelectorAsync("input"); + using var tmpDir = new TempDirectory(); + var dir = Path.Combine(tmpDir.Path, "file-upload-test"); + { + Directory.CreateDirectory(dir); + File.WriteAllText(Path.Combine(dir, "file1.txt"), "file1 content"); + File.WriteAllText(Path.Combine(dir, "file2"), "file2 content"); + Directory.CreateDirectory(Path.Combine(dir, "sub-dir")); + File.WriteAllText(Path.Combine(dir, "sub-dir", "really.txt"), "sub-dir file content"); + } + await input.SetInputFilesAsync(dir); + var webkitRelativePaths = await input.EvaluateAsync("e => [...e.files].map(f => f.webkitRelativePath)"); + Assert.True(new HashSet { "file-upload-test/sub-dir/really.txt", "file-upload-test/file1.txt", "file-upload-test/file2" }.SetEquals(new HashSet(webkitRelativePaths))); + for (var i = 0; i < webkitRelativePaths.Length; i++) + { + var content = await input.EvaluateAsync(@"(e, i) => { + const reader = new FileReader(); + const promise = new Promise(fulfill => reader.onload = fulfill); + reader.readAsText(e.files[i]); + return promise.then(() => reader.result); + }", i); + Assert.AreEqual(File.ReadAllText(Path.Combine(dir, "..", webkitRelativePaths[i])), content); + } + } + + [PlaywrightTest("page-set-input-files.spec.ts", "should upload a folder and throw for multiple directories")] + public async Task ShouldUploadAFolderAndThrowForMultipleDirectories() + { + await Page.GotoAsync(Server.Prefix + "/input/folderupload.html"); + var input = await Page.QuerySelectorAsync("input"); + using var tmpDir = new TempDirectory(); + var dir = Path.Combine(tmpDir.Path, "file-upload-test"); + { + Directory.CreateDirectory(Path.Combine(dir, "folder1")); + File.WriteAllText(Path.Combine(dir, "folder1", "file1.txt"), "file1 content"); + Directory.CreateDirectory(Path.Combine(dir, "folder2")); + File.WriteAllText(Path.Combine(dir, "folder2", "file2.txt"), "file2 content"); + } + var ex = await PlaywrightAssert.ThrowsAsync(() => input.SetInputFilesAsync(new string[] { Path.Combine(dir, "folder1"), Path.Combine(dir, "folder2") })); + Assert.AreEqual("Multiple directories are not supported", ex.Message); + } + + [PlaywrightTest("page-set-input-files.spec.ts", "should throw if a directory and files are passed")] + public async Task ShouldThrowIfADirectoryAndFilesArePassed() + { + await Page.GotoAsync(Server.Prefix + "/input/folderupload.html"); + var input = await Page.QuerySelectorAsync("input"); + using var tmpDir = new TempDirectory(); + var dir = Path.Combine(tmpDir.Path, "file-upload-test"); + { + Directory.CreateDirectory(Path.Combine(dir, "folder1")); + File.WriteAllText(Path.Combine(dir, "folder1", "file1.txt"), "file1 content"); + } + var ex = await PlaywrightAssert.ThrowsAsync(() => input.SetInputFilesAsync(new string[] { Path.Combine(dir, "folder1"), Path.Combine(dir, "folder1", "file1.txt") })); + Assert.AreEqual("File paths must be all files or a single directory", ex.Message); + } + + [PlaywrightTest("page-set-input-files.spec.ts", "should throw when uploading a folder in a normal file upload input")] + public async Task ShouldThrowWhenUploadingAFolderInANormalFileUploadInput() + { + await Page.GotoAsync(Server.Prefix + "/input/fileupload.html"); + var input = await Page.QuerySelectorAsync("input"); + using var tmpDir = new TempDirectory(); + var dir = Path.Combine(tmpDir.Path, "file-upload-test"); + { + Directory.CreateDirectory(dir); + File.WriteAllText(Path.Combine(dir, "file1.txt"), "file1 content"); + } + var ex = await PlaywrightAssert.ThrowsAsync(() => input.SetInputFilesAsync(dir)); + Assert.AreEqual("Error: File input does not support directories, pass individual files instead", ex.Message); + } } diff --git a/src/Playwright.Tests/PageWaitForSelector1Tests.cs b/src/Playwright.Tests/PageWaitForSelector1Tests.cs index 43b61aea05..1a1bb53dad 100644 --- a/src/Playwright.Tests/PageWaitForSelector1Tests.cs +++ b/src/Playwright.Tests/PageWaitForSelector1Tests.cs @@ -145,7 +145,7 @@ await frame.EvaluateAsync(@"() => { StringAssert.Contains("Timeout 5000ms", exception.Message); StringAssert.Contains("waiting for Locator(\"div\") to be visible", exception.Message); - StringAssert.Contains("locator resolved to hidden
abcdefghijklmnopqrstuvwyxzabcdefghijklmnopqrstuvw…
", exception.Message); + StringAssert.Contains("locator resolved to hidden
abcdefghijklmnopqrstuvwyxzabcdefghijklmnopqrstuvw…
", exception.Message); StringAssert.Contains("locator resolved to hidden
", exception.Message); } diff --git a/src/Playwright/API/Generated/Enums/HttpCredentialsSend.cs b/src/Playwright/API/Generated/Enums/HttpCredentialsSend.cs new file mode 100644 index 0000000000..b9b06c4cab --- /dev/null +++ b/src/Playwright/API/Generated/Enums/HttpCredentialsSend.cs @@ -0,0 +1,39 @@ +/* + * 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.Runtime.Serialization; + +#nullable enable + +namespace Microsoft.Playwright; + +public enum HttpCredentialsSend +{ + [EnumMember(Value = "unauthorized")] + Unauthorized, + [EnumMember(Value = "always")] + Always, +} + +#nullable disable diff --git a/src/Playwright/API/Generated/IBrowserContext.cs b/src/Playwright/API/Generated/IBrowserContext.cs index 81d6643af6..b33e6db527 100644 --- a/src/Playwright/API/Generated/IBrowserContext.cs +++ b/src/Playwright/API/Generated/IBrowserContext.cs @@ -68,6 +68,9 @@ public partial interface IBrowserContext /// Only works with Chromium browser's persistent context. event EventHandler BackgroundPage; + /// Playwright has ability to mock clock and passage of time. + public IClock Clock { get; } + /// /// /// Emitted when Browser context gets closed. This might happen because of one of the @@ -433,21 +436,22 @@ public partial interface IBrowserContext /// A permission or an array of permissions to grant. Permissions can be one of the /// following values: /// - /// 'geolocation' - /// 'midi' - /// 'midi-sysex' (system-exclusive midi) - /// 'notifications' - /// 'camera' - /// 'microphone' - /// 'background-sync' - /// 'ambient-light-sensor' /// 'accelerometer' - /// 'gyroscope' - /// 'magnetometer' /// 'accessibility-events' + /// 'ambient-light-sensor' + /// 'background-sync' + /// 'camera' /// 'clipboard-read' /// 'clipboard-write' + /// 'geolocation' + /// 'gyroscope' + /// 'magnetometer' + /// 'microphone' + /// 'midi-sysex' (system-exclusive midi) + /// 'midi' + /// 'notifications' /// 'payment-handler' + /// 'storage-access' /// /// /// Call options diff --git a/src/Playwright/API/Generated/IClock.cs b/src/Playwright/API/Generated/IClock.cs new file mode 100644 index 0000000000..f98361d791 --- /dev/null +++ b/src/Playwright/API/Generated/IClock.cs @@ -0,0 +1,290 @@ +/* + * 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.Threading.Tasks; + +#nullable enable + +namespace Microsoft.Playwright; + +/// +/// +/// Accurately simulating time-dependent behavior is essential for verifying the correctness +/// of applications. Learn more about clock +/// emulation. +/// +/// +/// Note that clock is installed for the entire , so the +/// time in all the pages and iframes is controlled by the same clock. +/// +/// +public partial interface IClock +{ + /// + /// + /// Advance the clock by jumping forward in time. Only fires due timers at most once. + /// This is equivalent to user closing the laptop lid for a while and reopening it later, + /// after given time. + /// + /// **Usage** + /// + /// await page.Clock.FastForwardAsync(1000);
+ /// await page.Clock.FastForwardAsync("30:00"); + ///
+ ///
+ /// + /// Time may be the number of milliseconds to advance the clock by or a human-readable + /// string. Valid string formats are "08" for eight seconds, "01:00" for one minute + /// and "02:34:10" for two hours, 34 minutes and ten seconds. + /// + Task FastForwardAsync(long ticks); + + /// + /// + /// Advance the clock by jumping forward in time. Only fires due timers at most once. + /// This is equivalent to user closing the laptop lid for a while and reopening it later, + /// after given time. + /// + /// **Usage** + /// + /// await page.Clock.FastForwardAsync(1000);
+ /// await page.Clock.FastForwardAsync("30:00"); + ///
+ ///
+ /// + /// Time may be the number of milliseconds to advance the clock by or a human-readable + /// string. Valid string formats are "08" for eight seconds, "01:00" for one minute + /// and "02:34:10" for two hours, 34 minutes and ten seconds. + /// + Task FastForwardAsync(string ticks); + + /// + /// Install fake implementations for the following time-related functions: + /// + /// Date + /// setTimeout + /// clearTimeout + /// setInterval + /// clearInterval + /// requestAnimationFrame + /// cancelAnimationFrame + /// requestIdleCallback + /// cancelIdleCallback + /// performance + /// + /// + /// Fake timers are used to manually control the flow of time in tests. They allow you + /// to advance time, fire timers, and control the behavior of time-dependent functions. + /// See and for + /// more information. + /// + /// + /// Call options + Task InstallAsync(ClockInstallOptions? options = default); + + /// + /// Advance the clock, firing all the time-related callbacks. + /// **Usage** + /// + /// await page.Clock.RunForAsync(1000);
+ /// await page.Clock.RunForAsync("30:00"); + ///
+ ///
+ /// + /// Time may be the number of milliseconds to advance the clock by or a human-readable + /// string. Valid string formats are "08" for eight seconds, "01:00" for one minute + /// and "02:34:10" for two hours, 34 minutes and ten seconds. + /// + Task RunForAsync(long ticks); + + /// + /// Advance the clock, firing all the time-related callbacks. + /// **Usage** + /// + /// await page.Clock.RunForAsync(1000);
+ /// await page.Clock.RunForAsync("30:00"); + ///
+ ///
+ /// + /// Time may be the number of milliseconds to advance the clock by or a human-readable + /// string. Valid string formats are "08" for eight seconds, "01:00" for one minute + /// and "02:34:10" for two hours, 34 minutes and ten seconds. + /// + Task RunForAsync(string ticks); + + /// + /// + /// Advance the clock by jumping forward in time and pause the time. Once this method + /// is called, no timers are fired unless , , + /// or is called. + /// + /// + /// Only fires due timers at most once. This is equivalent to user closing the laptop + /// lid for a while and reopening it at the specified time and pausing. + /// + /// **Usage** + /// + /// await page.Clock.PauseAtAsync(DateTime.Parse("2020-02-02"));
+ /// await page.Clock.PauseAtAsync("2020-02-02"); + ///
+ ///
+ /// + /// + Task PauseAtAsync(long time); + + /// + /// + /// Advance the clock by jumping forward in time and pause the time. Once this method + /// is called, no timers are fired unless , , + /// or is called. + /// + /// + /// Only fires due timers at most once. This is equivalent to user closing the laptop + /// lid for a while and reopening it at the specified time and pausing. + /// + /// **Usage** + /// + /// await page.Clock.PauseAtAsync(DateTime.Parse("2020-02-02"));
+ /// await page.Clock.PauseAtAsync("2020-02-02"); + ///
+ ///
+ /// + /// + Task PauseAtAsync(string time); + + /// + /// + /// Advance the clock by jumping forward in time and pause the time. Once this method + /// is called, no timers are fired unless , , + /// or is called. + /// + /// + /// Only fires due timers at most once. This is equivalent to user closing the laptop + /// lid for a while and reopening it at the specified time and pausing. + /// + /// **Usage** + /// + /// await page.Clock.PauseAtAsync(DateTime.Parse("2020-02-02"));
+ /// await page.Clock.PauseAtAsync("2020-02-02"); + ///
+ ///
+ /// + /// + Task PauseAtAsync(DateTime time); + + /// + /// + /// Resumes timers. Once this method is called, time resumes flowing, timers are fired + /// as usual. + /// + /// + Task ResumeAsync(); + + /// + /// + /// Makes Date.now and new Date() return fixed fake time at all times, + /// keeps all the timers running. + /// + /// **Usage** + /// + /// await page.Clock.SetFixedTimeAsync(DateTime.Now);
+ /// await page.Clock.SetFixedTimeAsync(new DateTime(2020, 2, 2));
+ /// await page.Clock.SetFixedTimeAsync("2020-02-02"); + ///
+ ///
+ /// Time to be set. + Task SetFixedTimeAsync(long time); + + /// + /// + /// Makes Date.now and new Date() return fixed fake time at all times, + /// keeps all the timers running. + /// + /// **Usage** + /// + /// await page.Clock.SetFixedTimeAsync(DateTime.Now);
+ /// await page.Clock.SetFixedTimeAsync(new DateTime(2020, 2, 2));
+ /// await page.Clock.SetFixedTimeAsync("2020-02-02"); + ///
+ ///
+ /// Time to be set. + Task SetFixedTimeAsync(string time); + + /// + /// + /// Makes Date.now and new Date() return fixed fake time at all times, + /// keeps all the timers running. + /// + /// **Usage** + /// + /// await page.Clock.SetFixedTimeAsync(DateTime.Now);
+ /// await page.Clock.SetFixedTimeAsync(new DateTime(2020, 2, 2));
+ /// await page.Clock.SetFixedTimeAsync("2020-02-02"); + ///
+ ///
+ /// Time to be set. + Task SetFixedTimeAsync(DateTime time); + + /// + /// Sets current system time but does not trigger any timers. + /// **Usage** + /// + /// await page.Clock.SetSystemTimeAsync(DateTime.Now);
+ /// await page.Clock.SetSystemTimeAsync(new DateTime(2020, 2, 2));
+ /// await page.Clock.SetSystemTimeAsync("2020-02-02"); + ///
+ ///
+ /// + /// + Task SetSystemTimeAsync(long time); + + /// + /// Sets current system time but does not trigger any timers. + /// **Usage** + /// + /// await page.Clock.SetSystemTimeAsync(DateTime.Now);
+ /// await page.Clock.SetSystemTimeAsync(new DateTime(2020, 2, 2));
+ /// await page.Clock.SetSystemTimeAsync("2020-02-02"); + ///
+ ///
+ /// + /// + Task SetSystemTimeAsync(string time); + + /// + /// Sets current system time but does not trigger any timers. + /// **Usage** + /// + /// await page.Clock.SetSystemTimeAsync(DateTime.Now);
+ /// await page.Clock.SetSystemTimeAsync(new DateTime(2020, 2, 2));
+ /// await page.Clock.SetSystemTimeAsync("2020-02-02"); + ///
+ ///
+ /// + /// + Task SetSystemTimeAsync(DateTime time); +} + +#nullable disable diff --git a/src/Playwright/API/Generated/IConsoleMessage.cs b/src/Playwright/API/Generated/IConsoleMessage.cs index b127b4945e..f178a2e84e 100644 --- a/src/Playwright/API/Generated/IConsoleMessage.cs +++ b/src/Playwright/API/Generated/IConsoleMessage.cs @@ -31,8 +31,8 @@ namespace Microsoft.Playwright; /// /// /// objects are dispatched by page via the -/// event. For each console messages logged in the page there will be corresponding -/// event in the Playwright context. +/// event. For each console message logged in the page there will be corresponding event +/// in the Playwright context. /// /// /// // Listen for all console messages and print them to the standard output.
diff --git a/src/Playwright/API/Generated/IElementHandle.cs b/src/Playwright/API/Generated/IElementHandle.cs index 700f598973..03da1f4f4d 100644 --- a/src/Playwright/API/Generated/IElementHandle.cs +++ b/src/Playwright/API/Generated/IElementHandle.cs @@ -611,6 +611,10 @@ public partial interface IElementHandle : IJSHandle /// Throws when elementHandle does not point to an element connected /// to a Document or a ShadowRoot. /// + /// + /// See scrolling for + /// alternative ways to scroll. + /// ///
/// Call options Task ScrollIntoViewIfNeededAsync(ElementHandleScrollIntoViewIfNeededOptions? options = default); @@ -943,7 +947,8 @@ public partial interface IElementHandle : IJSHandle /// /// Sets the value of the file input to these file paths or files. If some of the filePaths /// are relative paths, then they are resolved relative to the current working directory. - /// For empty array, clears the selected files. + /// For empty array, clears the selected files. For inputs with a [webkitdirectory] + /// attribute, only a single directory path is supported. /// /// /// This method expects to point to an input @@ -965,7 +970,8 @@ public partial interface IElementHandle : IJSHandle /// /// Sets the value of the file input to these file paths or files. If some of the filePaths /// are relative paths, then they are resolved relative to the current working directory. - /// For empty array, clears the selected files. + /// For empty array, clears the selected files. For inputs with a [webkitdirectory] + /// attribute, only a single directory path is supported. /// /// /// This method expects to point to an input @@ -987,7 +993,8 @@ public partial interface IElementHandle : IJSHandle /// /// Sets the value of the file input to these file paths or files. If some of the filePaths /// are relative paths, then they are resolved relative to the current working directory. - /// For empty array, clears the selected files. + /// For empty array, clears the selected files. For inputs with a [webkitdirectory] + /// attribute, only a single directory path is supported. /// /// /// This method expects to point to an input @@ -1009,7 +1016,8 @@ public partial interface IElementHandle : IJSHandle /// /// Sets the value of the file input to these file paths or files. If some of the filePaths /// are relative paths, then they are resolved relative to the current working directory. - /// For empty array, clears the selected files. + /// For empty array, clears the selected files. For inputs with a [webkitdirectory] + /// attribute, only a single directory path is supported. /// /// /// This method expects to point to an input diff --git a/src/Playwright/API/Generated/ILocator.cs b/src/Playwright/API/Generated/ILocator.cs index ebe986fe88..00abd2e879 100644 --- a/src/Playwright/API/Generated/ILocator.cs +++ b/src/Playwright/API/Generated/ILocator.cs @@ -1183,6 +1183,10 @@ public partial interface ILocator /// as defined by IntersectionObserver's /// ratio. /// + /// + /// See scrolling for + /// alternative ways to scroll. + /// ///
/// Call options Task ScrollIntoViewIfNeededAsync(LocatorScrollIntoViewIfNeededOptions? options = default); @@ -1465,7 +1469,10 @@ public partial interface ILocator Task SetCheckedAsync(bool checkedState, LocatorSetCheckedOptions? options = default); /// - /// Upload file or multiple files into <input type=file>. + /// + /// Upload file or multiple files into <input type=file>. For inputs with + /// a [webkitdirectory] attribute, only a single directory path is supported. + /// /// **Usage** /// /// // Select one file
@@ -1474,6 +1481,9 @@ public partial interface ILocator /// // Select multiple files
/// await page.GetByLabel("Upload files").SetInputFilesAsync(new[] { "file1.txt", "file12.txt" });
///
+ /// // Select a directory
+ /// await page.GetByLabel("Upload directory").SetInputFilesAsync("mydir");
+ ///
/// // Remove all the selected files
/// await page.GetByLabel("Upload file").SetInputFilesAsync(new[] {});
///
@@ -1504,7 +1514,10 @@ public partial interface ILocator Task SetInputFilesAsync(string files, LocatorSetInputFilesOptions? options = default); /// - /// Upload file or multiple files into <input type=file>. + /// + /// Upload file or multiple files into <input type=file>. For inputs with + /// a [webkitdirectory] attribute, only a single directory path is supported. + /// /// **Usage** /// /// // Select one file
@@ -1513,6 +1526,9 @@ public partial interface ILocator /// // Select multiple files
/// await page.GetByLabel("Upload files").SetInputFilesAsync(new[] { "file1.txt", "file12.txt" });
///
+ /// // Select a directory
+ /// await page.GetByLabel("Upload directory").SetInputFilesAsync("mydir");
+ ///
/// // Remove all the selected files
/// await page.GetByLabel("Upload file").SetInputFilesAsync(new[] {});
///
@@ -1543,7 +1559,10 @@ public partial interface ILocator Task SetInputFilesAsync(IEnumerable files, LocatorSetInputFilesOptions? options = default); /// - /// Upload file or multiple files into <input type=file>. + /// + /// Upload file or multiple files into <input type=file>. For inputs with + /// a [webkitdirectory] attribute, only a single directory path is supported. + /// /// **Usage** /// /// // Select one file
@@ -1552,6 +1571,9 @@ public partial interface ILocator /// // Select multiple files
/// await page.GetByLabel("Upload files").SetInputFilesAsync(new[] { "file1.txt", "file12.txt" });
///
+ /// // Select a directory
+ /// await page.GetByLabel("Upload directory").SetInputFilesAsync("mydir");
+ ///
/// // Remove all the selected files
/// await page.GetByLabel("Upload file").SetInputFilesAsync(new[] {});
///
@@ -1582,7 +1604,10 @@ public partial interface ILocator Task SetInputFilesAsync(FilePayload files, LocatorSetInputFilesOptions? options = default); /// - /// Upload file or multiple files into <input type=file>. + /// + /// Upload file or multiple files into <input type=file>. For inputs with + /// a [webkitdirectory] attribute, only a single directory path is supported. + /// /// **Usage** /// /// // Select one file
@@ -1591,6 +1616,9 @@ public partial interface ILocator /// // Select multiple files
/// await page.GetByLabel("Upload files").SetInputFilesAsync(new[] { "file1.txt", "file12.txt" });
///
+ /// // Select a directory
+ /// await page.GetByLabel("Upload directory").SetInputFilesAsync("mydir");
+ ///
/// // Remove all the selected files
/// await page.GetByLabel("Upload file").SetInputFilesAsync(new[] {});
///
diff --git a/src/Playwright/API/Generated/ILocatorAssertions.cs b/src/Playwright/API/Generated/ILocatorAssertions.cs index f00bce8c11..3a9cedf0ba 100644 --- a/src/Playwright/API/Generated/ILocatorAssertions.cs +++ b/src/Playwright/API/Generated/ILocatorAssertions.cs @@ -36,21 +36,19 @@ namespace Microsoft.Playwright; /// used to make assertions about the state in the tests. /// /// -/// using System.Text.RegularExpressions;
-/// using System.Threading.Tasks;
-/// using Microsoft.Playwright.NUnit;
-/// using NUnit.Framework;
+/// using Microsoft.Playwright;
+/// using Microsoft.Playwright.MSTest;
///
/// namespace PlaywrightTests;
///
-/// [TestFixture]
+/// [TestClass]
/// public class ExampleTests : PageTest
/// {
-/// [Test]
+/// [TestMethod]
/// public async Task StatusBecomesSubmitted()
/// {
-/// // ..
-/// await Page.GetByRole(AriaRole.Button).ClickAsync();
+/// // ...
+/// await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync();
/// await Expect(Page.Locator(".status")).ToHaveTextAsync("Submitted");
/// }
/// } diff --git a/src/Playwright/API/Generated/IMouse.cs b/src/Playwright/API/Generated/IMouse.cs index 722a4b097d..6ece58aa71 100644 --- a/src/Playwright/API/Generated/IMouse.cs +++ b/src/Playwright/API/Generated/IMouse.cs @@ -88,7 +88,13 @@ public partial interface IMouse /// Call options Task UpAsync(MouseUpOptions? options = default); - /// Dispatches a wheel event. + /// + /// + /// Dispatches a wheel event. This method is usually used to manually scroll + /// the page. See scrolling + /// for alternative ways to scroll. + /// + /// /// /// /// Wheel events may cause scrolling if they are not handled, and this method does not diff --git a/src/Playwright/API/Generated/IPage.cs b/src/Playwright/API/Generated/IPage.cs index d9e5081be2..491dcfa3c1 100644 --- a/src/Playwright/API/Generated/IPage.cs +++ b/src/Playwright/API/Generated/IPage.cs @@ -75,6 +75,9 @@ namespace Microsoft.Playwright; ///
public partial interface IPage { + /// Playwright has ability to mock clock and passage of time. + public IClock Clock { get; } + /// Emitted when the page closes. event EventHandler Close; @@ -2424,7 +2427,8 @@ public partial interface IPage /// /// Sets the value of the file input to these file paths or files. If some of the filePaths /// are relative paths, then they are resolved relative to the current working directory. - /// For empty array, clears the selected files. + /// For empty array, clears the selected files. For inputs with a [webkitdirectory] + /// attribute, only a single directory path is supported. /// /// /// This method expects to point to an input @@ -2450,7 +2454,8 @@ public partial interface IPage /// /// Sets the value of the file input to these file paths or files. If some of the filePaths /// are relative paths, then they are resolved relative to the current working directory. - /// For empty array, clears the selected files. + /// For empty array, clears the selected files. For inputs with a [webkitdirectory] + /// attribute, only a single directory path is supported. /// /// /// This method expects to point to an input @@ -2476,7 +2481,8 @@ public partial interface IPage /// /// Sets the value of the file input to these file paths or files. If some of the filePaths /// are relative paths, then they are resolved relative to the current working directory. - /// For empty array, clears the selected files. + /// For empty array, clears the selected files. For inputs with a [webkitdirectory] + /// attribute, only a single directory path is supported. /// /// /// This method expects to point to an input @@ -2502,7 +2508,8 @@ public partial interface IPage /// /// Sets the value of the file input to these file paths or files. If some of the filePaths /// are relative paths, then they are resolved relative to the current working directory. - /// For empty array, clears the selected files. + /// For empty array, clears the selected files. For inputs with a [webkitdirectory] + /// attribute, only a single directory path is supported. /// /// /// This method expects to point to an input @@ -3205,7 +3212,7 @@ public partial interface IPage /// await page.RunAndWaitForResponseAsync(async () =>
/// {
/// await page.GetByText("trigger response").ClickAsync();
- /// }, response => response.Url == "https://example.com" && response.Status == 200); + /// }, response => response.Url == "https://example.com" && response.Status == 200 && response.Request.Method == "GET"); ///
///
/// @@ -3234,7 +3241,7 @@ public partial interface IPage /// await page.RunAndWaitForResponseAsync(async () =>
/// {
/// await page.GetByText("trigger response").ClickAsync();
- /// }, response => response.Url == "https://example.com" && response.Status == 200); + /// }, response => response.Url == "https://example.com" && response.Status == 200 && response.Request.Method == "GET"); ///
///
/// @@ -3263,7 +3270,7 @@ public partial interface IPage /// await page.RunAndWaitForResponseAsync(async () =>
/// {
/// await page.GetByText("trigger response").ClickAsync();
- /// }, response => response.Url == "https://example.com" && response.Status == 200); + /// }, response => response.Url == "https://example.com" && response.Status == 200 && response.Request.Method == "GET"); ///
///
/// @@ -3292,7 +3299,7 @@ public partial interface IPage /// await page.RunAndWaitForResponseAsync(async () =>
/// {
/// await page.GetByText("trigger response").ClickAsync();
- /// }, response => response.Url == "https://example.com" && response.Status == 200); + /// }, response => response.Url == "https://example.com" && response.Status == 200 && response.Request.Method == "GET"); /// /// /// Action that triggers the event. @@ -3322,7 +3329,7 @@ public partial interface IPage /// await page.RunAndWaitForResponseAsync(async () =>
/// {
/// await page.GetByText("trigger response").ClickAsync();
- /// }, response => response.Url == "https://example.com" && response.Status == 200); + /// }, response => response.Url == "https://example.com" && response.Status == 200 && response.Request.Method == "GET"); /// /// /// Action that triggers the event. @@ -3352,7 +3359,7 @@ public partial interface IPage /// await page.RunAndWaitForResponseAsync(async () =>
/// {
/// await page.GetByText("trigger response").ClickAsync();
- /// }, response => response.Url == "https://example.com" && response.Status == 200); + /// }, response => response.Url == "https://example.com" && response.Status == 200 && response.Request.Method == "GET"); /// /// /// Action that triggers the event. diff --git a/src/Playwright/API/Generated/IPageAssertions.cs b/src/Playwright/API/Generated/IPageAssertions.cs index c820fd5951..f4fcba8b40 100644 --- a/src/Playwright/API/Generated/IPageAssertions.cs +++ b/src/Playwright/API/Generated/IPageAssertions.cs @@ -36,21 +36,19 @@ namespace Microsoft.Playwright; /// /// /// using System.Text.RegularExpressions;
-/// using System.Threading.Tasks;
-/// using Microsoft.Playwright.NUnit;
-/// using NUnit.Framework;
+/// using Microsoft.Playwright;
+/// using Microsoft.Playwright.MSTest;
///
/// namespace PlaywrightTests;
///
-/// [TestFixture]
+/// [TestClass]
/// public class ExampleTests : PageTest
/// {
-/// [Test]
+/// [TestMethod]
/// public async Task NavigatetoLoginPage()
/// {
-/// // ..
-/// await Page.GetByText("Sing in").ClickAsync();
-/// await Expect(Page.Locator("div#foobar")).ToHaveURL(new Regex(".*/login"));
+/// await Page.GetByRole(AriaRole.Button, new() { Name = "Sign In" }).ClickAsync();
+/// await Expect(Page).ToHaveURLAsync(new Regex(".*/login"));
/// }
/// } ///
diff --git a/src/Playwright/API/Generated/IPlaywrightAssertions.cs b/src/Playwright/API/Generated/IPlaywrightAssertions.cs index 075e88eb8f..ec5cfbccbb 100644 --- a/src/Playwright/API/Generated/IPlaywrightAssertions.cs +++ b/src/Playwright/API/Generated/IPlaywrightAssertions.cs @@ -33,19 +33,18 @@ namespace Microsoft.Playwright; /// /// Consider the following example: /// -/// using System.Threading.Tasks;
-/// using Microsoft.Playwright.NUnit;
-/// using NUnit.Framework;
+/// using Microsoft.Playwright;
+/// using Microsoft.Playwright.MSTest;
///
/// namespace PlaywrightTests;
///
-/// [TestFixture]
+/// [TestClass]
/// public class ExampleTests : PageTest
/// {
-/// [Test]
+/// [TestMethod]
/// public async Task StatusBecomesSubmitted()
/// {
-/// await Page.Locator("#submit-button").ClickAsync();
+/// await Page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();
/// await Expect(Page.Locator(".status")).ToHaveTextAsync("Submitted");
/// }
/// } diff --git a/src/Playwright/API/Generated/Options/BrowserContextUnrouteAllOptions.cs b/src/Playwright/API/Generated/Options/BrowserContextUnrouteAllOptions.cs index 3b382178f9..9b0fe96dbb 100644 --- a/src/Playwright/API/Generated/Options/BrowserContextUnrouteAllOptions.cs +++ b/src/Playwright/API/Generated/Options/BrowserContextUnrouteAllOptions.cs @@ -44,7 +44,7 @@ public BrowserContextUnrouteAllOptions(BrowserContextUnrouteAllOptions clone) /// /// - /// Specifies wether to wait for already running handlers and what to do if they throw + /// Specifies whether to wait for already running handlers and what to do if they throw /// errors: /// /// diff --git a/src/Playwright/API/Generated/Options/ClockInstallOptions.cs b/src/Playwright/API/Generated/Options/ClockInstallOptions.cs new file mode 100644 index 0000000000..6bde6b4090 --- /dev/null +++ b/src/Playwright/API/Generated/Options/ClockInstallOptions.cs @@ -0,0 +1,66 @@ +/* + * 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.Text.Json.Serialization; + +#nullable enable + +namespace Microsoft.Playwright; + +public class ClockInstallOptions +{ + public ClockInstallOptions() { } + + public ClockInstallOptions(ClockInstallOptions clone) + { + if (clone == null) + { + return; + } + + Time = clone.Time; + TimeDate = clone.TimeDate; + TimeInt64 = clone.TimeInt64; + TimeString = clone.TimeString; + } + + /// Time to initialize with, current system time by default. + [JsonPropertyName("time")] + public string? Time { get; set; } + + /// Time to initialize with, current system time by default. + [JsonPropertyName("timeDate")] + public DateTime? TimeDate { get; set; } + + /// Time to initialize with, current system time by default. + [JsonPropertyName("timeInt64")] + public long? TimeInt64 { get; set; } + + /// Time to initialize with, current system time by default. + [JsonPropertyName("timeString")] + public string? TimeString { get; set; } +} + +#nullable disable diff --git a/src/Playwright/API/Generated/Options/PageUnrouteAllOptions.cs b/src/Playwright/API/Generated/Options/PageUnrouteAllOptions.cs index 254ddbff4a..3aa06a4fff 100644 --- a/src/Playwright/API/Generated/Options/PageUnrouteAllOptions.cs +++ b/src/Playwright/API/Generated/Options/PageUnrouteAllOptions.cs @@ -44,7 +44,7 @@ public PageUnrouteAllOptions(PageUnrouteAllOptions clone) /// /// - /// Specifies wether to wait for already running handlers and what to do if they throw + /// Specifies whether to wait for already running handlers and what to do if they throw /// errors: /// /// diff --git a/src/Playwright/API/Generated/Types/HttpCredentials.cs b/src/Playwright/API/Generated/Types/HttpCredentials.cs index 2a93bfb1fe..ebfef1d1ac 100644 --- a/src/Playwright/API/Generated/Types/HttpCredentials.cs +++ b/src/Playwright/API/Generated/Types/HttpCredentials.cs @@ -44,6 +44,18 @@ public partial class HttpCredentials /// Restrain sending http credentials on specific origin (scheme://host:port). [JsonPropertyName("origin")] public string? Origin { get; set; } + + /// + /// + /// This option only applies to the requests sent from corresponding + /// and does not affect requests sent from the browser. 'always' - Authorization + /// header with basic authentication credentials will be sent with the each API request. + /// 'unauthorized - the credentials are only sent when 401 (Unauthorized) response + /// with WWW-Authenticate header is received. Defaults to 'unauthorized'. + /// + /// + [JsonPropertyName("send")] + public HttpCredentialsSend? Send { get; set; } } #nullable disable diff --git a/src/Playwright/Core/BrowserContext.cs b/src/Playwright/Core/BrowserContext.cs index 10f635b655..6c13e1260f 100644 --- a/src/Playwright/Core/BrowserContext.cs +++ b/src/Playwright/Core/BrowserContext.cs @@ -44,6 +44,7 @@ internal class BrowserContext : ChannelOwner, IBrowserContext private readonly Dictionary _bindings = new(); private readonly BrowserContextInitializer _initializer; private readonly Tracing _tracing; + private readonly Clock _clock; internal readonly HashSet _backgroundPages = new(); internal readonly IAPIRequestContext _request; private readonly Dictionary _harRecorders = new(); @@ -62,6 +63,7 @@ internal BrowserContext(ChannelOwner parent, string guid, BrowserContextInitiali _browser?._contexts.Add(this); _tracing = initializer.Tracing; + _clock = new Clock(this); _request = initializer.RequestContext; _initializer = initializer; } @@ -124,11 +126,9 @@ public event EventHandler RequestFailed public event EventHandler ServiceWorker; - public ITracing Tracing - { - get => _tracing; - set => throw new NotSupportedException(); - } + public ITracing Tracing => _tracing; + + public IClock Clock => _clock; public IBrowser Browser => _browser; diff --git a/src/Playwright/Core/Clock.cs b/src/Playwright/Core/Clock.cs new file mode 100644 index 0000000000..c02e69ae55 --- /dev/null +++ b/src/Playwright/Core/Clock.cs @@ -0,0 +1,107 @@ +/* + * 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.Collections.Generic; +using System.Threading.Tasks; + +namespace Microsoft.Playwright.Core; + +internal class Clock(BrowserContext browserContext) : IClock +{ + public async Task InstallAsync(ClockInstallOptions options = null) + { + Dictionary args = null; + if ((options.Time ?? options.TimeString) != null) + { + args = ParseTime(options.Time ?? options.TimeString); + } + else if (options.TimeInt64 != null) + { + args = ParseTime(options.TimeInt64.Value); + } + else if (options.TimeDate != null) + { + args = ParseTime(options.TimeDate.Value); + } + await browserContext.SendMessageToServerAsync("clockInstall", args).ConfigureAwait(false); + } + + private static Dictionary ParseTime(string timeString) + => new() { ["timeString"] = timeString }; + + private static Dictionary ParseTime(DateTime? timeDate) + => new() { ["timeNumber"] = ((DateTimeOffset)timeDate.Value).ToUnixTimeMilliseconds() }; + + private static Dictionary ParseTime(long timeInt64) + => new() { ["timeNumber"] = timeInt64 }; + + private Dictionary ParseTicks(long ticks) + => new() { ["ticksNumber"] = ticks }; + + private Dictionary ParseTicks(string ticks) + => new() { ["ticksString"] = ticks }; + + public Task FastForwardAsync(long ticks) + => browserContext.SendMessageToServerAsync("clockFastForward", ParseTicks(ticks)); + + public Task FastForwardAsync(string ticks) + => browserContext.SendMessageToServerAsync("clockFastForward", ParseTicks(ticks)); + + public Task PauseAtAsync(long time) + => browserContext.SendMessageToServerAsync("clockPauseAt", ParseTime(time)); + + public Task PauseAtAsync(string time) + => browserContext.SendMessageToServerAsync("clockPauseAt", ParseTime(time)); + + public Task PauseAtAsync(DateTime time) + => browserContext.SendMessageToServerAsync("clockPauseAt", ParseTime(time)); + + public Task ResumeAsync() + => browserContext.SendMessageToServerAsync("clockResume"); + + public Task RunForAsync(long ticks) + => browserContext.SendMessageToServerAsync("clockRunFor", ParseTicks(ticks)); + + public Task RunForAsync(string ticks) + => browserContext.SendMessageToServerAsync("clockRunFor", ParseTicks(ticks)); + + public Task SetFixedTimeAsync(long time) + => browserContext.SendMessageToServerAsync("clockSetFixedTime", ParseTime(time)); + + public Task SetFixedTimeAsync(string time) + => browserContext.SendMessageToServerAsync("clockSetFixedTime", ParseTime(time)); + + public Task SetFixedTimeAsync(DateTime time) + => browserContext.SendMessageToServerAsync("clockSetFixedTime", ParseTime(time)); + + public Task SetSystemTimeAsync(long time) + => browserContext.SendMessageToServerAsync("clockSetSystemTime", ParseTime(time)); + + public Task SetSystemTimeAsync(string time) + => browserContext.SendMessageToServerAsync("clockSetSystemTime", ParseTime(time)); + + public Task SetSystemTimeAsync(DateTime time) + => browserContext.SendMessageToServerAsync("clockSetSystemTime", ParseTime(time)); +} diff --git a/src/Playwright/Core/ElementHandle.cs b/src/Playwright/Core/ElementHandle.cs index e71a76806f..8417263630 100644 --- a/src/Playwright/Core/ElementHandle.cs +++ b/src/Playwright/Core/ElementHandle.cs @@ -209,7 +209,9 @@ public async Task SetInputFilesAsync(IEnumerable files, ElementHandleSet { ["payloads"] = converted.Payloads, ["localPaths"] = converted.LocalPaths, + ["localDirectory"] = converted.LocalDirectory, ["streams"] = converted.Streams, + ["directoryStream"] = converted.DirectoryStream, ["timeout"] = options?.Timeout, ["noWaitAfter"] = options?.NoWaitAfter, }).ConfigureAwait(false); diff --git a/src/Playwright/Core/Frame.cs b/src/Playwright/Core/Frame.cs index 4d0220ca09..759a6e5c6b 100644 --- a/src/Playwright/Core/Frame.cs +++ b/src/Playwright/Core/Frame.cs @@ -535,7 +535,9 @@ private async Task _setInputFilesAsync(string selector, SetInputFilesFiles files ["selector"] = selector, ["payloads"] = files.Payloads, ["localPaths"] = files.LocalPaths, + ["localDirectory"] = files.LocalDirectory, ["streams"] = files.Streams, + ["directoryStream"] = files.DirectoryStream, ["noWaitAfter"] = noWaitAfter, ["timeout"] = timeout, ["strict"] = strict, diff --git a/src/Playwright/Core/Page.cs b/src/Playwright/Core/Page.cs index ef66662230..a60e6c44dc 100644 --- a/src/Playwright/Core/Page.cs +++ b/src/Playwright/Core/Page.cs @@ -182,6 +182,8 @@ public IMouse Mouse get; } + public IClock Clock => Context.Clock; + public string Url => MainFrame.Url; public IReadOnlyList