diff --git a/README.md b/README.md index c9f6e44a2..04f7c5845 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 124.0.6367.29 | ✅ | ✅ | ✅ | +| Chromium 125.0.6422.26 | ✅ | ✅ | ✅ | | WebKit 17.4 | ✅ | ✅ | ✅ | -| Firefox 124.0 | ✅ | ✅ | ✅ | +| Firefox 125.0.1 | ✅ | ✅ | ✅ | 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/Version.props b/src/Common/Version.props index cadd3de8a..6871fb54b 100644 --- a/src/Common/Version.props +++ b/src/Common/Version.props @@ -2,7 +2,7 @@ 1.43.0 $(AssemblyVersion) - 1.43.0-beta-1712646596000 + 1.44.0-beta-1715189091000 $(AssemblyVersion) $(AssemblyVersion) true diff --git a/src/Playwright.Tests.TestServer/SimpleServer.cs b/src/Playwright.Tests.TestServer/SimpleServer.cs index 66b452b0f..5239d0e68 100644 --- a/src/Playwright.Tests.TestServer/SimpleServer.cs +++ b/src/Playwright.Tests.TestServer/SimpleServer.cs @@ -49,7 +49,7 @@ public class SimpleServer const int MaxMessageSize = 256 * 1024; private readonly IDictionary> _requestWaits; - private readonly IList> _waitForWebSocketConnectionRequestsWaits; + private readonly IList> _waitForWebSocketConnectionRequestsWaits; private readonly IDictionary> _routes; private readonly IDictionary _auths; private readonly IDictionary _csp; @@ -81,7 +81,7 @@ public SimpleServer(int port, string contentRoot, bool isHttps) EmptyPage = $"{Prefix}/empty.html"; _requestWaits = new ConcurrentDictionary>(); - _waitForWebSocketConnectionRequestsWaits = new List>(); + _waitForWebSocketConnectionRequestsWaits = []; _routes = new ConcurrentDictionary>(); _auths = new ConcurrentDictionary(); _csp = new ConcurrentDictionary(); @@ -98,11 +98,12 @@ public SimpleServer(int port, string contentRoot, bool isHttps) { if (context.WebSockets.IsWebSocketRequest) { + var webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); foreach (var wait in _waitForWebSocketConnectionRequestsWaits) { - wait(context); + _waitForWebSocketConnectionRequestsWaits.Remove(wait); + await wait(webSocket, context).ConfigureAwait(false); } - var webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); if (_onWebSocketConnectionData != null) { await webSocket.SendAsync(_onWebSocketConnectionData, WebSocketMessageType.Text, true, CancellationToken.None).ConfigureAwait(false); @@ -287,18 +288,20 @@ public async Task WaitForRequest(string path, Func selecto public Task WaitForRequest(string path) => WaitForRequest(path, _ => true); - public async Task WaitForWebSocketConnectionRequest() + public async Task<(WebSocket, HttpRequest)> WaitForWebSocketConnectionRequest() { - var taskCompletion = new TaskCompletionSource(); - void entryCb(HttpContext context) + var taskCompletion = new TaskCompletionSource<(WebSocket, HttpRequest)>(); + OnceWebSocketConnection((WebSocket ws, HttpContext context) => { - taskCompletion.SetResult(context.Request); - }; - _waitForWebSocketConnectionRequestsWaits.Add(entryCb); + taskCompletion.SetResult((ws, context.Request)); + return Task.CompletedTask; + }); + return await taskCompletion.Task.ConfigureAwait(false); + } - var request = await taskCompletion.Task.ConfigureAwait(false); - _waitForWebSocketConnectionRequestsWaits.Remove(entryCb); - return request; + public void OnceWebSocketConnection(Func handler) + { + _waitForWebSocketConnectionRequestsWaits.Add(handler); } private static bool Authenticate(string username, string password, HttpContext context) diff --git a/src/Playwright.Tests.TestServer/assets/input/handle-locator.html b/src/Playwright.Tests.TestServer/assets/input/handle-locator.html index 865fb5364..f8f2111c9 100644 --- a/src/Playwright.Tests.TestServer/assets/input/handle-locator.html +++ b/src/Playwright.Tests.TestServer/assets/input/handle-locator.html @@ -50,9 +50,16 @@ }, false); close.addEventListener('click', () => { - interstitial.classList.remove('visible'); - target.classList.remove('hidden'); - target.classList.remove('removed'); + const closeInterstitial = () => { + interstitial.classList.remove('visible'); + target.classList.remove('hidden'); + target.classList.remove('removed'); + }; + + if (interstitial.classList.contains('timeout')) + setTimeout(closeInterstitial, 3000); + else + closeInterstitial(); }); let timesToShow = 0; @@ -65,9 +72,11 @@ if (!timesToShow && event !== 'none') target.removeEventListener(event, listener, capture === 'capture'); }; - if (event === 'hide') { + if (event === 'hide' || event === 'timeout') { target.classList.add('hidden'); listener(); + if (event === 'timeout') + interstitial.classList.add('timeout'); } else if (event === 'remove') { target.classList.add('removed'); listener(); diff --git a/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs b/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs index 59532df0c..5100422ea 100644 --- a/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs +++ b/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs @@ -695,4 +695,36 @@ await Page.SetContentAsync(@" await Expect(Page.Locator("div")).Not.ToBeInViewportAsync(new() { Ratio = 0.7f }); await Expect(Page.Locator("div")).Not.ToBeInViewportAsync(new() { Ratio = 0.8f }); } + + [PlaywrightTest("page/expect-misc.spec.ts", "toHaveAccessibleName")] + public async Task ToHaveAccessibleName() + { + await Page.SetContentAsync(@"
"); + await Expect(Page.Locator("div")).ToHaveAccessibleNameAsync("Hello"); + await Expect(Page.Locator("div")).Not.ToHaveAccessibleNameAsync("hello"); + await Expect(Page.Locator("div")).ToHaveAccessibleNameAsync("hello", new() { IgnoreCase = true }); + await Expect(Page.Locator("div")).ToHaveAccessibleNameAsync(new Regex(@"ell\w")); + await Expect(Page.Locator("div")).Not.ToHaveAccessibleNameAsync(new Regex("hello")); + await Expect(Page.Locator("div")).ToHaveAccessibleNameAsync(new Regex("hello"), new() { IgnoreCase = true }); + } + + [PlaywrightTest("page/expect-misc.spec.ts", "toHaveAccessibleDescription")] + public async Task ToHaveAccessibleDescription() + { + await Page.SetContentAsync(@"
"); + await Expect(Page.Locator("div")).ToHaveAccessibleDescriptionAsync("Hello"); + await Expect(Page.Locator("div")).Not.ToHaveAccessibleDescriptionAsync("hello"); + await Expect(Page.Locator("div")).ToHaveAccessibleDescriptionAsync("hello", new() { IgnoreCase = true }); + await Expect(Page.Locator("div")).ToHaveAccessibleDescriptionAsync(new Regex(@"ell\w")); + await Expect(Page.Locator("div")).Not.ToHaveAccessibleDescriptionAsync(new Regex("hello")); + await Expect(Page.Locator("div")).ToHaveAccessibleDescriptionAsync(new Regex("hello"), new() { IgnoreCase = true }); + } + + [PlaywrightTest("page/expect-misc.spec.ts", "toHaveRole")] + public async Task ToHaveRole() + { + await Page.SetContentAsync(@"
Button!
"); + await Expect(Page.Locator("div")).ToHaveRoleAsync(AriaRole.Button); + await Expect(Page.Locator("div")).Not.ToHaveRoleAsync(AriaRole.Checkbox); + } } diff --git a/src/Playwright.Tests/Assertions/PageAssertionsTests.cs b/src/Playwright.Tests/Assertions/PageAssertionsTests.cs index cd215cec1..b9af896a6 100644 --- a/src/Playwright.Tests/Assertions/PageAssertionsTests.cs +++ b/src/Playwright.Tests/Assertions/PageAssertionsTests.cs @@ -51,23 +51,28 @@ public async Task ShouldSupportToHaveTitleAsync() [PlaywrightTest("playwright-test/playwright.expect.misc.spec.ts", "should support toHaveURL")] public async Task ShouldSupportToHaveURLAsync() { + // Pass await Page.GotoAsync("data:text/html,
A
"); await Expect(Page).ToHaveURLAsync("data:text/html,
A
"); + // Fail await Page.GotoAsync("data:text/html,
B
"); var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(Page).ToHaveURLAsync("wrong", new() { Timeout = 1000 })); StringAssert.Contains("Page URL expected to be 'wrong'", exception.Message); StringAssert.Contains("But was: 'data:text/html,
B
'", exception.Message); StringAssert.Contains("PageAssertions.ToHaveURLAsync with timeout 1000ms", exception.Message); + // Fail with Regex await Page.GotoAsync(Server.EmptyPage); await Expect(Page).ToHaveURLAsync(new Regex(".*empty.html")); await PlaywrightAssert.ThrowsAsync(() => Expect(Page).ToHaveURLAsync(new Regex("nooo"), new() { Timeout = 1000 })); + // Pass with Regex await Page.GotoAsync(Server.EmptyPage); await Expect(Page).ToHaveURLAsync(Server.Prefix + "/empty.html"); await Expect(Page).Not.ToHaveURLAsync(Server.Prefix + "/foobar.html"); + // With BaseURL var page = await Browser.NewPageAsync(new() { BaseURL = Server.Prefix }); try { @@ -79,5 +84,10 @@ public async Task ShouldSupportToHaveURLAsync() { await page.CloseAsync(); } + + // Support IgnoreCase + await Page.GotoAsync("data:text/html,
A
"); + await Expect(Page).ToHaveURLAsync("DATA:teXT/HTml,
a
", new() { IgnoreCase = true }); + await Expect(Page).ToHaveURLAsync(new Regex("DATA:teXT/HTml,
a
"), new() { IgnoreCase = true }); } } diff --git a/src/Playwright.Tests/BrowserContextFetchTests.cs b/src/Playwright.Tests/BrowserContextFetchTests.cs index 3fa37f515..fa2dfe557 100644 --- a/src/Playwright.Tests/BrowserContextFetchTests.cs +++ b/src/Playwright.Tests/BrowserContextFetchTests.cs @@ -809,6 +809,49 @@ public async Task ShouldAcceptBoolAndNumericParams() Assert.AreEqual("False", receivedQueryParams["bool2"].First()); } + [PlaywrightTest("browsercontext-fetch.spec.ts", "should support repeating names in multipart/form-data")] + public async Task ShouldSupportRepeatingNamesInMultipartFormData() + { + var postBodyPromise = new TaskCompletionSource(); + var formData = Context.APIRequest.CreateFormData(); + formData.Set("name", "John"); + formData.Append("name", "Doe"); + formData.Append("file", new FilePayload() + { + Name = "f1.js", + MimeType = "text/javascript", + Buffer = System.Text.Encoding.UTF8.GetBytes("var x = 10;\r\n;console.log(x);") + }); + formData.Append("file", new FilePayload() + { + Name = "f2.txt", + MimeType = "text/plain", + Buffer = System.Text.Encoding.UTF8.GetBytes("hello") + }); + formData.Append("file", new FilePayload() + { + Name = "blob", + MimeType = "text/plain", + Buffer = System.Text.Encoding.UTF8.GetBytes("boo") + }); + + var (postBody, response) = await TaskUtils.WhenAll( + Server.WaitForRequest("/empty.html", request => + { + using StreamReader reader = new(request.Body, System.Text.Encoding.UTF8); + return reader.ReadToEndAsync().GetAwaiter().GetResult(); + }), + Context.APIRequest.PostAsync(Server.EmptyPage, new() { Multipart = formData }) + ); + + StringAssert.Contains("content-disposition: form-data; name=\"name\"\r\n\r\nJohn", postBody); + StringAssert.Contains("content-disposition: form-data; name=\"name\"\r\n\r\nDoe", postBody); + StringAssert.Contains("content-disposition: form-data; name=\"file\"; filename=\"f1.js\"\r\ncontent-type: text/javascript\r\n\r\nvar x = 10;\r\n;console.log(x);", postBody); + StringAssert.Contains("content-disposition: form-data; name=\"file\"; filename=\"f2.txt\"\r\ncontent-type: text/plain\r\n\r\nhello", postBody); + StringAssert.Contains("content-disposition: form-data; name=\"file\"; filename=\"blob\"\r\ncontent-type: text/plain\r\n\r\nboo", postBody); + Assert.AreEqual(200, response.Status); + } + private async Task ForAllMethods(IAPIRequestContext request, Func, Task> callback, string url, APIRequestContextOptions options = null) { var methodsToTest = new[] { "fetch", "delete", "get", "head", "patch", "post", "put" }; diff --git a/src/Playwright.Tests/BrowserContextPageEventTests.cs b/src/Playwright.Tests/BrowserContextPageEventTests.cs index 8ae841c67..cb72f5f12 100644 --- a/src/Playwright.Tests/BrowserContextPageEventTests.cs +++ b/src/Playwright.Tests/BrowserContextPageEventTests.cs @@ -226,7 +226,7 @@ public async Task ShouldWorkWithCtrlClicking() var popupEventTask = context.WaitForPageAsync(); await TaskUtils.WhenAll( popupEventTask, - page.ClickAsync("a", new() { Modifiers = new[] { TestConstants.IsMacOSX ? KeyboardModifier.Meta : KeyboardModifier.Control } })); + page.ClickAsync("a", new() { Modifiers = new[] { KeyboardModifier.ControlOrMeta } })); Assert.Null(await popupEventTask.Result.OpenerAsync()); } diff --git a/src/Playwright.Tests/BrowserTypeConnectOverCDPTests.cs b/src/Playwright.Tests/BrowserTypeConnectOverCDPTests.cs index 85ee028ef..8ed6d4c62 100644 --- a/src/Playwright.Tests/BrowserTypeConnectOverCDPTests.cs +++ b/src/Playwright.Tests/BrowserTypeConnectOverCDPTests.cs @@ -47,7 +47,6 @@ public async Task ShouldConnectToAnExistingCDPSession() } finally { - await browserServer.CloseAsync(); } } @@ -63,7 +62,7 @@ public async Task ShouldSendExtraHeadersWithConnectRequest() { "x-foo-bar", "fookek" } }, }).IgnoreException(); - var req = await waitForRequest; + (_, var req) = await waitForRequest; Assert.AreEqual("fookek", req.Headers["x-foo-bar"]); StringAssert.Contains("Playwright", req.Headers["user-agent"]); } @@ -96,4 +95,17 @@ public async Task ShouldReportAllPagesInAnExistingBrowser() await browserServer.CloseAsync(); } } + + [PlaywrightTest("chromium/chromium.spec.ts", "should report all pages in an existing browser")] + [Skip(SkipAttribute.Targets.Firefox, SkipAttribute.Targets.Webkit)] + public async Task ShouldPrintCustomWsCloseError() + { + Server.OnceWebSocketConnection(async (ws, request) => + { + await ws.ReceiveAsync(new byte[1024], CancellationToken.None); + await ws.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, "Oh my!", CancellationToken.None); + }); + var error = await PlaywrightAssert.ThrowsAsync(() => BrowserType.ConnectOverCDPAsync($"ws://localhost:{Server.Port}/ws")); + StringAssert.Contains("Browser logs:\n\nOh my!\n", error.Message); + } } diff --git a/src/Playwright.Tests/BrowserTypeConnectTests.cs b/src/Playwright.Tests/BrowserTypeConnectTests.cs index 449aac391..13964e7dc 100644 --- a/src/Playwright.Tests/BrowserTypeConnectTests.cs +++ b/src/Playwright.Tests/BrowserTypeConnectTests.cs @@ -79,7 +79,7 @@ public async Task ShouldSendDefaultUserAgentAndPlaywrightBrowserHeadersWithConne ["hello-foo"] = "i-am-bar", } }).IgnoreException(); - var request = await connectionRequest; + (_, var request) = await connectionRequest; StringAssert.Contains("Playwright", request.Headers["User-Agent"]); Assert.AreEqual(request.Headers["hello-foo"], "i-am-bar"); Assert.AreEqual(request.Headers["x-playwright-browser"], BrowserType.Name); @@ -511,6 +511,18 @@ public async Task SetInputFilesShouldPreserveLastModifiedTimestamp() Assert.LessOrEqual(Math.Abs(timestamps[i] - expectedTimestamps[i]), 1000); } + [PlaywrightTest("browsertype-connect.spec.ts", "should print custom ws close error")] + public async Task ShouldPrintCustomWsCloseError() + { + Server.OnceWebSocketConnection(async (webSocket, _) => + { + await webSocket.ReceiveAsync(new byte[1], CancellationToken.None); + await webSocket.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.PolicyViolation, "Oh my!", CancellationToken.None); + }); + var error = await PlaywrightAssert.ThrowsAsync(() => BrowserType.ConnectAsync($"ws://localhost:{Server.Port}/ws")); + StringAssert.Contains("Oh my!", error.Message); + } + private class RemoteServer { private Process Process { get; set; } diff --git a/src/Playwright.Tests/PageAddLocatorHandlerTests.cs b/src/Playwright.Tests/PageAddLocatorHandlerTests.cs index 44de65647..082733fe6 100644 --- a/src/Playwright.Tests/PageAddLocatorHandlerTests.cs +++ b/src/Playwright.Tests/PageAddLocatorHandlerTests.cs @@ -35,8 +35,10 @@ public async Task ShouldWork() var beforeCount = 0; var afterCount = 0; - await Page.AddLocatorHandlerAsync(Page.GetByText("This interstitial covers the button"), async () => + var originalLocator = Page.GetByText("This interstitial covers the button"); + await Page.AddLocatorHandlerAsync(originalLocator, async (locator) => { + Assert.AreEqual(originalLocator, locator); ++beforeCount; await Page.Locator("#close").ClickAsync(); ++afterCount; @@ -84,7 +86,7 @@ await Page.AddLocatorHandlerAsync(Page.GetByText("This interstitial covers the b { await Page.Locator("#close").ClickAsync(); } - }); + }, new() { NoWaitAfter = true }); foreach (var args in new[] { @@ -217,4 +219,180 @@ await Page.EvaluateAsync(@"() => await Expect(Page.Locator("#target")).ToBeVisibleAsync(); await Expect(Page.Locator("#interstitial")).Not.ToBeVisibleAsync(); } + + [PlaywrightTest("page-add-locator-handler.spec.ts", "should work when owner frame detaches")] + public async Task ShouldWorkWhenOwnerFrameDetaches() + { + await Page.GotoAsync(Server.EmptyPage); + + await Page.EvaluateAsync(@"() => + { + const iframe = document.createElement('iframe'); + iframe.src = 'data:text/html,hello from iframe'; + document.body.append(iframe); + + const target = document.createElement('button'); + target.textContent = 'Click me'; + target.id = 'target'; + target.addEventListener('click', () => window._clicked = true); + document.body.appendChild(target); + + const closeButton = document.createElement('button'); + closeButton.textContent = 'close'; + closeButton.id = 'close'; + closeButton.addEventListener('click', () => iframe.remove()); + document.body.appendChild(closeButton); + }"); + + await Page.AddLocatorHandlerAsync(Page.FrameLocator("iframe").Locator("body"), async () => + { + await Page.Locator("#close").ClickAsync(); + }); + + await Page.Locator("#target").ClickAsync(); + Assert.Null(await Page.QuerySelectorAsync("iframe")); + Assert.True(await Page.EvaluateAsync("window._clicked")); + } + + [PlaywrightTest("page-add-locator-handler.spec.ts", "should work with times: option")] + public async Task ShouldWorkWithTimesOption() + { + await Page.GotoAsync(Server.Prefix + "/input/handle-locator.html"); + + var called = 0; + await Page.AddLocatorHandlerAsync(Page.Locator("body"), () => + { + ++called; + return Task.CompletedTask; + }, new() { NoWaitAfter = true, Times = 2 }); + + await Page.Locator("#aside").HoverAsync(); + await Page.EvaluateAsync(@"() => + { + window.clicked = 0; + window.setupAnnoyingInterstitial('mouseover', 4); + }"); + var error = await PlaywrightAssert.ThrowsAsync(() => Page.Locator("#target").ClickAsync(new() { Timeout = 3000 })); + Assert.AreEqual(2, called); + Assert.AreEqual(0, await Page.EvaluateAsync("window.clicked")); + await Expect(Page.Locator("#interstitial")).ToBeVisibleAsync(); + StringAssert.Contains("Timeout 3000ms exceeded", error.Message); + StringAssert.Contains("
This interstitial covers the button
from
subtree intercepts pointer events", error.Message); + } + + [PlaywrightTest("page-add-locator-handler.spec.ts", "should wait for hidden by default")] + public async Task ShouldWaitForHiddenByDefault() + { + await Page.GotoAsync(Server.Prefix + "/input/handle-locator.html"); + + var called = 0; + await Page.AddLocatorHandlerAsync(Page.GetByRole(AriaRole.Button, new() { Name = "close" }), async button => + { + called++; + await button.ClickAsync(); + }); + + await Page.Locator("#aside").HoverAsync(); + await Page.EvaluateAsync(@"() => + { + window.clicked = 0; + window.setupAnnoyingInterstitial('timeout', 1); + }"); + await Page.Locator("#target").ClickAsync(); + Assert.AreEqual(1, await Page.EvaluateAsync("window.clicked")); + await Expect(Page.Locator("#interstitial")).Not.ToBeVisibleAsync(); + Assert.AreEqual(1, called); + } + + [PlaywrightTest("page-add-locator-handler.spec.ts", "should wait for hidden by default 2")] + public async Task ShouldWaitForHiddenByDefault2() + { + await Page.GotoAsync(Server.Prefix + "/input/handle-locator.html"); + + var called = 0; + await Page.AddLocatorHandlerAsync(Page.GetByRole(AriaRole.Button, new() { Name = "close" }), button => + { + called++; + return Task.CompletedTask; + }); + + await Page.Locator("#aside").HoverAsync(); + await Page.EvaluateAsync(@"() => + { + window.clicked = 0; + window.setupAnnoyingInterstitial('hide', 1); + }"); + var error = await PlaywrightAssert.ThrowsAsync(() => Page.Locator("#target").ClickAsync(new() { Timeout = 3000 })); + Assert.AreEqual(0, await Page.EvaluateAsync("window.clicked")); + await Expect(Page.Locator("#interstitial")).ToBeVisibleAsync(); + Assert.AreEqual(1, called); + StringAssert.Contains("locator handler has finished, waiting for GetByRole(AriaRole.Button, new() { Name = \"close\" }) to be hidden", error.Message); + } + + [PlaywrightTest("page-add-locator-handler.spec.ts", "should work with noWaitAfter")] + public async Task ShouldWorkWithNoWaitAfter() + { + await Page.GotoAsync(Server.Prefix + "/input/handle-locator.html"); + + var called = 0; + await Page.AddLocatorHandlerAsync(Page.GetByRole(AriaRole.Button, new() { Name = "close" }), async button => + { + called++; + if (called == 1) + { + await button.ClickAsync(); + } + else + { + await Page.Locator("#interstitial").WaitForAsync(new() { State = WaitForSelectorState.Hidden }); + } + }, new() { NoWaitAfter = true }); + + await Page.Locator("#aside").HoverAsync(); + await Page.EvaluateAsync(@"() => + { + window.clicked = 0; + window.setupAnnoyingInterstitial('timeout', 1); + }"); + await Page.Locator("#target").ClickAsync(); + Assert.AreEqual(1, await Page.EvaluateAsync("window.clicked")); + await Expect(Page.Locator("#interstitial")).Not.ToBeVisibleAsync(); + Assert.AreEqual(2, called); + } + + [PlaywrightTest("page-add-locator-handler.spec.ts", "should removeLocatorHandler")] + public async Task ShouldRemoveLocatorHandler() + { + await Page.GotoAsync(Server.Prefix + "/input/handle-locator.html"); + + var called = 0; + await Page.AddLocatorHandlerAsync(Page.GetByRole(AriaRole.Button, new() { Name = "close" }), async button => + { + ++called; + await button.ClickAsync(); + }); + + await Page.EvaluateAsync(@"() => + { + window.clicked = 0; + window.setupAnnoyingInterstitial('hide', 1); + }"); + await Page.Locator("#target").ClickAsync(); + Assert.AreEqual(1, called); + Assert.AreEqual(1, await Page.EvaluateAsync("window.clicked")); + await Expect(Page.Locator("#interstitial")).Not.ToBeVisibleAsync(); + + await Page.EvaluateAsync(@"() => + { + window.clicked = 0; + window.setupAnnoyingInterstitial('hide', 1); + }"); + await Page.RemoveLocatorHandlerAsync(Page.GetByRole(AriaRole.Button, new() { Name = "close" })); + + var error = await PlaywrightAssert.ThrowsAsync(() => Page.Locator("#target").ClickAsync(new() { Timeout = 3000 })); + Assert.AreEqual(1, called); + Assert.AreEqual(0, await Page.EvaluateAsync("window.clicked")); + await Expect(Page.Locator("#interstitial")).ToBeVisibleAsync(); + StringAssert.Contains("Timeout 3000ms exceeded", error.Message); + } } diff --git a/src/Playwright/API/Generated/Enums/KeyboardModifier.cs b/src/Playwright/API/Generated/Enums/KeyboardModifier.cs index 10ba699c5..ff86d0023 100644 --- a/src/Playwright/API/Generated/Enums/KeyboardModifier.cs +++ b/src/Playwright/API/Generated/Enums/KeyboardModifier.cs @@ -34,6 +34,8 @@ public enum KeyboardModifier Alt, [EnumMember(Value = "Control")] Control, + [EnumMember(Value = "ControlOrMeta")] + ControlOrMeta, [EnumMember(Value = "Meta")] Meta, [EnumMember(Value = "Shift")] diff --git a/src/Playwright/API/Generated/IAPIRequestContext.cs b/src/Playwright/API/Generated/IAPIRequestContext.cs index b33a5e02b..f90ff8996 100644 --- a/src/Playwright/API/Generated/IAPIRequestContext.cs +++ b/src/Playwright/API/Generated/IAPIRequestContext.cs @@ -82,10 +82,10 @@ public partial interface IAPIRequestContext /// /// Sends HTTP(S) request and returns its response. The method will populate request /// cookies from the context and update context cookies from the response. The method - /// will automatically follow redirects. JSON objects can be passed directly to the - /// request. + /// will automatically follow redirects. /// /// **Usage** + /// JSON objects can be passed directly to the request: /// /// var data = new Dictionary<string, object>() {
/// { "title", "Book Title" },
@@ -94,9 +94,10 @@ public partial interface IAPIRequestContext /// await Request.FetchAsync("https://example.com/api/createBook", new() { Method = "post", DataObject = data }); ///
/// - /// The common way to send file(s) in the body of a request is to encode it as form - /// fields with multipart/form-data encoding. You can achieve that with Playwright - /// API like this: + /// The common way to send file(s) in the body of a request is to upload them as form + /// fields with multipart/form-data encoding. Use to + /// construct request body and pass it to the request as + /// parameter: /// /// /// var file = new FilePayload()
@@ -118,10 +119,10 @@ public partial interface IAPIRequestContext /// /// Sends HTTP(S) request and returns its response. The method will populate request /// cookies from the context and update context cookies from the response. The method - /// will automatically follow redirects. JSON objects can be passed directly to the - /// request. + /// will automatically follow redirects. /// /// **Usage** + /// JSON objects can be passed directly to the request: /// /// var data = new Dictionary<string, object>() {
/// { "title", "Book Title" },
@@ -130,9 +131,10 @@ public partial interface IAPIRequestContext /// await Request.FetchAsync("https://example.com/api/createBook", new() { Method = "post", DataObject = data }); ///
/// - /// The common way to send file(s) in the body of a request is to encode it as form - /// fields with multipart/form-data encoding. You can achieve that with Playwright - /// API like this: + /// The common way to send file(s) in the body of a request is to upload them as form + /// fields with multipart/form-data encoding. Use to + /// construct request body and pass it to the request as + /// parameter: /// /// /// var file = new FilePayload()
@@ -210,7 +212,7 @@ public partial interface IAPIRequestContext /// JSON objects can be passed directly to the request: /// /// var data = new Dictionary<string, object>() {
- /// { "firstNam", "John" },
+ /// { "firstName", "John" },
/// { "lastName", "Doe" }
/// };
/// await request.PostAsync("https://example.com/api/createBook", new() { DataObject = data }); @@ -228,8 +230,8 @@ public partial interface IAPIRequestContext ///
/// /// The common way to send file(s) in the body of a request is to upload them as form - /// fields with multipart/form-data encoding. You can achieve that with Playwright - /// API like this: + /// fields with multipart/form-data encoding. Use to + /// construct request body and pass it to the request as multipart parameter: /// /// /// var file = new FilePayload()
diff --git a/src/Playwright/API/Generated/IBrowserContext.cs b/src/Playwright/API/Generated/IBrowserContext.cs index 82c5ceb02..81d6643af 100644 --- a/src/Playwright/API/Generated/IBrowserContext.cs +++ b/src/Playwright/API/Generated/IBrowserContext.cs @@ -136,7 +136,9 @@ public partial interface IBrowserContext /// The earliest moment that page is available is when it has navigated to the initial /// url. For example, when opening a popup with window.open('http://example.com'), /// this event will fire when the network request to "http://example.com" is done and - /// its response has started loading in the popup. + /// its response has started loading in the popup. If you would like to route/listen + /// to this network request, use and + /// respectively instead of similar methods on the . /// /// /// var popup = await context.RunAndWaitForPageAsync(async =>
diff --git a/src/Playwright/API/Generated/IElementHandle.cs b/src/Playwright/API/Generated/IElementHandle.cs index 3c4828d8d..700f59897 100644 --- a/src/Playwright/API/Generated/IElementHandle.cs +++ b/src/Playwright/API/Generated/IElementHandle.cs @@ -532,7 +532,7 @@ public partial interface IElementHandle : IJSHandle /// /// /// Following modification shortcuts are also supported: Shift, Control, - /// Alt, Meta, ShiftLeft. + /// Alt, Meta, ShiftLeft, ControlOrMeta. /// /// /// Holding down Shift will type the text that corresponds to the diff --git a/src/Playwright/API/Generated/IFormData.cs b/src/Playwright/API/Generated/IFormData.cs index 0dd212732..6c06f8fcc 100644 --- a/src/Playwright/API/Generated/IFormData.cs +++ b/src/Playwright/API/Generated/IFormData.cs @@ -29,6 +29,154 @@ namespace Microsoft.Playwright; /// The is used create form data that is sent via . public partial interface IFormData { + /// + /// + /// Appends a new value onto an existing key inside a FormData object, or adds the key + /// if it does not already exist. File values can be passed either as Path or + /// as FilePayload. Multiple fields with the same name can be added. + /// + /// + /// The difference between and + /// is that if the specified key already exists, will overwrite + /// all existing values with the new one, whereas will + /// append the new value onto the end of the existing set of values. + /// + /// + /// var multipart = Context.APIRequest.CreateFormData();
+ /// // Only name and value are set.
+ /// multipart.Append("firstName", "John");
+ /// // Name, value, filename and Content-Type are set.
+ /// multipart.Append("attachment", new FilePayload()
+ /// {
+ /// Name = "pic.jpg",
+ /// MimeType = "image/jpeg",
+ /// Buffer = File.ReadAllBytes("john.jpg")
+ /// });
+ /// // Name, value, filename and Content-Type are set.
+ /// multipart.Append("attachment", new FilePayload()
+ /// {
+ /// Name = "table.csv",
+ /// MimeType = "text/csv",
+ /// Buffer = File.ReadAllBytes("my-tble.csv")
+ /// });
+ /// await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart }); + ///
+ ///
+ /// Field name. + /// Field value. + IFormData Append(string name, string value); + + /// + /// + /// Appends a new value onto an existing key inside a FormData object, or adds the key + /// if it does not already exist. File values can be passed either as Path or + /// as FilePayload. Multiple fields with the same name can be added. + /// + /// + /// The difference between and + /// is that if the specified key already exists, will overwrite + /// all existing values with the new one, whereas will + /// append the new value onto the end of the existing set of values. + /// + /// + /// var multipart = Context.APIRequest.CreateFormData();
+ /// // Only name and value are set.
+ /// multipart.Append("firstName", "John");
+ /// // Name, value, filename and Content-Type are set.
+ /// multipart.Append("attachment", new FilePayload()
+ /// {
+ /// Name = "pic.jpg",
+ /// MimeType = "image/jpeg",
+ /// Buffer = File.ReadAllBytes("john.jpg")
+ /// });
+ /// // Name, value, filename and Content-Type are set.
+ /// multipart.Append("attachment", new FilePayload()
+ /// {
+ /// Name = "table.csv",
+ /// MimeType = "text/csv",
+ /// Buffer = File.ReadAllBytes("my-tble.csv")
+ /// });
+ /// await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart }); + ///
+ ///
+ /// Field name. + /// Field value. + IFormData Append(string name, bool value); + + /// + /// + /// Appends a new value onto an existing key inside a FormData object, or adds the key + /// if it does not already exist. File values can be passed either as Path or + /// as FilePayload. Multiple fields with the same name can be added. + /// + /// + /// The difference between and + /// is that if the specified key already exists, will overwrite + /// all existing values with the new one, whereas will + /// append the new value onto the end of the existing set of values. + /// + /// + /// var multipart = Context.APIRequest.CreateFormData();
+ /// // Only name and value are set.
+ /// multipart.Append("firstName", "John");
+ /// // Name, value, filename and Content-Type are set.
+ /// multipart.Append("attachment", new FilePayload()
+ /// {
+ /// Name = "pic.jpg",
+ /// MimeType = "image/jpeg",
+ /// Buffer = File.ReadAllBytes("john.jpg")
+ /// });
+ /// // Name, value, filename and Content-Type are set.
+ /// multipart.Append("attachment", new FilePayload()
+ /// {
+ /// Name = "table.csv",
+ /// MimeType = "text/csv",
+ /// Buffer = File.ReadAllBytes("my-tble.csv")
+ /// });
+ /// await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart }); + ///
+ ///
+ /// Field name. + /// Field value. + IFormData Append(string name, int value); + + /// + /// + /// Appends a new value onto an existing key inside a FormData object, or adds the key + /// if it does not already exist. File values can be passed either as Path or + /// as FilePayload. Multiple fields with the same name can be added. + /// + /// + /// The difference between and + /// is that if the specified key already exists, will overwrite + /// all existing values with the new one, whereas will + /// append the new value onto the end of the existing set of values. + /// + /// + /// var multipart = Context.APIRequest.CreateFormData();
+ /// // Only name and value are set.
+ /// multipart.Append("firstName", "John");
+ /// // Name, value, filename and Content-Type are set.
+ /// multipart.Append("attachment", new FilePayload()
+ /// {
+ /// Name = "pic.jpg",
+ /// MimeType = "image/jpeg",
+ /// Buffer = File.ReadAllBytes("john.jpg")
+ /// });
+ /// // Name, value, filename and Content-Type are set.
+ /// multipart.Append("attachment", new FilePayload()
+ /// {
+ /// Name = "table.csv",
+ /// MimeType = "text/csv",
+ /// Buffer = File.ReadAllBytes("my-tble.csv")
+ /// });
+ /// await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart }); + ///
+ ///
+ /// Field name. + /// Field value. + IFormData Append(string name, FilePayload value); + ///
/// /// Sets a field on the form. File values can be passed either as Path or as @@ -45,6 +193,7 @@ public partial interface IFormData /// MimeType = "image/jpeg",
/// Buffer = File.ReadAllBytes("john.jpg")
/// });
+ /// multipart.Set("age", 30);
/// await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart }); ///
///
@@ -68,6 +217,7 @@ public partial interface IFormData /// MimeType = "image/jpeg",
/// Buffer = File.ReadAllBytes("john.jpg")
/// });
+ /// multipart.Set("age", 30);
/// await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart }); ///
/// @@ -91,6 +241,7 @@ public partial interface IFormData /// MimeType = "image/jpeg",
/// Buffer = File.ReadAllBytes("john.jpg")
/// });
+ /// multipart.Set("age", 30);
/// await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart }); ///
/// @@ -114,6 +265,7 @@ public partial interface IFormData /// MimeType = "image/jpeg",
/// Buffer = File.ReadAllBytes("john.jpg")
/// });
+ /// multipart.Set("age", 30);
/// await Page.APIRequest.PostAsync("https://localhost/submit", new() { Multipart = multipart }); ///
/// diff --git a/src/Playwright/API/Generated/IFrame.cs b/src/Playwright/API/Generated/IFrame.cs index d7a01e6f7..d3f7cdbba 100644 --- a/src/Playwright/API/Generated/IFrame.cs +++ b/src/Playwright/API/Generated/IFrame.cs @@ -1067,7 +1067,8 @@ public partial interface IFrame /// /// /// Following modification shortcuts are also supported: Shift, Control, - /// Alt, Meta, ShiftLeft. + /// Alt, Meta, ShiftLeft, ControlOrMeta. ControlOrMeta + /// resolves to Control on Windows and Linux and to Meta on macOS. /// /// /// Holding down Shift will type the text that corresponds to the @@ -1726,6 +1727,12 @@ public partial interface IFrame /// await frame.WaitForLoadStateAsync(); // Defaults to LoadState.Load /// /// + /// + /// + /// Most of the time, this method is not needed because Playwright auto-waits + /// before every action. + /// + /// /// /// Optional load state to wait for, defaults to load. If the state has been /// already reached while loading current document, the method resolves immediately. diff --git a/src/Playwright/API/Generated/IKeyboard.cs b/src/Playwright/API/Generated/IKeyboard.cs index b57a82e94..c207e85dc 100644 --- a/src/Playwright/API/Generated/IKeyboard.cs +++ b/src/Playwright/API/Generated/IKeyboard.cs @@ -86,7 +86,8 @@ public partial interface IKeyboard /// /// /// Following modification shortcuts are also supported: Shift, Control, - /// Alt, Meta, ShiftLeft. + /// Alt, Meta, ShiftLeft, ControlOrMeta. ControlOrMeta + /// resolves to Control on Windows and Linux and to Meta on macOS. /// /// /// Holding down Shift will type the text that corresponds to the @@ -152,7 +153,8 @@ public partial interface IKeyboard /// /// /// Following modification shortcuts are also supported: Shift, Control, - /// Alt, Meta, ShiftLeft. + /// Alt, Meta, ShiftLeft, ControlOrMeta. ControlOrMeta + /// resolves to Control on Windows and Linux and to Meta on macOS. /// /// /// Holding down Shift will type the text that corresponds to the diff --git a/src/Playwright/API/Generated/ILocator.cs b/src/Playwright/API/Generated/ILocator.cs index bb9c150c1..ebe986fe8 100644 --- a/src/Playwright/API/Generated/ILocator.cs +++ b/src/Playwright/API/Generated/ILocator.cs @@ -1095,7 +1095,8 @@ public partial interface ILocator /// /// /// Following modification shortcuts are also supported: Shift, Control, - /// Alt, Meta, ShiftLeft. + /// Alt, Meta, ShiftLeft, ControlOrMeta. ControlOrMeta + /// resolves to Control on Windows and Linux and to Meta on macOS. /// /// /// Holding down Shift will type the text that corresponds to the diff --git a/src/Playwright/API/Generated/ILocatorAssertions.cs b/src/Playwright/API/Generated/ILocatorAssertions.cs index 2e8690884..f00bce8c1 100644 --- a/src/Playwright/API/Generated/ILocatorAssertions.cs +++ b/src/Playwright/API/Generated/ILocatorAssertions.cs @@ -394,6 +394,66 @@ public partial interface ILocatorAssertions /// Call options Task ToContainTextAsync(IEnumerable expected, LocatorAssertionsToContainTextOptions? options = default); + /// + /// + /// Ensures the points to an element with a given accessible + /// description. + /// + /// **Usage** + /// + /// var locator = Page.GetByTestId("save-button");
+ /// await Expect(locator).toHaveAccessibleDescriptionAsync("Save results to disk"); + ///
+ ///
+ /// Expected accessible description. + /// Call options + Task ToHaveAccessibleDescriptionAsync(string description, LocatorAssertionsToHaveAccessibleDescriptionOptions? options = default); + + /// + /// + /// Ensures the points to an element with a given accessible + /// description. + /// + /// **Usage** + /// + /// var locator = Page.GetByTestId("save-button");
+ /// await Expect(locator).toHaveAccessibleDescriptionAsync("Save results to disk"); + ///
+ ///
+ /// Expected accessible description. + /// Call options + Task ToHaveAccessibleDescriptionAsync(Regex description, LocatorAssertionsToHaveAccessibleDescriptionOptions? options = default); + + /// + /// + /// Ensures the points to an element with a given accessible + /// name. + /// + /// **Usage** + /// + /// var locator = Page.GetByTestId("save-button");
+ /// await Expect(locator).toHaveAccessibleNameAsync("Save to disk"); + ///
+ ///
+ /// Expected accessible name. + /// Call options + Task ToHaveAccessibleNameAsync(string name, LocatorAssertionsToHaveAccessibleNameOptions? options = default); + + /// + /// + /// Ensures the points to an element with a given accessible + /// name. + /// + /// **Usage** + /// + /// var locator = Page.GetByTestId("save-button");
+ /// await Expect(locator).toHaveAccessibleNameAsync("Save to disk"); + ///
+ ///
+ /// Expected accessible name. + /// Call options + Task ToHaveAccessibleNameAsync(Regex name, LocatorAssertionsToHaveAccessibleNameOptions? options = default); + /// /// Ensures the points to an element with given attribute. /// **Usage** @@ -607,6 +667,26 @@ public partial interface ILocatorAssertions /// Call options Task ToHaveJSPropertyAsync(string name, object value, LocatorAssertionsToHaveJSPropertyOptions? options = default); + /// + /// + /// Ensures the points to an element with a given ARIA + /// role. + /// + /// + /// Note that role is matched as a string, disregarding the ARIA role hierarchy. For + /// example, asserting a superclass role "checkbox" on an element with a subclass + /// role "switch" will fail. + /// + /// **Usage** + /// + /// var locator = Page.GetByTestId("save-button");
+ /// await Expect(locator).ToHaveRoleAsync(AriaRole.Button); + ///
+ ///
+ /// Required aria role. + /// Call options + Task ToHaveRoleAsync(AriaRole role, LocatorAssertionsToHaveRoleOptions? options = default); + /// /// /// Ensures the points to an element with the given text. All diff --git a/src/Playwright/API/Generated/IPage.cs b/src/Playwright/API/Generated/IPage.cs index 79c4471ba..d9e5081be 100644 --- a/src/Playwright/API/Generated/IPage.cs +++ b/src/Playwright/API/Generated/IPage.cs @@ -212,7 +212,9 @@ public partial interface IPage /// The earliest moment that page is available is when it has navigated to the initial /// url. For example, when opening a popup with window.open('http://example.com'), /// this event will fire when the network request to "http://example.com" is done and - /// its response has started loading in the popup. + /// its response has started loading in the popup. If you would like to route/listen + /// to this network request, use and + /// respectively instead of similar methods on the . /// /// /// var popup = await page.RunAndWaitForPopupAsync(async () =>
@@ -1626,7 +1628,8 @@ public partial interface IPage /// /// /// Following modification shortcuts are also supported: Shift, Control, - /// Alt, Meta, ShiftLeft. + /// Alt, Meta, ShiftLeft, ControlOrMeta. ControlOrMeta + /// resolves to Control on Windows and Linux and to Meta on macOS. /// /// /// Holding down Shift will type the text that corresponds to the @@ -1715,6 +1718,11 @@ public partial interface IPage /// be triggered. /// /// + /// After executing the handler, Playwright will ensure that overlay that triggered + /// the handler is not visible anymore. You can opt-out of this behavior with . + /// + /// /// The execution time of the handler counts towards the timeout of the action/assertion /// that executed the handler. If your handler takes too long, it might cause timeouts. /// @@ -1749,21 +1757,30 @@ public partial interface IPage /// /// An example with a custom callback on every actionability check. It uses a <body> /// locator that is always visible, so the handler is called before every actionability - /// check: + /// check. It is important to specify , because the handler + /// does not hide the <body> element. /// /// /// // Setup the handler.
/// await page.AddLocatorHandlerAsync(page.Locator("body"), async () => {
/// await page.EvaluateAsync("window.removeObstructionsForTestIfNeeded()");
- /// });
+ /// }, new() { NoWaitAfter = true });
///
/// // Write the test as usual.
/// await page.GotoAsync("https://example.com");
/// await page.GetByRole("button", new() { Name = "Start here" }).ClickAsync(); ///
+ /// + /// Handler takes the original locator as an argument. You can also automatically remove + /// the handler after a number of invocations by setting : + /// + /// + /// await page.AddLocatorHandlerAsync(page.GetByText("Sign up to the newsletter"), async locator => {
+ /// await locator.ClickAsync();
+ /// }, new() { Times = 1 }); + ///
///
/// - /// This method is experimental and its behavior may change in the upcoming releases. /// /// Running the handler will alter your page state mid-test. For example it will change /// the currently focused element and move the mouse. Make sure that actions that run @@ -1784,7 +1801,17 @@ public partial interface IPage /// Function that should be run once appears. This function /// should get rid of the element that blocks actions like click. /// - Task AddLocatorHandlerAsync(ILocator locator, Func handler); + /// Call options + Task AddLocatorHandlerAsync(ILocator locator, Func handler, PageAddLocatorHandlerOptions? options = default); + + /// + /// + /// Removes all locator handlers added by + /// for a specific locator. + /// + /// + /// Locator passed to . + Task RemoveLocatorHandlerAsync(ILocator locator); /// /// @@ -1851,6 +1878,10 @@ public partial interface IPage /// issue. We recommend disabling Service Workers when using request interception by /// setting to 'block'. /// + /// + /// will not intercept the first request of a popup page. + /// Use instead. + /// /// Enabling routing disables http cache. /// /// @@ -1909,6 +1940,10 @@ public partial interface IPage /// issue. We recommend disabling Service Workers when using request interception by /// setting to 'block'. /// + /// + /// will not intercept the first request of a popup page. + /// Use instead. + /// /// Enabling routing disables http cache. /// /// @@ -1967,6 +2002,10 @@ public partial interface IPage /// issue. We recommend disabling Service Workers when using request interception by /// setting to 'block'. /// + /// + /// will not intercept the first request of a popup page. + /// Use instead. + /// /// Enabling routing disables http cache. /// /// @@ -2824,6 +2863,12 @@ public partial interface IPage /// Console.WriteLine(await popup.TitleAsync()); // popup is ready to use. /// /// + /// + /// + /// Most of the time, this method is not needed because Playwright auto-waits + /// before every action. + /// + /// /// /// Optional load state to wait for, defaults to load. If the state has been /// already reached while loading current document, the method resolves immediately. diff --git a/src/Playwright/API/Generated/Options/APIRequestContextOptions.cs b/src/Playwright/API/Generated/Options/APIRequestContextOptions.cs index 0be4800b7..eca9668ee 100644 --- a/src/Playwright/API/Generated/Options/APIRequestContextOptions.cs +++ b/src/Playwright/API/Generated/Options/APIRequestContextOptions.cs @@ -158,8 +158,8 @@ public APIRequestContextOptions(APIRequestContextOptions clone) /// Provides an object that will be serialized as html form using multipart/form-data /// encoding and sent as this request body. If this parameter is specified content-type /// header will be set to multipart/form-data unless explicitly provided. File - /// values can be passed either as fs.ReadStream - /// or as file-like object containing file name, mime-type and its content. + /// values can be passed as file-like object containing file name, mime-type and its + /// content. /// /// An instance of can be created via . ///
diff --git a/src/Playwright/API/Generated/Options/ElementHandleClickOptions.cs b/src/Playwright/API/Generated/Options/ElementHandleClickOptions.cs index b30bc013b..1cc479067 100644 --- a/src/Playwright/API/Generated/Options/ElementHandleClickOptions.cs +++ b/src/Playwright/API/Generated/Options/ElementHandleClickOptions.cs @@ -81,7 +81,8 @@ public ElementHandleClickOptions(ElementHandleClickOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/ElementHandleDblClickOptions.cs b/src/Playwright/API/Generated/Options/ElementHandleDblClickOptions.cs index b6b7fabf1..573f90b11 100644 --- a/src/Playwright/API/Generated/Options/ElementHandleDblClickOptions.cs +++ b/src/Playwright/API/Generated/Options/ElementHandleDblClickOptions.cs @@ -76,7 +76,8 @@ public ElementHandleDblClickOptions(ElementHandleDblClickOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/ElementHandleHoverOptions.cs b/src/Playwright/API/Generated/Options/ElementHandleHoverOptions.cs index ebbacad3d..6caf2a488 100644 --- a/src/Playwright/API/Generated/Options/ElementHandleHoverOptions.cs +++ b/src/Playwright/API/Generated/Options/ElementHandleHoverOptions.cs @@ -61,7 +61,8 @@ public ElementHandleHoverOptions(ElementHandleHoverOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/ElementHandleTapOptions.cs b/src/Playwright/API/Generated/Options/ElementHandleTapOptions.cs index ea6db2883..51d77747e 100644 --- a/src/Playwright/API/Generated/Options/ElementHandleTapOptions.cs +++ b/src/Playwright/API/Generated/Options/ElementHandleTapOptions.cs @@ -61,7 +61,8 @@ public ElementHandleTapOptions(ElementHandleTapOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/FrameClickOptions.cs b/src/Playwright/API/Generated/Options/FrameClickOptions.cs index 2db6c72a8..d4e8625b1 100644 --- a/src/Playwright/API/Generated/Options/FrameClickOptions.cs +++ b/src/Playwright/API/Generated/Options/FrameClickOptions.cs @@ -82,7 +82,8 @@ public FrameClickOptions(FrameClickOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/FrameDblClickOptions.cs b/src/Playwright/API/Generated/Options/FrameDblClickOptions.cs index 1803e5437..4912abe3f 100644 --- a/src/Playwright/API/Generated/Options/FrameDblClickOptions.cs +++ b/src/Playwright/API/Generated/Options/FrameDblClickOptions.cs @@ -77,7 +77,8 @@ public FrameDblClickOptions(FrameDblClickOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/FrameHoverOptions.cs b/src/Playwright/API/Generated/Options/FrameHoverOptions.cs index de38d6aaa..af8e88466 100644 --- a/src/Playwright/API/Generated/Options/FrameHoverOptions.cs +++ b/src/Playwright/API/Generated/Options/FrameHoverOptions.cs @@ -62,7 +62,8 @@ public FrameHoverOptions(FrameHoverOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/FrameTapOptions.cs b/src/Playwright/API/Generated/Options/FrameTapOptions.cs index 0fc8f41cc..43c8d67a8 100644 --- a/src/Playwright/API/Generated/Options/FrameTapOptions.cs +++ b/src/Playwright/API/Generated/Options/FrameTapOptions.cs @@ -62,7 +62,8 @@ public FrameTapOptions(FrameTapOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/LocatorAssertionsToHaveAccessibleDescriptionOptions.cs b/src/Playwright/API/Generated/Options/LocatorAssertionsToHaveAccessibleDescriptionOptions.cs new file mode 100644 index 000000000..ae254e61e --- /dev/null +++ b/src/Playwright/API/Generated/Options/LocatorAssertionsToHaveAccessibleDescriptionOptions.cs @@ -0,0 +1,60 @@ +/* + * 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.Serialization; + +#nullable enable + +namespace Microsoft.Playwright; + +public class LocatorAssertionsToHaveAccessibleDescriptionOptions +{ + public LocatorAssertionsToHaveAccessibleDescriptionOptions() { } + + public LocatorAssertionsToHaveAccessibleDescriptionOptions(LocatorAssertionsToHaveAccessibleDescriptionOptions clone) + { + if (clone == null) + { + return; + } + + IgnoreCase = clone.IgnoreCase; + Timeout = clone.Timeout; + } + + /// + /// + /// Whether to perform case-insensitive match. option + /// takes precedence over the corresponding regular expression flag if specified. + /// + /// + [JsonPropertyName("ignoreCase")] + public bool? IgnoreCase { get; set; } + + /// Time to retry the assertion for in milliseconds. Defaults to 5000. + [JsonPropertyName("timeout")] + public float? Timeout { get; set; } +} + +#nullable disable diff --git a/src/Playwright/API/Generated/Options/LocatorAssertionsToHaveAccessibleNameOptions.cs b/src/Playwright/API/Generated/Options/LocatorAssertionsToHaveAccessibleNameOptions.cs new file mode 100644 index 000000000..74d1cdcb6 --- /dev/null +++ b/src/Playwright/API/Generated/Options/LocatorAssertionsToHaveAccessibleNameOptions.cs @@ -0,0 +1,60 @@ +/* + * 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.Serialization; + +#nullable enable + +namespace Microsoft.Playwright; + +public class LocatorAssertionsToHaveAccessibleNameOptions +{ + public LocatorAssertionsToHaveAccessibleNameOptions() { } + + public LocatorAssertionsToHaveAccessibleNameOptions(LocatorAssertionsToHaveAccessibleNameOptions clone) + { + if (clone == null) + { + return; + } + + IgnoreCase = clone.IgnoreCase; + Timeout = clone.Timeout; + } + + /// + /// + /// Whether to perform case-insensitive match. option + /// takes precedence over the corresponding regular expression flag if specified. + /// + /// + [JsonPropertyName("ignoreCase")] + public bool? IgnoreCase { get; set; } + + /// Time to retry the assertion for in milliseconds. Defaults to 5000. + [JsonPropertyName("timeout")] + public float? Timeout { get; set; } +} + +#nullable disable diff --git a/src/Playwright/API/Generated/Options/LocatorAssertionsToHaveRoleOptions.cs b/src/Playwright/API/Generated/Options/LocatorAssertionsToHaveRoleOptions.cs new file mode 100644 index 000000000..72e1406fd --- /dev/null +++ b/src/Playwright/API/Generated/Options/LocatorAssertionsToHaveRoleOptions.cs @@ -0,0 +1,50 @@ +/* + * 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.Serialization; + +#nullable enable + +namespace Microsoft.Playwright; + +public class LocatorAssertionsToHaveRoleOptions +{ + public LocatorAssertionsToHaveRoleOptions() { } + + public LocatorAssertionsToHaveRoleOptions(LocatorAssertionsToHaveRoleOptions clone) + { + if (clone == null) + { + return; + } + + Timeout = clone.Timeout; + } + + /// Time to retry the assertion for in milliseconds. Defaults to 5000. + [JsonPropertyName("timeout")] + public float? Timeout { get; set; } +} + +#nullable disable diff --git a/src/Playwright/API/Generated/Options/LocatorClickOptions.cs b/src/Playwright/API/Generated/Options/LocatorClickOptions.cs index f416866f4..89dddacdc 100644 --- a/src/Playwright/API/Generated/Options/LocatorClickOptions.cs +++ b/src/Playwright/API/Generated/Options/LocatorClickOptions.cs @@ -81,7 +81,8 @@ public LocatorClickOptions(LocatorClickOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/LocatorDblClickOptions.cs b/src/Playwright/API/Generated/Options/LocatorDblClickOptions.cs index 33265146d..d4d1f4cf2 100644 --- a/src/Playwright/API/Generated/Options/LocatorDblClickOptions.cs +++ b/src/Playwright/API/Generated/Options/LocatorDblClickOptions.cs @@ -76,7 +76,8 @@ public LocatorDblClickOptions(LocatorDblClickOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/LocatorHoverOptions.cs b/src/Playwright/API/Generated/Options/LocatorHoverOptions.cs index 19f0d35d3..e2da4f8c4 100644 --- a/src/Playwright/API/Generated/Options/LocatorHoverOptions.cs +++ b/src/Playwright/API/Generated/Options/LocatorHoverOptions.cs @@ -61,7 +61,8 @@ public LocatorHoverOptions(LocatorHoverOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/LocatorTapOptions.cs b/src/Playwright/API/Generated/Options/LocatorTapOptions.cs index c5c0e2674..9d0d9c2f5 100644 --- a/src/Playwright/API/Generated/Options/LocatorTapOptions.cs +++ b/src/Playwright/API/Generated/Options/LocatorTapOptions.cs @@ -61,7 +61,8 @@ public LocatorTapOptions(LocatorTapOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/PageAddLocatorHandlerOptions.cs b/src/Playwright/API/Generated/Options/PageAddLocatorHandlerOptions.cs new file mode 100644 index 000000000..4c748933a --- /dev/null +++ b/src/Playwright/API/Generated/Options/PageAddLocatorHandlerOptions.cs @@ -0,0 +1,67 @@ +/* + * 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.Serialization; + +#nullable enable + +namespace Microsoft.Playwright; + +public class PageAddLocatorHandlerOptions +{ + public PageAddLocatorHandlerOptions() { } + + public PageAddLocatorHandlerOptions(PageAddLocatorHandlerOptions clone) + { + if (clone == null) + { + return; + } + + NoWaitAfter = clone.NoWaitAfter; + Times = clone.Times; + } + + /// + /// + /// By default, after calling the handler Playwright will wait until the overlay becomes + /// hidden, and only then Playwright will continue with the action/assertion that triggered + /// the handler. This option allows to opt-out of this behavior, so that overlay can + /// stay visible after the handler has run. + /// + /// + [JsonPropertyName("noWaitAfter")] + public bool? NoWaitAfter { get; set; } + + /// + /// + /// Specifies the maximum number of times this handler should be called. Unlimited by + /// default. + /// + /// + [JsonPropertyName("times")] + public int? Times { get; set; } +} + +#nullable disable diff --git a/src/Playwright/API/Generated/Options/PageAssertionsToHaveURLOptions.cs b/src/Playwright/API/Generated/Options/PageAssertionsToHaveURLOptions.cs index f611815bd..2bc952baa 100644 --- a/src/Playwright/API/Generated/Options/PageAssertionsToHaveURLOptions.cs +++ b/src/Playwright/API/Generated/Options/PageAssertionsToHaveURLOptions.cs @@ -39,9 +39,19 @@ public PageAssertionsToHaveURLOptions(PageAssertionsToHaveURLOptions clone) return; } + IgnoreCase = clone.IgnoreCase; Timeout = clone.Timeout; } + /// + /// + /// Whether to perform case-insensitive match. option + /// takes precedence over the corresponding regular expression flag if specified. + /// + /// + [JsonPropertyName("ignoreCase")] + public bool? IgnoreCase { get; set; } + /// Time to retry the assertion for in milliseconds. Defaults to 5000. [JsonPropertyName("timeout")] public float? Timeout { get; set; } diff --git a/src/Playwright/API/Generated/Options/PageClickOptions.cs b/src/Playwright/API/Generated/Options/PageClickOptions.cs index cbdd59435..1766fb6b4 100644 --- a/src/Playwright/API/Generated/Options/PageClickOptions.cs +++ b/src/Playwright/API/Generated/Options/PageClickOptions.cs @@ -82,7 +82,8 @@ public PageClickOptions(PageClickOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/PageDblClickOptions.cs b/src/Playwright/API/Generated/Options/PageDblClickOptions.cs index ba8790db4..8b900d628 100644 --- a/src/Playwright/API/Generated/Options/PageDblClickOptions.cs +++ b/src/Playwright/API/Generated/Options/PageDblClickOptions.cs @@ -77,7 +77,8 @@ public PageDblClickOptions(PageDblClickOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/PageHoverOptions.cs b/src/Playwright/API/Generated/Options/PageHoverOptions.cs index 9247aa261..7772c7ee0 100644 --- a/src/Playwright/API/Generated/Options/PageHoverOptions.cs +++ b/src/Playwright/API/Generated/Options/PageHoverOptions.cs @@ -62,7 +62,8 @@ public PageHoverOptions(PageHoverOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Generated/Options/PageTapOptions.cs b/src/Playwright/API/Generated/Options/PageTapOptions.cs index 6635d2dd4..51a4b14ab 100644 --- a/src/Playwright/API/Generated/Options/PageTapOptions.cs +++ b/src/Playwright/API/Generated/Options/PageTapOptions.cs @@ -62,7 +62,8 @@ public PageTapOptions(PageTapOptions clone) /// /// Modifier keys to press. Ensures that only these modifiers are pressed during the /// operation, and then restores current modifiers back. If not specified, currently - /// pressed modifiers are used. + /// pressed modifiers are used. "ControlOrMeta" resolves to "Control" on Windows and + /// Linux and to "Meta" on macOS. /// /// [JsonPropertyName("modifiers")] diff --git a/src/Playwright/API/Supplements/IPage.cs b/src/Playwright/API/Supplements/IPage.cs index ea6c5c74e..07b1e29e8 100644 --- a/src/Playwright/API/Supplements/IPage.cs +++ b/src/Playwright/API/Supplements/IPage.cs @@ -84,4 +84,7 @@ public partial interface IPage /// Task UnrouteAsync(Func url, Func handler); + + /// + Task AddLocatorHandlerAsync(ILocator locator, Func handler, PageAddLocatorHandlerOptions? options = default); } diff --git a/src/Playwright/Core/APIRequestContext.cs b/src/Playwright/Core/APIRequestContext.cs index c01e5703e..c27bc1204 100644 --- a/src/Playwright/Core/APIRequestContext.cs +++ b/src/Playwright/Core/APIRequestContext.cs @@ -47,7 +47,11 @@ public APIRequestContext(ChannelOwner parent, string guid, APIRequestContextInit } [MethodImpl(MethodImplOptions.NoInlining)] - public ValueTask DisposeAsync() => new(SendMessageToServerAsync("dispose")); + public async ValueTask DisposeAsync() + { + await SendMessageToServerAsync("dispose").ConfigureAwait(false); + _tracing.ResetStackCounter(); + } [MethodImpl(MethodImplOptions.NoInlining)] public Task FetchAsync(IRequest request, APIRequestContextOptions options = null) diff --git a/src/Playwright/Core/Browser.cs b/src/Playwright/Core/Browser.cs index 5224b00aa..8f7490f46 100644 --- a/src/Playwright/Core/Browser.cs +++ b/src/Playwright/Core/Browser.cs @@ -80,7 +80,7 @@ public async Task CloseAsync(BrowserCloseOptions options = default) { if (ShouldCloseConnectionOnClose) { - _connection.DoClose(); + _connection.DoClose(null as string); } else { diff --git a/src/Playwright/Core/BrowserContext.cs b/src/Playwright/Core/BrowserContext.cs index 2ef040ed9..10f635b65 100644 --- a/src/Playwright/Core/BrowserContext.cs +++ b/src/Playwright/Core/BrowserContext.cs @@ -781,6 +781,7 @@ internal void OnClose() } DisposeHarRouters(); + _tracing.ResetStackCounter(); Close?.Invoke(this, this); _closeTcs.TrySetResult(true); } diff --git a/src/Playwright/Core/BrowserType.cs b/src/Playwright/Core/BrowserType.cs index d78a06eeb..95e7b80ad 100644 --- a/src/Playwright/Core/BrowserType.cs +++ b/src/Playwright/Core/BrowserType.cs @@ -196,9 +196,9 @@ void ClosePipe() connection.MarkAsRemote(); connection.Close += (_, _) => ClosePipe(); - Exception closeError = null; + string closeError = null; Browser browser = null; - void OnPipeClosed() + void OnPipeClosed(string reason = null) { // Emulate all pages, contexts and the browser closing upon disconnect. foreach (BrowserContext context in browser?._contexts.ToArray() ?? Array.Empty()) @@ -210,9 +210,9 @@ void OnPipeClosed() context.OnClose(); } browser?.DidClose(); - connection.DoClose(closeError); + connection.DoClose(reason ?? closeError); } - pipe.Closed += (_, _) => OnPipeClosed(); + pipe.Closed += (_, reason) => OnPipeClosed(reason); connection.OnMessage = async (object message, bool _) => { try @@ -237,7 +237,7 @@ void OnPipeClosed() } catch (Exception ex) { - closeError = ex; + closeError = ex.ToString(); ClosePipe(); } }; diff --git a/src/Playwright/Core/FormData.cs b/src/Playwright/Core/FormData.cs index 5ef28745e..54295caae 100644 --- a/src/Playwright/Core/FormData.cs +++ b/src/Playwright/Core/FormData.cs @@ -30,16 +30,12 @@ namespace Microsoft.Playwright.Core; internal class FormData : IFormData { - public FormData() - { - Values = new Dictionary(); - } - - internal Dictionary Values { get; } + internal List<(string Name, object Value)> Fields { get; } = new(); private FormData SetImpl(string name, object value) { - Values.Add(name, value); + Fields.RemoveAll(f => f.Name == name); + Fields.Add((name, value)); return this; } @@ -54,7 +50,7 @@ private FormData SetImpl(string name, object value) internal IList ToProtocol(bool throwWhenSerializingFilePayloads = false) { var output = new List(); - foreach (var kvp in Values) + foreach (var kvp in Fields) { if (kvp.Value is FilePayload file) { @@ -64,7 +60,7 @@ internal IList ToProtocol(bool throwWhenSerializingFilePayloads = false) } output.Add(new Dictionary() { - ["name"] = kvp.Key, + ["name"] = kvp.Name, ["file"] = new Dictionary() { ["name"] = file.Name, @@ -75,9 +71,23 @@ internal IList ToProtocol(bool throwWhenSerializingFilePayloads = false) } else { - output.Add(new NameValue() { Name = kvp.Key, Value = kvp.Value.ToString() }); + output.Add(new NameValue() { Name = kvp.Name, Value = kvp.Value.ToString() }); } } return output; } + + public IFormData Append(string name, string value) => AppendImpl(name, value); + + public IFormData Append(string name, bool value) => AppendImpl(name, value); + + public IFormData Append(string name, int value) => AppendImpl(name, value); + + public IFormData Append(string name, FilePayload value) => AppendImpl(name, value); + + private FormData AppendImpl(string name, object value) + { + Fields.Add((name, value)); + return this; + } } diff --git a/src/Playwright/Core/JsonPipe.cs b/src/Playwright/Core/JsonPipe.cs index 5c9018148..c6264d047 100644 --- a/src/Playwright/Core/JsonPipe.cs +++ b/src/Playwright/Core/JsonPipe.cs @@ -44,21 +44,15 @@ public JsonPipe(ChannelOwner parent, string guid, JsonPipeInitializer initialize public event EventHandler Message; - public event EventHandler Closed; + public event EventHandler Closed; internal override void OnMessage(string method, JsonElement? serverParams) { switch (method) { case "closed": - if (serverParams.Value.TryGetProperty("error", out var error)) - { - Closed?.Invoke(this, error.ToObject(_connection.DefaultJsonSerializerOptions)); - } - else - { - Closed?.Invoke(this, null); - } + var reason = serverParams?.TryGetProperty("reason", out var r) == true ? r.GetString() : null; + Closed?.Invoke(this, reason); break; case "message": Message?.Invoke(this, serverParams?.GetProperty("message").ToObject(_connection.DefaultJsonSerializerOptions)); diff --git a/src/Playwright/Core/Locator.cs b/src/Playwright/Core/Locator.cs index 0141dc1e0..8f2d14993 100644 --- a/src/Playwright/Core/Locator.cs +++ b/src/Playwright/Core/Locator.cs @@ -109,6 +109,8 @@ public Locator(Frame parent, string selector, LocatorLocatorOptions options = nu public IFrameLocator ContentFrame => new FrameLocator(_frame, _selector); + internal bool EqualLocator(Locator locator) => _frame == locator._frame && _selector == locator._selector; + public Task BoundingBoxAsync(LocatorBoundingBoxOptions options = null) => WithElementAsync( async (h, _) => diff --git a/src/Playwright/Core/LocatorAssertions.cs b/src/Playwright/Core/LocatorAssertions.cs index 74c5a4fe1..39a11ab50 100644 --- a/src/Playwright/Core/LocatorAssertions.cs +++ b/src/Playwright/Core/LocatorAssertions.cs @@ -201,4 +201,19 @@ public Task ToHaveValuesAsync(IEnumerable values, LocatorAssertionsToHav public Task ToHaveValuesAsync(IEnumerable values, LocatorAssertionsToHaveValuesOptions options = null) => ExpectImplAsync("to.have.values", values.Select(regex => ExpectedRegex(regex)).ToArray(), values, "Locator expected to have matching regex", ConvertToFrameExpectOptions(options)); + + public Task ToHaveAccessibleDescriptionAsync(string expected, LocatorAssertionsToHaveAccessibleDescriptionOptions options = null) + => ExpectImplAsync("to.have.accessible.description", new ExpectedTextValue() { String = expected, IgnoreCase = options?.IgnoreCase }, expected, "Locator expected to have accessible description", ConvertToFrameExpectOptions(options)); + + public Task ToHaveAccessibleDescriptionAsync(Regex expected, LocatorAssertionsToHaveAccessibleDescriptionOptions options = null) + => ExpectImplAsync("to.have.accessible.description", ExpectedRegex(expected, new() { IgnoreCase = options?.IgnoreCase }), expected, "Locator expected to have accessible description matching regex", ConvertToFrameExpectOptions(options)); + + public Task ToHaveAccessibleNameAsync(string expected, LocatorAssertionsToHaveAccessibleNameOptions options = null) + => ExpectImplAsync("to.have.accessible.name", new ExpectedTextValue() { String = expected, IgnoreCase = options?.IgnoreCase }, expected, "Locator expected to have accessible name", ConvertToFrameExpectOptions(options)); + + public Task ToHaveAccessibleNameAsync(Regex expected, LocatorAssertionsToHaveAccessibleNameOptions options = null) + => ExpectImplAsync("to.have.accessible.name", ExpectedRegex(expected, new() { IgnoreCase = options?.IgnoreCase }), expected, "Locator expected to have accessible name matching regex", ConvertToFrameExpectOptions(options)); + + public Task ToHaveRoleAsync(AriaRole role, LocatorAssertionsToHaveRoleOptions options = null) + => ExpectImplAsync("to.have.role", new ExpectedTextValue() { String = role.ToString().ToLowerInvariant() }, role, "Locator expected to have role", ConvertToFrameExpectOptions(options)); } diff --git a/src/Playwright/Core/Page.cs b/src/Playwright/Core/Page.cs index dfd266c8e..ef6666223 100644 --- a/src/Playwright/Core/Page.cs +++ b/src/Playwright/Core/Page.cs @@ -46,7 +46,8 @@ internal class Page : ChannelOwner, IPage internal readonly List _workers = new(); internal readonly TimeoutSettings _timeoutSettings; private readonly List _harRouters = new(); - private readonly Dictionary> _locatorHandlers = new(); + // Func Handler + private readonly Dictionary _locatorHandlers = new(); private List _routes = new(); private Video _video; private string _closeReason; @@ -1476,32 +1477,112 @@ public ILocator GetByTitle(string text, PageGetByTitleOptions options = null) public ILocator GetByTitle(Regex text, PageGetByTitleOptions options = null) => MainFrame.GetByTitle(text, new() { Exact = options?.Exact }); - public async Task AddLocatorHandlerAsync(ILocator locator, Func handler) + public Task AddLocatorHandlerAsync(ILocator locator, Func handler, PageAddLocatorHandlerOptions options = null) + => AddLocatorHandlerImplAsync(locator, handler, options); + + public Task AddLocatorHandlerAsync(ILocator locator, Func handler, PageAddLocatorHandlerOptions options = null) + => AddLocatorHandlerImplAsync(locator, handler, options); + + private async Task AddLocatorHandlerImplAsync(ILocator locator, object handler, PageAddLocatorHandlerOptions options = null) { if ((locator as Locator)._frame != MainFrame) { throw new PlaywrightException("Locator must belong to the main frame of this page"); } + if (options?.Times == 0) + { + return; + } var response = await SendMessageToServerAsync("registerLocatorHandler", new Dictionary { ["selector"] = (locator as Locator)._selector, + ["noWaitAfter"] = options?.NoWaitAfter, }).ConfigureAwait(false); - _locatorHandlers.Add(response.Value.GetProperty("uid").GetInt32(), handler); + _locatorHandlers.Add(response.Value.GetProperty("uid").GetInt32(), new LocatorHandler((Locator)locator, handler, options?.Times)); } private async Task Channel_LocatorHandlerTriggeredAsync(int uid) { + var remove = false; try { - await _locatorHandlers[uid]().ConfigureAwait(false); + if (_locatorHandlers.TryGetValue(uid, out var handler)) + { + if (handler.Times != 0) + { + if (handler.Times != null) + { + handler.Times--; + } + await handler.HandleAsync().ConfigureAwait(false); + } + remove = handler.Times == 0; + } } finally { + if (remove) + { + _locatorHandlers.Remove(uid); + } SendMessageToServerAsync("resolveLocatorHandlerNoReply", new Dictionary { ["uid"] = uid, + ["remove"] = remove, }).IgnoreException(); } } + + public async Task RemoveLocatorHandlerAsync(ILocator locator) + { + foreach (KeyValuePair entry in _locatorHandlers) + { + var (uid, data) = (entry.Key, entry.Value); + if (data.Locator.EqualLocator(locator as Locator)) + { + _locatorHandlers.Remove(uid); + try + { + await SendMessageToServerAsync("unregisterLocatorHandler", new Dictionary + { + ["uid"] = uid, + }).ConfigureAwait(false); + } + catch (System.Exception) + { + // Ignore + } + } + } + } +} + +internal class LocatorHandler +{ + internal LocatorHandler(Locator locator, object handler, int? times) + { + Locator = locator; + Handler = handler; + Times = times; + } + + internal Locator Locator { get; } + + private object Handler { get; } + + internal int? Times { get; set; } + + internal Task HandleAsync() + { + if (Handler is Func funcTask) + { + return funcTask(); + } + if (Handler is Func funcLocatorTask) + { + return funcLocatorTask(Locator); + } + throw new PlaywrightException("Locator handler must be a Func or Func"); + } } diff --git a/src/Playwright/Core/PageAssertions.cs b/src/Playwright/Core/PageAssertions.cs index 2eabef6c5..e4ea6d25d 100644 --- a/src/Playwright/Core/PageAssertions.cs +++ b/src/Playwright/Core/PageAssertions.cs @@ -57,8 +57,8 @@ public Task ToHaveTitleAsync(Regex titleOrRegExp, PageAssertionsToHaveTitleOptio ExpectImplAsync("to.have.title", ExpectedRegex(titleOrRegExp, new() { NormalizeWhiteSpace = true }), titleOrRegExp, "Page title expected to be", ConvertToFrameExpectOptions(options)); public Task ToHaveURLAsync(string urlOrRegExp, PageAssertionsToHaveURLOptions options = null) => - ExpectImplAsync("to.have.url", new ExpectedTextValue() { String = _page.Context.CombineUrlWithBase(urlOrRegExp) }, urlOrRegExp, "Page URL expected to be", ConvertToFrameExpectOptions(options)); + ExpectImplAsync("to.have.url", new ExpectedTextValue() { String = _page.Context.CombineUrlWithBase(urlOrRegExp), IgnoreCase = options?.IgnoreCase }, urlOrRegExp, "Page URL expected to be", ConvertToFrameExpectOptions(options)); public Task ToHaveURLAsync(Regex urlOrRegExp, PageAssertionsToHaveURLOptions options = null) => - ExpectImplAsync("to.have.url", ExpectedRegex(urlOrRegExp), urlOrRegExp, "Page URL expected to match regex", ConvertToFrameExpectOptions(options)); + ExpectImplAsync("to.have.url", ExpectedRegex(urlOrRegExp, new() { IgnoreCase = options?.IgnoreCase }), urlOrRegExp, "Page URL expected to match regex", ConvertToFrameExpectOptions(options)); } diff --git a/src/Playwright/Core/Tracing.cs b/src/Playwright/Core/Tracing.cs index e2b36c61e..ff34ccd2f 100644 --- a/src/Playwright/Core/Tracing.cs +++ b/src/Playwright/Core/Tracing.cs @@ -109,11 +109,7 @@ await _connection.WrapApiCallAsync( private async Task DoStopChunkAsync(string filePath) { - if (_isTracing) - { - _isTracing = false; - _connection.SetIsTracing(false); - } + ResetStackCounter(); if (string.IsNullOrEmpty(filePath)) { @@ -172,4 +168,13 @@ private async Task DoStopChunkAsync(string filePath) } return (artifact, entries); } + + internal void ResetStackCounter() + { + if (_isTracing) + { + _isTracing = false; + _connection.SetIsTracing(false); + } + } } diff --git a/src/Playwright/Transport/Connection.cs b/src/Playwright/Transport/Connection.cs index 3247b528e..af1d1a6f4 100644 --- a/src/Playwright/Transport/Connection.cs +++ b/src/Playwright/Transport/Connection.cs @@ -413,11 +413,17 @@ private ChannelOwner CreateRemoteObject(string parentGuid, ChannelOwnerType type } internal void DoClose(Exception cause = null) + => DoCloseImpl(cause != null ? new TargetClosedException(cause.Message, cause) : new TargetClosedException()); + + internal void DoClose(string cause = null) + => DoCloseImpl(!string.IsNullOrEmpty(cause) ? new TargetClosedException(cause) : new TargetClosedException()); + + internal void DoCloseImpl(Exception closeError = null) { - _closedError = cause != null ? new TargetClosedException(cause.Message, cause) : new TargetClosedException(); + this._closedError = closeError; foreach (var callback in _callbacks) { - callback.Value.TaskCompletionSource.TrySetException(_closedError); + callback.Value.TaskCompletionSource.TrySetException(closeError.InnerException ?? closeError); // We need to make sure that the task is handled otherwise it will be reported as unhandled on the caller side. // Its still possible to get the exception from the task. callback.Value.TaskCompletionSource.Task.IgnoreException();