From 7993c20dfe2cce77b9087a03fb0858d385d34ef6 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 15 Sep 2026 10:49:35 +0100 Subject: [PATCH 1/2] chore(driver): roll to 1.63.0 --- README.md | 6 +- src/Common/Version.props | 4 +- .../package-lock.json | 72 ++-- .../package.json | 2 +- .../Assertions/LocatorAssertionsTests.cs | 22 +- .../BrowserContextEventsTests.cs | 15 + .../BrowserContextFetchTests.cs | 19 + .../BrowserContextHttpCredentialsTests.cs | 77 ++++ .../BrowserContextStorageStateTests.cs | 81 ++++ .../BrowserTypeConnectTests.cs | 4 +- src/Playwright.Tests/GlobalFetchTests.cs | 51 +++ src/Playwright.Tests/HARTests.cs | 16 + .../Locator/LocatorAnyFrameTests.cs | 389 ++++++++++++++++++ .../Locator/LocatorMisc2Tests.cs | 9 + src/Playwright.Tests/PageAriaSnapshotTests.cs | 2 +- src/Playwright.Tests/PageDialogTests.cs | 65 +++ src/Playwright.Tests/TracingTests.cs | 85 +++- .../API/Generated/IAPIRequestContext.cs | 1 + .../API/Generated/IBrowserContext.cs | 21 +- src/Playwright/API/Generated/IFrame.cs | 22 +- src/Playwright/API/Generated/IFrameLocator.cs | 23 ++ src/Playwright/API/Generated/ILocator.cs | 23 +- src/Playwright/API/Generated/IPage.cs | 35 +- src/Playwright/API/Generated/ITracing.cs | 2 +- .../APIRequestContextStorageStateOptions.cs | 10 + .../Options/APIRequestNewContextOptions.cs | 19 + .../BrowserContextStorageStateOptions.cs | 12 + .../Options/BrowserNewContextOptions.cs | 19 + .../Options/BrowserNewPageOptions.cs | 19 + ...owserTypeLaunchPersistentContextOptions.cs | 19 + .../Generated/Options/LocatorFilterOptions.cs | 7 +- .../Generated/Options/TracingStartOptions.cs | 18 +- src/Playwright/Core/APIRequest.cs | 2 +- src/Playwright/Core/APIRequestContext.cs | 8 +- src/Playwright/Core/Browser.cs | 11 +- src/Playwright/Core/BrowserContext.cs | 37 +- src/Playwright/Core/BrowserType.cs | 2 +- src/Playwright/Core/Frame.cs | 4 +- src/Playwright/Core/FrameLocator.cs | 34 +- src/Playwright/Core/Locator.cs | 2 + src/Playwright/Core/Page.cs | 12 +- src/Playwright/Core/PageEvent.cs | 2 + src/Playwright/Core/Tracing.cs | 78 +++- .../Protocol/Generated/HttpCredentials.cs | 42 ++ .../Transport/Protocol/Generated/OPFSEntry.cs | 39 ++ 45 files changed, 1310 insertions(+), 132 deletions(-) create mode 100644 src/Playwright.Tests/Locator/LocatorAnyFrameTests.cs create mode 100644 src/Playwright/Transport/Protocol/Generated/HttpCredentials.cs create mode 100644 src/Playwright/Transport/Protocol/Generated/OPFSEntry.cs diff --git a/README.md b/README.md index 866a13a26e..d4a2e1f8a7 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 151.0.7922.34 | ✅ | ✅ | ✅ | -| WebKit 26.5 | ✅ | ✅ | ✅ | -| Firefox 153.0 | ✅ | ✅ | ✅ | +| Chromium 153.0.8010.12 | ✅ | ✅ | ✅ | +| WebKit 26.6 | ✅ | ✅ | ✅ | +| Firefox 155.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/Version.props b/src/Common/Version.props index 09cdba1484..879135cc71 100644 --- a/src/Common/Version.props +++ b/src/Common/Version.props @@ -2,10 +2,10 @@ 1.62.0 $(AssemblyVersion) - 1.62.0 + 1.63.0 - 24.18.1 + 24.21.0 $(AssemblyVersion) $(AssemblyVersion) true diff --git a/src/Playwright.TestingHarnessTest/package-lock.json b/src/Playwright.TestingHarnessTest/package-lock.json index 1cbdd580fa..0e06f5c64e 100644 --- a/src/Playwright.TestingHarnessTest/package-lock.json +++ b/src/Playwright.TestingHarnessTest/package-lock.json @@ -7,19 +7,19 @@ "": { "name": "playwright.testingharnesstest", "devDependencies": { - "@playwright/test": "1.62.0", + "@playwright/test": "1.63.0", "@types/node": "^22.12.0", "fast-xml-parser": "^4.5.0" } }, "node_modules/@playwright/test": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz", - "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.62.0" + "playwright": "1.63.0" }, "bin": { "playwright": "cli.js" @@ -61,44 +61,26 @@ "fxparser": "src/cli/cli.js" } }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/playwright": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", - "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.62.0" + "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" }, "engines": { "node": ">=20" - }, - "optionalDependencies": { - "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", - "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -124,12 +106,12 @@ }, "dependencies": { "@playwright/test": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0.tgz", - "integrity": "sha512-9zOJ6ZQRAena31MpOH9VSzIz8Ou3YJ/wtY/eQm5T2uhfhG7/U3COrMS8xOtUrZrp9OgdmzEnIYODye3nY1VqzA==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", "dev": true, "requires": { - "playwright": "1.62.0" + "playwright": "1.63.0" } }, "@types/node": { @@ -150,27 +132,19 @@ "strnum": "^1.0.5" } }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, "playwright": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", - "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", "dev": true, "requires": { - "fsevents": "2.3.2", - "playwright-core": "1.62.0" + "playwright-core": "1.63.0" } }, "playwright-core": { - "version": "1.62.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", - "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", "dev": true }, "strnum": { diff --git a/src/Playwright.TestingHarnessTest/package.json b/src/Playwright.TestingHarnessTest/package.json index 096b942204..34e520991b 100644 --- a/src/Playwright.TestingHarnessTest/package.json +++ b/src/Playwright.TestingHarnessTest/package.json @@ -2,7 +2,7 @@ "name": "playwright.testingharnesstest", "private": true, "devDependencies": { - "@playwright/test": "1.62.0", + "@playwright/test": "1.63.0", "@types/node": "^22.12.0", "fast-xml-parser": "^4.5.0" } diff --git a/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs b/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs index b84b58df69..15f0ed2b6e 100644 --- a/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs +++ b/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs @@ -40,11 +40,11 @@ public async Task ShouldSupportToBeChecked() var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(Page.Locator("input")).ToBeCheckedAsync(new() { Checked = false, Timeout = 300 })); StringAssert.Contains("Locator expected not to be checked", exception.Message); - StringAssert.Contains("Expect \"ToBeCheckedAsync\" with timeout 300ms", exception.Message); + StringAssert.Contains("Expect \"ToBeCheckedAsync\" Locator(\"input\") with timeout 300ms", exception.Message); exception = await PlaywrightAssert.ThrowsAsync(() => Expect(Page.Locator("input")).Not.ToBeCheckedAsync(new() { Timeout = 300 })); StringAssert.Contains("Locator expected not to be checked", exception.Message); - StringAssert.Contains("Expect \"ToBeCheckedAsync\" with timeout 300ms", exception.Message); + StringAssert.Contains("Expect \"ToBeCheckedAsync\" Locator(\"input\") with timeout 300ms", exception.Message); } [PlaywrightTest("tests/page/expect-boolean.spec.ts", "with indeterminate:true")] @@ -70,7 +70,7 @@ public async Task FailWithIndeterminateTrue() await Page.SetContentAsync(""); var locator = Page.Locator("input"); var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(locator).ToBeCheckedAsync(new() { Indeterminate = true, Timeout = 1000 })); - StringAssert.Contains("Expect \"ToBeCheckedAsync\" with timeout 1000ms", exception.Message); + StringAssert.Contains("Expect \"ToBeCheckedAsync\" Locator(\"input\") with timeout 1000ms", exception.Message); } [PlaywrightTest("playwright-test/playwright.expect.spec.ts", "should support a custom expect message")] @@ -99,7 +99,7 @@ public async Task ShouldBeAbleToSetDefaultTimeout() SetDefaultExpectTimeout(1111); var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(Page.Locator("input")).Not.ToBeCheckedAsync()); StringAssert.Contains("Locator expected not to be checked", exception.Message); - StringAssert.Contains("Expect \"ToBeCheckedAsync\" with timeout 1111ms", exception.Message); + StringAssert.Contains("Expect \"ToBeCheckedAsync\" Locator(\"input\") with timeout 1111ms", exception.Message); } finally { @@ -296,7 +296,7 @@ public async Task ShouldSupportToBeEnabled() StringAssert.Contains("locator resolved to ", exception.Message); // extra checks StringAssert.Contains("Locator expected to be enabled", exception.Message); - StringAssert.Contains("Expect \"ToBeEnabledAsync\" with timeout 1000ms", exception.Message); + StringAssert.Contains("Expect \"ToBeEnabledAsync\" Locator(\"button\") with timeout 1000ms", exception.Message); } { // eventually @@ -420,14 +420,14 @@ public async Task ShouldSupportToBeVisibleToBeHiddenFail() var locator = Page.Locator("button"); var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(locator).ToBeVisibleAsync(new() { Timeout = 500 })); StringAssert.Contains("Locator expected to be visible", exception.Message); - StringAssert.Contains("Expect \"ToBeVisibleAsync\" with timeout 500ms", exception.Message); + StringAssert.Contains("Expect \"ToBeVisibleAsync\" Locator(\"button\") with timeout 500ms", exception.Message); } { await Page.SetContentAsync(""); var locator = Page.Locator("input"); var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(locator).Not.ToBeVisibleAsync(new() { Timeout = 500 })); StringAssert.Contains("Locator expected not to be visible", exception.Message); - StringAssert.Contains("Expect \"ToBeVisibleAsync\" with timeout 500ms", exception.Message); + StringAssert.Contains("Expect \"ToBeVisibleAsync\" Locator(\"input\") with timeout 500ms", exception.Message); } } @@ -456,7 +456,7 @@ public async Task ShouldSupportToContainText() var exeption = await PlaywrightAssert.ThrowsAsync(() => Expect(Page.Locator("#node")).ToContainTextAsync(new Regex("ex2"), new() { Timeout = 100 })); StringAssert.Contains("Locator expected text matching regex 'ex2'", exeption.Message); StringAssert.Contains("But was: 'Text content'", exeption.Message); - StringAssert.Contains("Expect \"ToContainTextAsync\" with timeout 100ms", exeption.Message); + StringAssert.Contains("Expect \"ToContainTextAsync\" Locator(\"#node\") with timeout 100ms", exeption.Message); } { await Page.SetContentAsync("
Text \ncontent 
"); @@ -490,7 +490,7 @@ public async Task ShouldSupportToContainText() var exeption = await PlaywrightAssert.ThrowsAsync(() => Expect(locator).ToHaveTextAsync("Text", new() { Timeout = 100 })); StringAssert.Contains("Locator expected to have text 'Text'", exeption.Message); StringAssert.Contains("But was: 'Text content'", exeption.Message); - StringAssert.Contains("Expect \"ToHaveTextAsync\" with timeout 100ms", exeption.Message); + StringAssert.Contains("Expect \"ToHaveTextAsync\" Locator(\"#node\") with timeout 100ms", exeption.Message); } } @@ -561,7 +561,7 @@ public async Task ShouldSupportToHaveClass() var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(locator).ToHaveClassAsync("kektus", new() { Timeout = 300 })); StringAssert.Contains("Locator expected to have class 'kektus'", exception.Message); StringAssert.Contains("But was: 'foo bar baz'", exception.Message); - StringAssert.Contains("Expect \"ToHaveClassAsync\" with timeout 300ms", exception.Message); + StringAssert.Contains("Expect \"ToHaveClassAsync\" Locator(\"div\") with timeout 300ms", exception.Message); } { await Page.SetContentAsync("
"); @@ -585,7 +585,7 @@ public async Task ShouldSupportToContainClass() var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(locator).ToContainClassAsync("does-not-exist", new() { Timeout = 300 })); StringAssert.Contains("Locator expected to contain class names 'does-not-exist'", exception.Message); StringAssert.Contains("But was: 'foo bar baz'", exception.Message); - StringAssert.Contains("Expect \"ToContainClassAsync\" with timeout 300ms", exception.Message); + StringAssert.Contains("Expect \"ToContainClassAsync\" Locator(\"div\") with timeout 300ms", exception.Message); } { await Page.SetContentAsync("
"); diff --git a/src/Playwright.Tests/BrowserContextEventsTests.cs b/src/Playwright.Tests/BrowserContextEventsTests.cs index 6dca3da48a..3975b42949 100644 --- a/src/Playwright.Tests/BrowserContextEventsTests.cs +++ b/src/Playwright.Tests/BrowserContextEventsTests.cs @@ -105,6 +105,21 @@ public async Task DialogEventShouldWork() Assert.AreEqual("hello", await task); } + [PlaywrightTest("browsercontext-events.spec.ts", "dialogclosed event should work")] + public async Task DialogClosedEventShouldWork() + { + var task = Page.EvaluateAsync("() => prompt('hey?')"); + var dialog = await WaitForContextDialog(Page.Context); + var contextClosedTcs = new TaskCompletionSource(); + Page.Context.DialogClosed += (_, e) => contextClosedTcs.TrySetResult(e); + var pageClosedTcs = new TaskCompletionSource(); + Page.DialogClosed += (_, e) => pageClosedTcs.TrySetResult(e); + await dialog.AcceptAsync("hello"); + Assert.AreEqual(dialog, await contextClosedTcs.Task); + Assert.AreEqual(dialog, await pageClosedTcs.Task); + Assert.AreEqual("hello", await task); + } + [PlaywrightTest("browsercontext-events.spec.ts", "dialog event should work in popup")] public async Task DialogEventShouldWorkInPopup() { diff --git a/src/Playwright.Tests/BrowserContextFetchTests.cs b/src/Playwright.Tests/BrowserContextFetchTests.cs index 881275332f..b0466ed555 100644 --- a/src/Playwright.Tests/BrowserContextFetchTests.cs +++ b/src/Playwright.Tests/BrowserContextFetchTests.cs @@ -324,6 +324,25 @@ public async Task ShouldWorkWithHttpCredentials() Assert.AreEqual("/empty.html", requestURL); } + [PlaywrightTest("browsercontext-fetch.spec.ts", "should support multiple httpCredentials")] + public async Task ShouldSupportMultipleHttpCredentials() + { + Server.SetAuth("/empty.html", "user1", "pass1"); + await using var context = await Browser.NewContextAsync(new() + { + HttpCredentialsList = new[] + { + new HttpCredentials { Username = "user1", Password = "pass1", Origin = Server.Prefix }, + new HttpCredentials { Username = "user2", Password = "pass2", Origin = Server.CrossProcessPrefix }, + }, + }); + var response1 = await context.APIRequest.GetAsync(Server.EmptyPage); + Assert.AreEqual(200, response1.Status); + // Wrong credentials are picked for the other origin. + var response2 = await context.APIRequest.GetAsync(Server.CrossProcessPrefix + "/empty.html"); + Assert.AreEqual(401, response2.Status); + } + [PlaywrightTest("browsercontext-fetch.spec.ts", "should support HTTPCredentials.send")] public async Task ShouldSupportHttpCredentialsSend() { diff --git a/src/Playwright.Tests/BrowserContextHttpCredentialsTests.cs b/src/Playwright.Tests/BrowserContextHttpCredentialsTests.cs index d1f0ae5cc4..3a9160c56b 100644 --- a/src/Playwright.Tests/BrowserContextHttpCredentialsTests.cs +++ b/src/Playwright.Tests/BrowserContextHttpCredentialsTests.cs @@ -97,6 +97,83 @@ public async Task ShouldReturnResourceBody() StringAssert.Contains("Playground", await response.TextAsync()); } + [PlaywrightTest("browsercontext-credentials.spec.ts", "should work with a single credential in an array")] + public async Task ShouldWorkWithASingleCredentialInAnArray() + { + Server.SetAuth("/empty.html", "user", "pass"); + await using var context = await Browser.NewContextAsync(new() + { + HttpCredentialsList = new[] + { + new HttpCredentials { Username = "user", Password = "pass" }, + }, + }); + + var page = await context.NewPageAsync(); + var response = await page.GotoAsync(Server.EmptyPage); + Assert.AreEqual((int)HttpStatusCode.OK, response.Status); + } + + [PlaywrightTest("browsercontext-credentials.spec.ts", "should work with multiple credentials for different origins")] + public async Task ShouldWorkWithMultipleCredentialsForDifferentOrigins() + { + Server.SetAuth("/empty.html", "user1", "pass1"); + await using var context = await Browser.NewContextAsync(new() + { + HttpCredentialsList = new[] + { + new HttpCredentials { Username = "user1", Password = "pass1", Origin = Server.Prefix }, + new HttpCredentials { Username = "user2", Password = "pass2", Origin = Server.CrossProcessPrefix }, + }, + }); + + var page = await context.NewPageAsync(); + var response1 = await page.GotoAsync(Server.EmptyPage); + Assert.AreEqual((int)HttpStatusCode.OK, response1.Status); + // Wrong credentials are picked for the other origin. + var response2 = await page.GotoAsync(Server.CrossProcessPrefix + "/empty.html"); + Assert.AreEqual((int)HttpStatusCode.Unauthorized, response2.Status); + } + + [PlaywrightTest("browsercontext-credentials.spec.ts", "should fall back to credentials without origin")] + public async Task ShouldFallBackToCredentialsWithoutOrigin() + { + Server.SetAuth("/empty.html", "user", "pass"); + await using var context = await Browser.NewContextAsync(new() + { + HttpCredentialsList = new[] + { + new HttpCredentials { Username = "user2", Password = "pass2", Origin = Server.CrossProcessPrefix }, + new HttpCredentials { Username = "user", Password = "pass" }, + }, + }); + + var page = await context.NewPageAsync(); + var response1 = await page.GotoAsync(Server.EmptyPage); + Assert.AreEqual((int)HttpStatusCode.OK, response1.Status); + // First matching entry has wrong credentials for this origin. + var response2 = await page.GotoAsync(Server.CrossProcessPrefix + "/empty.html"); + Assert.AreEqual((int)HttpStatusCode.Unauthorized, response2.Status); + } + + [PlaywrightTest("browsercontext-credentials.spec.ts", "should use the first matching credential")] + public async Task ShouldUseTheFirstMatchingCredential() + { + Server.SetAuth("/empty.html", "user", "pass"); + await using var context = await Browser.NewContextAsync(new() + { + HttpCredentialsList = new[] + { + new HttpCredentials { Username = "wrong", Password = "wrong" }, + new HttpCredentials { Username = "user", Password = "pass", Origin = Server.Prefix }, + }, + }); + + var page = await context.NewPageAsync(); + var response = await page.GotoAsync(Server.EmptyPage); + Assert.AreEqual((int)HttpStatusCode.Unauthorized, response.Status); + } + [PlaywrightTest("browsercontext-credentials.spec.ts", "should work with correct credentials and matching origin")] public async Task ShouldWorkWithCorrectCredentialsAndMatchingOrigin() { diff --git a/src/Playwright.Tests/BrowserContextStorageStateTests.cs b/src/Playwright.Tests/BrowserContextStorageStateTests.cs index c32f33aa58..eb3edd7653 100644 --- a/src/Playwright.Tests/BrowserContextStorageStateTests.cs +++ b/src/Playwright.Tests/BrowserContextStorageStateTests.cs @@ -22,6 +22,8 @@ * SOFTWARE. */ +using System.Text.Json; + namespace Microsoft.Playwright.Tests; public sealed class BrowserContextStorageStateTests : PageTestEx @@ -136,6 +138,85 @@ await page2.RouteAsync("**/*", (route) => Assert.AreEqual("foo", idbValue); } + [PlaywrightTest("browsercontext-storage-state.spec.ts", "should round-trip OPFS")] + [Skip(SkipAttribute.Targets.Webkit)] // OPFS is unavailable in non-persistent WebKit contexts + public async Task ShouldRoundTripOPFS() + { + await Page.GotoAsync(Server.EmptyPage); + await Page.EvaluateAsync(@"async () => { + const root = await navigator.storage.getDirectory(); + const nested = await root.getDirectoryHandle('nested', { create: true }); + await nested.getDirectoryHandle('empty', { create: true }); + + const binary = await nested.getFileHandle('data.bin', { create: true }); + const binaryWritable = await binary.createWritable(); + await binaryWritable.write(new Uint8Array([0, 1, 2, 255])); + await binaryWritable.close(); + + const text = await root.getFileHandle('hello.txt', { create: true }); + const textWritable = await text.createWritable(); + await textWritable.write('Hello, world!'); + await textWritable.close(); + }"); + + Assert.AreEqual("{\"cookies\":[],\"origins\":[]}", await Context.StorageStateAsync()); + + using var tempDir = new TempDirectory(); + string path = Path.Combine(tempDir.Path, "storage-state.json"); + string storageState = await Context.StorageStateAsync(new() { Opfs = true, Path = path }); + var origins = JsonDocument.Parse(storageState).RootElement.GetProperty("origins"); + Assert.AreEqual(1, origins.GetArrayLength()); + Assert.AreEqual(Server.Prefix, origins[0].GetProperty("origin").GetString()); + var opfs = origins[0].GetProperty("opfs").EnumerateArray().Select(entry => (entry.GetProperty("path").GetString(), entry.GetProperty("type").GetString(), entry.TryGetProperty("base64", out var base64) ? base64.GetString() : null)).ToArray(); + CollectionAssert.AreEqual(new[] + { + ("hello.txt", "file", "SGVsbG8sIHdvcmxkIQ=="), + ("nested", "directory", null), + ("nested/data.bin", "file", "AAEC/w=="), + ("nested/empty", "directory", null), + }, opfs); + Assert.AreEqual(storageState, File.ReadAllText(path)); + Assert.AreEqual(storageState, await Context.APIRequest.StorageStateAsync(new() { Opfs = true })); + + async Task CheckContext(IBrowserContext context) + { + Assert.AreEqual(storageState, await context.StorageStateAsync(new() { Opfs = true })); + var checkPage = await context.NewPageAsync(); + await checkPage.GotoAsync(Server.EmptyPage); + var result = await checkPage.EvaluateAsync(@"async () => { + const root = await navigator.storage.getDirectory(); + const hello = await (await root.getFileHandle('hello.txt')).getFile(); + const nested = await root.getDirectoryHandle('nested'); + const data = await (await nested.getFileHandle('data.bin')).getFile(); + const empty = await nested.getDirectoryHandle('empty'); + const emptyEntries = []; + for await (const name of empty.keys()) + emptyEntries.push(name); + return { + text: await hello.text(), + bytes: [...new Uint8Array(await data.arrayBuffer())], + empty: emptyEntries, + }; + }"); + Assert.AreEqual("Hello, world!", result.GetProperty("text").GetString()); + CollectionAssert.AreEqual(new[] { 0, 1, 2, 255 }, result.GetProperty("bytes").EnumerateArray().Select(e => e.GetInt32()).ToArray()); + Assert.AreEqual(0, result.GetProperty("empty").GetArrayLength()); + } + + await using var context2 = await Browser.NewContextAsync(new() { StorageStatePath = path }); + await CheckContext(context2); + + await using var context3 = await Browser.NewContextAsync(); + var page3 = await context3.NewPageAsync(); + await page3.GotoAsync(Server.EmptyPage); + await page3.EvaluateAsync(@"async () => { + const root = await navigator.storage.getDirectory(); + await root.getFileHandle('stale.txt', { create: true }); + }"); + await context3.SetStorageStateAsync(path); + await CheckContext(context3); + } + [PlaywrightTest("browsercontext-storage-state.spec.ts", "should capture cookies")] public async Task ShouldCaptureCookies() { diff --git a/src/Playwright.Tests/BrowserTypeConnectTests.cs b/src/Playwright.Tests/BrowserTypeConnectTests.cs index 466857b507..acdd435b2d 100644 --- a/src/Playwright.Tests/BrowserTypeConnectTests.cs +++ b/src/Playwright.Tests/BrowserTypeConnectTests.cs @@ -417,7 +417,7 @@ public async Task ShouldRecordContextTraces() ZipFile.ExtractToDirectory(tracePath, tempDirectory.Path); Assert.That(tempDirectory.Path + "/trace.trace", Does.Exist); Assert.That(tempDirectory.Path + "/trace.network", Does.Exist); - Assert.AreEqual(1, Directory.GetFiles(Path.Join(tempDirectory.Path, "resources"), "*.txt").Length); + Assert.AreEqual(1, Directory.GetFiles(Path.Join(tempDirectory.Path, "src"), "*.cs").Length); } [PlaywrightTest("browsertype-connect.spec.ts", "should record trace with no directory name")] @@ -443,7 +443,7 @@ public async Task ShouldRecordContextTraceWithNoDirectoryName() ZipFile.ExtractToDirectory(tracePath, tempDirectory.Path); Assert.That(tempDirectory.Path + "/trace.trace", Does.Exist); Assert.That(tempDirectory.Path + "/trace.network", Does.Exist); - Assert.AreEqual(1, Directory.GetFiles(Path.Join(tempDirectory.Path, "resources"), "*.txt").Length); + Assert.AreEqual(1, Directory.GetFiles(Path.Join(tempDirectory.Path, "src"), "*.cs").Length); } finally { diff --git a/src/Playwright.Tests/GlobalFetchTests.cs b/src/Playwright.Tests/GlobalFetchTests.cs index d2b45b4ef2..38e2dff3d6 100644 --- a/src/Playwright.Tests/GlobalFetchTests.cs +++ b/src/Playwright.Tests/GlobalFetchTests.cs @@ -216,6 +216,57 @@ public async Task ShouldSupportHTTPCredentialsSend() } + [PlaywrightTest("global-fetch.spec.ts", "should support multiple httpCredentials")] + public async Task ShouldSupportMultipleHttpCredentials() + { + Server.SetAuth("/empty.html", "user1", "pass1"); + var request = await Playwright.APIRequest.NewContextAsync(new() + { + HttpCredentialsList = new[] + { + new HttpCredentials { Username = "user1", Password = "pass1", Origin = Server.Prefix }, + new HttpCredentials { Username = "user2", Password = "pass2", Origin = Server.CrossProcessPrefix }, + }, + }); + var response1 = await request.GetAsync(Server.EmptyPage); + Assert.AreEqual(200, response1.Status); + // Wrong credentials are picked for the other origin. + var response2 = await request.GetAsync(Server.CrossProcessPrefix + "/empty.html"); + Assert.AreEqual(401, response2.Status); + await request.DisposeAsync(); + } + + [PlaywrightTest("global-fetch.spec.ts", "should support HTTPCredentials.send with multiple httpCredentials")] + public async Task ShouldSupportHTTPCredentialsSendWithMultipleHttpCredentials() + { + var request = await Playwright.APIRequest.NewContextAsync(new() + { + HttpCredentialsList = new[] + { + new HttpCredentials { Username = "user1", Password = "pass1", Origin = Server.Prefix, Send = HttpCredentialsSend.Always }, + new HttpCredentials { Username = "user2", Password = "pass2", Origin = Server.CrossProcessPrefix, Send = HttpCredentialsSend.Unauthorized }, + }, + }); + { + 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("user1:pass1"))); + 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") + ); + // This origin has send: 'unauthorized', so credentials are not sent proactively. + Assert.AreEqual(false, requestHeaders.ContainsKey("Authorization")); + Assert.AreEqual(200, response.Status); + } + await request.DisposeAsync(); + } + [PlaywrightTest("global-fetch.spec.ts", "should support global ignoreHTTPSErrors option")] public async Task ShouldSupportGlobalIgnoreHTTPSErrorsOption() { diff --git a/src/Playwright.Tests/HARTests.cs b/src/Playwright.Tests/HARTests.cs index a22446d053..80167a9cec 100644 --- a/src/Playwright.Tests/HARTests.cs +++ b/src/Playwright.Tests/HARTests.cs @@ -62,6 +62,22 @@ public async Task ShouldWorkWithPersistentContext() Assert.AreEqual("Hello", pageEntry.GetProperty("title").ToString()); } + [PlaywrightTest("har.spec.ts", "should close the context when saving the har fails")] + public async Task ShouldCloseTheContextWhenSavingTheHarFails() + { + using var tmp = new TempDirectory(); + var filePath = Path.Combine(tmp.Path, "not-a-directory"); + File.WriteAllText(filePath, "data"); + var context = await Browser.NewContextAsync(new() { RecordHarPath = Path.Combine(filePath, "test.har") }); + var page = await context.NewPageAsync(); + await page.GotoAsync(Server.EmptyPage); + var closedTcs = new TaskCompletionSource(); + context.Close += (_, _) => closedTcs.TrySetResult(true); + await PlaywrightAssert.ThrowsAsync(() => context.CloseAsync()); + await closedTcs.Task; + await context.CloseAsync(); + } + private async Task<(IPage, IBrowserContext, System.Func>)> PageWithHAR() { diff --git a/src/Playwright.Tests/Locator/LocatorAnyFrameTests.cs b/src/Playwright.Tests/Locator/LocatorAnyFrameTests.cs new file mode 100644 index 0000000000..cff12b672a --- /dev/null +++ b/src/Playwright.Tests/Locator/LocatorAnyFrameTests.cs @@ -0,0 +1,389 @@ +/* + * 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. + */ + + +namespace Microsoft.Playwright.Tests.Locator; + +public class LocatorAnyFrameTests : PageTestEx +{ + private Task RoutePage(string url, string body) + { + return Page.RouteAsync("**/" + url, route => route.FulfillAsync(new() + { + Body = body, + ContentType = "text/html" + })); + } + + private async Task WaitForAllFrames(int frameCount, string selector) + { + // Wait for all child frames to load their content, so that the search + // deterministically sees elements in all of them. + while (Page.Frames.Count != frameCount) + { + await Task.Delay(10); + } + foreach (var frame in Page.Frames) + { + if (frame != Page.MainFrame) + { + await frame.WaitForSelectorAsync(selector, new() { State = WaitForSelectorState.Attached }); + } + } + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should click a button inside an iframe")] + public async Task ShouldClickAButtonInsideAnIframe() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", ""); + await Page.GotoAsync(Server.EmptyPage); + await Page.FrameLocator().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync(); + Assert.IsTrue(await Page.Frames[1].EvaluateAsync("() => window.__clicked")); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should click a button in the main frame")] + public async Task ShouldClickAButtonInTheMainFrame() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "
No buttons here
"); + await Page.GotoAsync(Server.EmptyPage); + await Page.FrameLocator().Locator("button").ClickAsync(); + Assert.IsTrue(await Page.EvaluateAsync("() => window.__clicked")); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should fail click when elements match in multiple frames")] + public async Task ShouldFailClickWhenElementsMatchInMultipleFrames() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", ""); + await RoutePage("b.html", ""); + await Page.GotoAsync(Server.EmptyPage); + await WaitForAllFrames(3, "button"); + var error = await PlaywrightAssert.ThrowsAsync(() => Page.FrameLocator().Locator("button").ClickAsync(new() { Timeout = 3000 })); + StringAssert.Contains("frameLocator() matched elements in multiple frames", error.Message); + StringAssert.Contains("waiting for FrameLocator().Locator(\"button\")", error.Message); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should fail click upon strict mode violation inside a single frame")] + public async Task ShouldFailClickUponStrictModeViolationInsideASingleFrame() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", ""); + await Page.GotoAsync(Server.EmptyPage); + await WaitForAllFrames(2, "button"); + var error = await PlaywrightAssert.ThrowsAsync(() => Page.FrameLocator().Locator("button").ClickAsync(new() { Timeout = 3000 })); + StringAssert.Contains("strict mode violation", error.Message); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should time out on click when there are no matches")] + public async Task ShouldTimeOutOnClickWhenThereAreNoMatches() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "
Nothing here
"); + await Page.GotoAsync(Server.EmptyPage); + var error = await PlaywrightAssert.ThrowsAsync(() => Page.FrameLocator().Locator("button").ClickAsync(new() { Timeout = 1000 })); + StringAssert.Contains("Timeout 1000ms exceeded", error.Message); + StringAssert.Contains("waiting for FrameLocator().Locator(\"button\")", error.Message); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should count elements in a single frame")] + public async Task ShouldCountElementsInASingleFrame() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "
1
2
3
"); + await Page.GotoAsync(Server.EmptyPage); + await WaitForAllFrames(2, "div"); + Assert.AreEqual(3, await Page.FrameLocator().Locator("div").CountAsync()); + Assert.AreEqual(0, await Page.FrameLocator().Locator("button").CountAsync()); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should fail count when elements match in multiple frames")] + public async Task ShouldFailCountWhenElementsMatchInMultipleFrames() + { + await RoutePage("empty.html", "
main
"); + await RoutePage("a.html", "
child
"); + await Page.GotoAsync(Server.EmptyPage); + await WaitForAllFrames(2, "div"); + var error = await PlaywrightAssert.ThrowsAsync(() => Page.FrameLocator().Locator("div").CountAsync()); + StringAssert.Contains("frameLocator() matched elements in multiple frames", error.Message); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support toHaveCount")] + public async Task ShouldSupportToHaveCount() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "onetwo"); + await Page.GotoAsync(Server.EmptyPage); + await Expect(Page.FrameLocator().Locator("span")).ToHaveCountAsync(2); + await Expect(Page.FrameLocator().Locator("button")).ToHaveCountAsync(0); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should wait for a frame to appear with toHaveCount")] + public async Task ShouldWaitForAFrameToAppearWithToHaveCount() + { + await RoutePage("empty.html", "
No frames yet
"); + await RoutePage("a.html", "onetwo"); + await Page.GotoAsync(Server.EmptyPage); + await Page.EvaluateAsync(@"() => { + setTimeout(() => { + const iframe = document.createElement('iframe'); + iframe.src = 'a.html'; + document.body.appendChild(iframe); + }, 500); + }"); + await Expect(Page.FrameLocator().Locator("span")).ToHaveCountAsync(2); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should fail toHaveCount when elements match in multiple frames")] + public async Task ShouldFailToHaveCountWhenElementsMatchInMultipleFrames() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "one"); + await RoutePage("b.html", "two"); + await Page.GotoAsync(Server.EmptyPage); + await WaitForAllFrames(3, "span"); + var error = await PlaywrightAssert.ThrowsAsync(() => Expect(Page.FrameLocator().Locator("span")).ToHaveCountAsync(2, new() { Timeout = 3000 })); + StringAssert.Contains("frameLocator() matched elements in multiple frames", error.Message); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support toHaveText")] + public async Task ShouldSupportToHaveText() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "
Hello iframe
"); + await Page.GotoAsync(Server.EmptyPage); + await Expect(Page.FrameLocator().Locator("div")).ToHaveTextAsync("Hello iframe"); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support toHaveText with an array")] + public async Task ShouldSupportToHaveTextWithAnArray() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "onetwo"); + await Page.GotoAsync(Server.EmptyPage); + await Expect(Page.FrameLocator().Locator("span")).ToHaveTextAsync(new[] { "one", "two" }); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should fail toHaveText when elements match in multiple frames")] + public async Task ShouldFailToHaveTextWhenElementsMatchInMultipleFrames() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "
one
"); + await RoutePage("b.html", "
two
"); + await Page.GotoAsync(Server.EmptyPage); + await WaitForAllFrames(3, "div"); + var error = await PlaywrightAssert.ThrowsAsync(() => Expect(Page.FrameLocator().Locator("div")).ToHaveTextAsync("one", new() { Timeout = 3000 })); + StringAssert.Contains("frameLocator() matched elements in multiple frames", error.Message); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support evaluate")] + public async Task ShouldSupportEvaluate() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "
Hello
"); + await Page.GotoAsync(Server.EmptyPage); + Assert.AreEqual("bar", await Page.FrameLocator().Locator("div").EvaluateAsync("e => e.getAttribute('data-foo')")); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support evaluateAll")] + public async Task ShouldSupportEvaluateAll() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "onetwo"); + await Page.GotoAsync(Server.EmptyPage); + await WaitForAllFrames(2, "span"); + CollectionAssert.AreEqual(new[] { "one", "two" }, await Page.FrameLocator().Locator("span").EvaluateAllAsync("els => els.map(e => e.textContent)")); + Assert.AreEqual(0, await Page.FrameLocator().Locator("button").EvaluateAllAsync("els => els.length")); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support hasText filter")] + public async Task ShouldSupportHasTextFilter() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "
foo
bar
"); + await Page.GotoAsync(Server.EmptyPage); + await Expect(Page.FrameLocator().Locator("div", new() { HasText = "bar" })).ToHaveTextAsync("bar"); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support first/last/nth")] + public async Task ShouldSupportFirstLastNth() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "onetwothree"); + await Page.GotoAsync(Server.EmptyPage); + await WaitForAllFrames(2, "span"); + await Expect(Page.FrameLocator().Locator("span").First).ToHaveTextAsync("one"); + await Expect(Page.FrameLocator().Locator("span").Last).ToHaveTextAsync("three"); + await Expect(Page.FrameLocator().Locator("span").Nth(1)).ToHaveTextAsync("two"); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support composite locators")] + public async Task ShouldSupportCompositeLocators() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "
foo
bar
"); + await Page.GotoAsync(Server.EmptyPage); + await Expect(Page.FrameLocator().Locator("div", new() { Has = Page.Locator("span") })).ToHaveTextAsync("foo"); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should only search frames inside the starting frame")] + public async Task ShouldOnlySearchFramesInsideTheStartingFrame() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", ""); + await RoutePage("b.html", ""); + await Page.GotoAsync(Server.EmptyPage); + while (Page.Frames.Count != 3) + { + await Task.Delay(10); + } + var deepFrame = Page.Frames.First(f => f.Url.Contains("b.html")); + await deepFrame.WaitForSelectorAsync("button", new() { State = WaitForSelectorState.Attached }); + var middleFrame = Page.Frames.First(f => f.Url.Contains("a.html")); + await Expect(middleFrame.FrameLocator().Locator("button")).ToHaveTextAsync("deep"); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should enter a frame found in a nested frame")] + public async Task ShouldEnterAFrameFoundInANestedFrame() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", ""); + await RoutePage("b.html", ""); + await Page.GotoAsync(Server.EmptyPage); + await Expect(Page.FrameLocator().FrameLocator("#target").Locator("button")).ToHaveTextAsync("inside"); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should click inside an entered frame")] + public async Task ShouldClickInsideAnEnteredFrame() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", ""); + await RoutePage("b.html", ""); + await Page.GotoAsync(Server.EmptyPage); + await Page.FrameLocator().FrameLocator("#target").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync(); + var frame = Page.Frames.First(f => f.Url.Contains("b.html")); + Assert.IsTrue(await frame.EvaluateAsync("() => window.__clicked")); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should not search nested frames after entering a frame")] + public async Task ShouldNotSearchNestedFramesAfterEnteringAFrame() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", ""); + await RoutePage("b.html", ""); + await Page.GotoAsync(Server.EmptyPage); + while (Page.Frames.Count != 3) + { + await Task.Delay(10); + } + // The entered frame itself has no button, and we do not look inside its nested frames. + await Expect(Page.FrameLocator().FrameLocator("#target").Locator("button")).ToHaveCountAsync(0); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support two frameLocators")] + public async Task ShouldSupportTwoFrameLocators() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", ""); + await RoutePage("b.html", ""); + await RoutePage("c.html", ""); + await Page.GotoAsync(Server.EmptyPage); + await Expect(Page.FrameLocator().FrameLocator("#x").FrameLocator("#y").Locator("button")).ToHaveTextAsync("bottom"); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support locator before frameLocator")] + public async Task ShouldSupportLocatorBeforeFrameLocator() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "
"); + await RoutePage("b.html", ""); + await RoutePage("c.html", ""); + await Page.GotoAsync(Server.EmptyPage); + await Expect(Page.FrameLocator().Locator("section").FrameLocator("iframe").Locator("button")).ToHaveTextAsync("in-section"); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support owner of a frameLocator")] + public async Task ShouldSupportOwnerOfAFrameLocator() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", ""); + await RoutePage("b.html", ""); + await Page.GotoAsync(Server.EmptyPage); + Assert.AreEqual("target", await Page.FrameLocator().FrameLocator("#target").Owner.GetAttributeAsync("id")); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support contentFrame")] + public async Task ShouldSupportContentFrame() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", ""); + await RoutePage("b.html", ""); + await Page.GotoAsync(Server.EmptyPage); + await Expect(Page.FrameLocator().Locator("#target").ContentFrame.Locator("button")).ToHaveTextAsync("inside"); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should not allow frameLocator() inside a composite locator")] + public async Task ShouldNotAllowFrameLocatorInsideACompositeLocator() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "link"); + await Page.GotoAsync(Server.EmptyPage); + await WaitForAllFrames(2, "a"); + + var error = await PlaywrightAssert.ThrowsAsync(() => Page.Locator("button").Or(Page.FrameLocator().Locator("a")).CountAsync()); + StringAssert.Contains("frameLocator() is not allowed inside composite locators", error.Message); + + var error2 = await PlaywrightAssert.ThrowsAsync(() => Page.Locator("button").Filter(new() { Has = Page.FrameLocator().Locator("a") }).CountAsync()); + StringAssert.Contains("frameLocator() is not allowed inside composite locators", error2.Message); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should support a composite locator under frameLocator()")] + public async Task ShouldSupportACompositeLocatorUnderFrameLocator() + { + await RoutePage("empty.html", ""); + await RoutePage("a.html", "
first
"); + await Page.GotoAsync(Server.EmptyPage); + await WaitForAllFrames(2, "button"); + await Expect(Page.FrameLocator().Locator(".classname").Or(Page.GetByRole(AriaRole.Button))).ToHaveTextAsync(new[] { "first", "second" }); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should not allow first/last/nth on frameLocator()")] + public void ShouldNotAllowFirstLastNthOnFrameLocator() + { + var error1 = Assert.Throws(() => _ = Page.FrameLocator().First); + StringAssert.Contains("Selecting the nth frame is not allowed on frameLocator()", error1.Message); + var error2 = Assert.Throws(() => _ = Page.FrameLocator().Last); + StringAssert.Contains("Selecting the nth frame is not allowed on frameLocator()", error2.Message); + var error3 = Assert.Throws(() => Page.FrameLocator().Nth(1)); + StringAssert.Contains("Selecting the nth frame is not allowed on frameLocator()", error3.Message); + } + + [PlaywrightTest("locator-any-frame.spec.ts", "should not allow owner on frameLocator()")] + public async Task ShouldNotAllowOwnerOnFrameLocator() + { + var error = await PlaywrightAssert.ThrowsAsync(() => Page.FrameLocator().Owner.CountAsync()); + StringAssert.Contains("Selector cannot be empty after frameLocator()", error.Message); + } +} diff --git a/src/Playwright.Tests/Locator/LocatorMisc2Tests.cs b/src/Playwright.Tests/Locator/LocatorMisc2Tests.cs index 9a5eb13016..c02e20da63 100644 --- a/src/Playwright.Tests/Locator/LocatorMisc2Tests.cs +++ b/src/Playwright.Tests/Locator/LocatorMisc2Tests.cs @@ -208,6 +208,15 @@ public async Task LocatorShouldSupportFilterVisible() await Expect(Page.Locator(".item").Filter(new() { Visible = false }).GetByText("data1")).ToHaveTextAsync("Hidden data1"); } + [PlaywrightTest("locator-misc-2.spec.ts", "should support visible()")] + public async Task LocatorShouldSupportVisible() + { + await Page.SetContentAsync("
visible data1
visible data2
visible data3
"); + await Expect(Page.Locator(".item").Visible.Nth(1)).ToHaveTextAsync("visible data2"); + await Expect(Page.Locator(".item").Visible.GetByText("data3")).ToHaveTextAsync("visible data3"); + await Expect(Page.Locator(".item").Visible).ToHaveCountAsync(3); + } + [PlaywrightTest("locator-misc-2.spec.ts", "locator.count should work with deleted Map in main world")] public async Task LocatorCountShouldWorkWithDeletedMapInMainWorld() { diff --git a/src/Playwright.Tests/PageAriaSnapshotTests.cs b/src/Playwright.Tests/PageAriaSnapshotTests.cs index c2d1eb4430..862af5cafb 100644 --- a/src/Playwright.Tests/PageAriaSnapshotTests.cs +++ b/src/Playwright.Tests/PageAriaSnapshotTests.cs @@ -128,7 +128,7 @@ await Expect(Page.Locator("body")).ToMatchAriaSnapshotAsync(@" - listitem: ""Three"" ", new() { Timeout = 300 }); }); - StringAssert.Contains("Expect \"ToMatchAriaSnapshotAsync\" with timeout 300ms", exception.Message); + StringAssert.Contains("Expect \"ToMatchAriaSnapshotAsync\" Locator(\"body\") with timeout 300ms", exception.Message); StringAssert.Contains("- unexpected value", exception.Message); } diff --git a/src/Playwright.Tests/PageDialogTests.cs b/src/Playwright.Tests/PageDialogTests.cs index 56619b297d..837bb9d7e2 100644 --- a/src/Playwright.Tests/PageDialogTests.cs +++ b/src/Playwright.Tests/PageDialogTests.cs @@ -42,6 +42,71 @@ public Task ShouldFire() return Page.EvaluateAsync("alert('yo');"); } + [PlaywrightTest("page-dialog.spec.ts", "should fire dialogclosed when dialog is accepted")] + public async Task ShouldFireDialogClosedWhenDialogIsAccepted() + { + var closed = new List(); + Page.DialogClosed += (_, dialog) => closed.Add(dialog); + IDialog opened = null; + Page.Dialog += (_, dialog) => + { + opened = dialog; + _ = dialog.AcceptAsync(); + }; + await Page.EvaluateAsync("alert('yo')"); + while (closed.Count == 0) + { + await Task.Delay(10); + } + Assert.AreEqual(1, closed.Count); + Assert.AreEqual(opened, closed[0]); + // Perform some roundtrips to ensure the event does not fire twice. + await Page.EvaluateAsync("1"); + await Page.EvaluateAsync("1"); + Assert.AreEqual(1, closed.Count); + } + + [PlaywrightTest("page-dialog.spec.ts", "should fire dialogclosed when dialog is dismissed")] + public async Task ShouldFireDialogClosedWhenDialogIsDismissed() + { + var closedTcs = new TaskCompletionSource(); + Page.DialogClosed += (_, dialog) => closedTcs.TrySetResult(dialog); + Page.Dialog += (_, dialog) => _ = dialog.DismissAsync(); + await Page.EvaluateAsync("confirm('boolean?')"); + var closed = await closedTcs.Task; + Assert.AreEqual(DialogType.Confirm, closed.Type); + Assert.AreEqual("boolean?", closed.Message); + } + + [PlaywrightTest("page-dialog.spec.ts", "should fire dialogclosed for auto-dismissed dialogs")] + public async Task ShouldFireDialogClosedForAutoDismissedDialogs() + { + var closedTcs = new TaskCompletionSource(); + Page.DialogClosed += (_, dialog) => closedTcs.TrySetResult(dialog); + await Page.EvaluateAsync("alert('yo')"); + var closed = await closedTcs.Task; + Assert.AreEqual("yo", closed.Message); + } + + [PlaywrightTest("chromium/chromium.spec.ts", "should fire dialogclosed event when dialog is closed out of band")] + [Skip(SkipAttribute.Targets.Firefox, SkipAttribute.Targets.Webkit)] + public async Task ShouldFireDialogClosedEventWhenDialogIsClosedOutOfBand() + { + // Establish the CDP session up front: creating one while a dialog is blocking the page hangs. + var client = await Page.Context.NewCDPSessionAsync(Page); + await client.SendAsync("Page.enable"); + var dialogTcs = new TaskCompletionSource(); + Page.Dialog += (_, dialog) => dialogTcs.TrySetResult(dialog); + var closedTcs = new TaskCompletionSource(); + Page.DialogClosed += (_, dialog) => closedTcs.TrySetResult(dialog); + var evaluateTask = Page.EvaluateAsync("alert('yo')"); + var opened = await dialogTcs.Task; + // Handle the dialog out of band, similar to the user closing it in the headed browser. + await client.SendAsync("Page.handleJavaScriptDialog", new Dictionary { { "accept", true } }); + Assert.AreEqual(opened, await closedTcs.Task); + await evaluateTask; + } + [PlaywrightTest("page-dialog.spec.ts", "should allow accepting prompts")] public async Task ShouldAllowAcceptingPrompts() { diff --git a/src/Playwright.Tests/TracingTests.cs b/src/Playwright.Tests/TracingTests.cs index 5063a45068..ca55b882ca 100644 --- a/src/Playwright.Tests/TracingTests.cs +++ b/src/Playwright.Tests/TracingTests.cs @@ -47,7 +47,6 @@ await Context.Tracing.StartAsync(new() await page.ClickAsync("\"Click\""); await page.Mouse.MoveAsync(20, 20); await page.Mouse.DblClickAsync(20, 30); - await page.APIRequest.GetAsync(Server.Prefix + "/empty.html"); await page.Keyboard.InsertTextAsync("abc"); await page.WaitForTimeoutAsync(2000); // Give it some time to produce screenshots. await page.CloseAsync(); @@ -60,12 +59,11 @@ await ShowTraceViewerAsync(tracePath, async traceViewer => { await Expect(traceViewer.ActionTitles).ToHaveTextAsync([ new Regex(@"Create page"), - new Regex(@"Navigate to ""/frames/frame.html"""), + new Regex(@"Navigate.*/frames/frame.html"), new Regex(@"Set content"), new Regex(@"Click"), new Regex(@"Mouse move"), new Regex(@"Double click"), - new Regex(@"GET ""/empty.html"""), new Regex(@"Insert ""abc"""), new Regex(@"Wait for timeout"), new Regex(@"Close") @@ -100,7 +98,7 @@ public async Task ShouldCollectTwoTraces() await ShowTraceViewerAsync(trace1Path, async traceViewer => { await Expect(traceViewer.ActionTitles).ToHaveTextAsync([ - new Regex(@"Navigate to ""/empty.html"""), + new Regex(@"Navigate.*/empty.html"), new Regex(@"Set content"), new Regex(@"Click") ]); @@ -196,7 +194,7 @@ await ShowTraceViewerAsync(tracePath, async traceViewer => { await Expect(traceViewer.ActionTitles).ToHaveTextAsync([ new Regex(@"Create page"), - new Regex(@"Navigate to ""/empty.html"""), + new Regex(@"Navigate.*/empty.html"), new Regex(@"Set content"), new Regex(@"Click"), new Regex(@"Close") @@ -230,6 +228,69 @@ await Context.Tracing.StartAsync(new() await Context.Tracing.StopAsync(); } + [PlaywrightTest("tracing.spec.ts", "should recover tracing after a failed stop")] + public async Task ShouldRecoverTracingAfterAFailedStop() + { + using var tmp = new TempDirectory(); + await Context.Tracing.StartAsync(); + // Saving fails: a parent of the destination is a file, not a directory. + var blocker = Path.Combine(tmp.Path, "blocker"); + File.WriteAllText(blocker, ""); + await PlaywrightAssert.ThrowsAsync(() => Context.Tracing.StopAsync(new() { Path = Path.Combine(blocker, "trace1.zip") })); + + // The failed stop must not wedge tracing for the rest of the context lifetime. + await Context.Tracing.StartAsync(); + var page = await Context.NewPageAsync(); + await page.GotoAsync(Server.Prefix + "/input/button.html"); + await page.ClickAsync("button"); + var tracePath = Path.Combine(tmp.Path, "trace2.zip"); + await Context.Tracing.StopAsync(new() { Path = tracePath }); + + await ShowTraceViewerAsync(tracePath, async traceViewer => + { + await Expect(traceViewer.ActionTitles).ToHaveTextAsync([ + new Regex(@"Create page"), + new Regex(@"Navigate.*/input/button.html"), + new Regex(@"Click"), + ]); + }); + } + + [PlaywrightTest("tracing.spec.ts", "should collect trace with aria and screen snapshots")] + public async Task ShouldCollectTraceWithAriaAndScreenSnapshots() + { + await Context.Tracing.StartAsync(new() + { + Snapshots = true, + AriaSnapshots = true, + ScreenSnapshots = true, + }); + + var page = await Context.NewPageAsync(); + await page.GotoAsync(Server.EmptyPage); + await page.SetContentAsync(""); + await page.ClickAsync("\"Click\""); + await page.CloseAsync(); + + using var tmp = new TempDirectory(); + var tracePath = Path.Combine(tmp.Path, "trace.zip"); + await Context.Tracing.StopAsync(new() { Path = tracePath }); + + await ShowTraceViewerAsync(tracePath, async traceViewer => + { + await Expect(traceViewer.ActionTitles).ToHaveTextAsync([ + new Regex(@"Create page"), + new Regex(@"Navigate.*/empty.html"), + new Regex(@"Set content"), + new Regex(@"Click"), + new Regex(@"Close") + ]); + await traceViewer.SelectActionAsync("Set content"); + var frame = await traceViewer.SnapshotFrame("Set content", 0, false); + await Expect(frame.Locator("button")).ToHaveTextAsync("Click"); + }); + } + [PlaywrightTest()] public async Task ShouldSendDotNetTitles() { @@ -258,14 +319,14 @@ await ShowTraceViewerAsync(tracePath, async traceViewer => { await Expect(traceViewer.ActionTitles).ToHaveTextAsync([ new Regex(@"Create page"), - new Regex(@"Navigate to ""/empty.html"""), + new Regex(@"Navigate.*/empty.html"), new Regex(@"Set content"), // TODO: Should be: Wait for event "Page" new Regex(@"Wait for event ""context\.WaitForEventAsync\(""Page""\)"""), new Regex(@"Click"), new Regex(@"Evaluate"), - new Regex(@"Navigate to ""/empty.html"""), - new Regex(@"Navigate to ""/one-style.html"""), + new Regex(@"Navigate.*/empty.html"), + new Regex(@"Navigate.*/one-style.html"), ]); }); } @@ -287,7 +348,7 @@ public async Task ShouldDisplayWaitForLoadStateEvenIfDidNotWaitForIt() await ShowTraceViewerAsync(tracePath, async traceViewer => { await Expect(traceViewer.ActionTitles).ToHaveTextAsync([ - new Regex(@"Navigate to ""/empty.html"""), + new Regex(@"Navigate.*/empty.html"), new Regex(@"Wait for event ""frame.WaitForLoadStateAsync"""), new Regex(@"Wait for event ""frame.WaitForLoadStateAsync"""), ]); @@ -320,7 +381,7 @@ public async Task ShouldRespectTracesDirAndName() await ShowTraceViewerAsync(Path.Combine(tracesDir.Path, "trace1.zip"), async traceViewer => { await Expect(traceViewer.ActionTitles).ToHaveTextAsync([ - new Regex(@"Navigate to ""/one-style.html"""), + new Regex(@"Navigate.*/one-style.html"), ]); var frame = await traceViewer.SnapshotFrame("Navigate", 0, false); await Expect(frame.Locator("body")).ToHaveCSSAsync("background-color", "rgb(255, 192, 203)"); @@ -330,7 +391,7 @@ await Expect(traceViewer.ActionTitles).ToHaveTextAsync([ await ShowTraceViewerAsync(Path.Combine(tracesDir.Path, "trace2.zip"), async traceViewer => { await Expect(traceViewer.ActionTitles).ToHaveTextAsync([ - new Regex(@"Navigate to ""/har.html"""), + new Regex(@"Navigate.*/har.html"), ]); var frame = await traceViewer.SnapshotFrame("Navigate", 0, false); await Expect(frame.Locator("body")).ToHaveCSSAsync("background-color", "rgb(255, 192, 203)"); @@ -364,7 +425,7 @@ await ShowTraceViewerAsync(tracePath, async traceViewer => await Expect(traceViewer.ActionTitles).ToHaveTextAsync([ new Regex(@"Create page"), new Regex("outer group"), - new Regex(@"Navigate to ""data"), + new Regex(@"Navigate.*data:"), new Regex("inner group 1"), new Regex(@"Click"), new Regex("inner group 2"), diff --git a/src/Playwright/API/Generated/IAPIRequestContext.cs b/src/Playwright/API/Generated/IAPIRequestContext.cs index cd24e67cd8..2f32f9cbe3 100644 --- a/src/Playwright/API/Generated/IAPIRequestContext.cs +++ b/src/Playwright/API/Generated/IAPIRequestContext.cs @@ -264,5 +264,6 @@ public partial interface IAPIRequestContext /// Call options Task StorageStateAsync(APIRequestContextStorageStateOptions? options = default); + /// Tracing recorder for requests made through this API request context. public ITracing Tracing { get; } } diff --git a/src/Playwright/API/Generated/IBrowserContext.cs b/src/Playwright/API/Generated/IBrowserContext.cs index 3d15410050..acab4df7b6 100644 --- a/src/Playwright/API/Generated/IBrowserContext.cs +++ b/src/Playwright/API/Generated/IBrowserContext.cs @@ -140,6 +140,15 @@ public partial interface IBrowserContext /// event EventHandler Dialog; + /// + /// + /// Emitted when a JavaScript dialog in any page belonging to this context has been + /// closed, either by , by , + /// or manually by the user in the headed browser. + /// + /// + event EventHandler DialogClosed; + /// /// /// Emitted when attachment download started in any page belonging to this context. @@ -1010,7 +1019,8 @@ public partial interface IBrowserContext /// /// /// Returns storage state for this browser context, contains current cookies, local - /// storage snapshot, IndexedDB snapshot and virtual WebAuthn credentials. + /// storage snapshot, IndexedDB snapshot, origin private file system snapshot and virtual + /// WebAuthn credentials. /// /// /// Call options @@ -1018,10 +1028,11 @@ public partial interface IBrowserContext /// /// - /// Clears the existing cookies, local storage, IndexedDB entries and virtual WebAuthn - /// credentials, and sets the new storage state. When the storage state contains credentials, - /// the virtual WebAuthn authenticator is installed (equivalent to ), - /// preventing all real authenticators from working in this context. + /// Clears the existing cookies, local storage, IndexedDB entries, origin private file + /// system entries and virtual WebAuthn credentials, and sets the new storage state. + /// When the storage state contains credentials, the virtual WebAuthn authenticator + /// is installed (equivalent to ), preventing + /// all real authenticators from working in this context. /// /// **Usage** /// diff --git a/src/Playwright/API/Generated/IFrame.cs b/src/Playwright/API/Generated/IFrame.cs index d4532c81d5..233fed76f1 100644 --- a/src/Playwright/API/Generated/IFrame.cs +++ b/src/Playwright/API/Generated/IFrame.cs @@ -504,6 +504,13 @@ public partial interface IFrame /// When working with iframes, you can create a frame locator that will enter the iframe /// and allow selecting elements in that iframe. /// + /// + /// When called without , the search starts in this + /// frame or in any of the iframes inside it, so that you don't need to locate each + /// iframe first. Note that the rest of the locator is resolved inside a single frame, + /// just like any other locator. If it matches elements inside multiple frames, an error + /// is thrown. + /// /// **Usage** /// /// Following snippet locates element with text "Submit" in the iframe with id my-frame, @@ -513,9 +520,20 @@ public partial interface IFrame /// var locator = frame.FrameLocator("#my-iframe").GetByText("Submit");
/// await locator.ClickAsync(); ///
+ /// + /// Following snippet locates a button, either in the frame or in one of the iframes + /// inside it: + /// + /// + /// var locator = frame.FrameLocator().GetByRole(AriaRole.Button);
+ /// await locator.ClickAsync(); + ///
///
- /// A selector to use when resolving DOM element. - IFrameLocator FrameLocator(string selector); + /// + /// A selector that matches the frame element. When not specified, locator is matched + /// in this frame or in any of the iframes inside it. + /// + IFrameLocator FrameLocator(string? selector = default); /// /// diff --git a/src/Playwright/API/Generated/IFrameLocator.cs b/src/Playwright/API/Generated/IFrameLocator.cs index e5cb7d3621..a5a7bc8486 100644 --- a/src/Playwright/API/Generated/IFrameLocator.cs +++ b/src/Playwright/API/Generated/IFrameLocator.cs @@ -49,6 +49,29 @@ namespace Microsoft.Playwright; /// // Works because we explicitly tell locator to pick the first frame:
/// await page.Locator(".result-frame").First.ContentFrame.getByRole(AriaRole.Button).ClickAsync(); /// +/// **Any frame** +/// +/// Calling or without +/// a selector creates a frame locator that starts the search in any frame of the subtree +/// - so that you don't need to locate the iframe first. +/// +/// +/// // Finds the button in any frame on the page:
+/// await page.FrameLocator().GetByRole(AriaRole.Button).ClickAsync();
+///
+/// // Finds the iframe with id "my-frame" anywhere on the page, and clicks the button inside it:
+/// await page.FrameLocator().Locator("#my-frame").ContentFrame.GetByRole(AriaRole.Button).ClickAsync(); +///
+/// +/// Only the start of the search is affected - the rest of the locator is resolved inside +/// a single frame, just like any other locator. Following the strictness rules above, +/// an error is thrown when elements are matched in multiple frames. +/// +/// +/// Such a frame locator does not point to a particular iframe, so , +/// , and +/// are not supported on it. +/// /// **Converting Locator to FrameLocator** /// /// If you have a object pointing to an iframe it can diff --git a/src/Playwright/API/Generated/ILocator.cs b/src/Playwright/API/Generated/ILocator.cs index 992bfae6aa..88d2e5b814 100644 --- a/src/Playwright/API/Generated/ILocator.cs +++ b/src/Playwright/API/Generated/ILocator.cs @@ -1190,7 +1190,7 @@ public partial interface ILocator /// /// Returns locator to the last matching element. /// **Usage** - /// var banana = await page.GetByRole(AriaRole.Listitem).Last(1); + /// var banana = page.GetByRole(AriaRole.Listitem).Last; /// ILocator Last { get; } @@ -1965,6 +1965,27 @@ public partial interface ILocator /// Call options Task UncheckAsync(LocatorUncheckOptions? options = default); + /// + /// + /// Returns a locator that matches only visible + /// elements, ignoring the invisible ones. This is the recommended way to distinguish + /// elements by visibility, as opposed to the :visible CSS pseudo-class. + /// + /// + /// Note that visibility is checked every time the locator is used, and not at the moment + /// of the call. + /// + /// **Usage** + /// Consider a page with two buttons, the first invisible and the second visible. + /// This will only find the second button, because it is visible, and then click it. + /// await page.Locator("button").Visible.ClickAsync(); + /// + /// To match invisible elements instead, use with the + /// option set to false. + /// + /// + ILocator Visible { get; } + /// /// /// Returns when element specified by locator satisfies the diff --git a/src/Playwright/API/Generated/IPage.cs b/src/Playwright/API/Generated/IPage.cs index bac08cf88f..ea93f678e2 100644 --- a/src/Playwright/API/Generated/IPage.cs +++ b/src/Playwright/API/Generated/IPage.cs @@ -147,6 +147,14 @@ public partial interface IPage /// event EventHandler Dialog; + /// + /// + /// Emitted when a JavaScript dialog has been closed, either by , + /// by , or manually by the user in the headed browser. + /// + /// + event EventHandler DialogClosed; + /// /// /// Emitted when the JavaScript DOMContentLoaded @@ -971,6 +979,13 @@ public partial interface IPage /// When working with iframes, you can create a frame locator that will enter the iframe /// and allow selecting elements in that iframe. /// + /// + /// When called without , the search starts in any frame + /// on the page - the main frame or any of the iframes - so that you don't need to locate + /// each iframe first. Note that the rest of the locator is resolved inside a single + /// frame, just like any other locator. If it matches elements inside multiple frames, + /// an error is thrown. + /// /// **Usage** /// /// Following snippet locates element with text "Submit" in the iframe with id my-frame, @@ -980,9 +995,17 @@ public partial interface IPage /// var locator = page.FrameLocator("#my-iframe").GetByText("Submit");
/// await locator.ClickAsync(); /// + /// Following snippet locates a button, either in the main frame or in one of the iframes: + /// + /// var locator = page.FrameLocator().GetByRole(AriaRole.Button);
+ /// await locator.ClickAsync(); + ///
///
- /// A selector to use when resolving DOM element. - IFrameLocator FrameLocator(string selector); + /// + /// A selector that matches the frame element. When not specified, locator is matched + /// in any frame on the page. + /// + IFrameLocator FrameLocator(string? selector = default); /// An array of all frames attached to the page. IReadOnlyList