diff --git a/README.md b/README.md index 28eb96284e..07ab459aee 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 134.0.6998.35 | ✅ | ✅ | ✅ | +| Chromium 136.0.7103.25 | ✅ | ✅ | ✅ | | WebKit 18.4 | ✅ | ✅ | ✅ | -| Firefox 135.0 | ✅ | ✅ | ✅ | +| Firefox 137.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 36d1b11ed3..1e762ee6de 100644 --- a/src/Common/Version.props +++ b/src/Common/Version.props @@ -2,7 +2,7 @@ 1.51.0 $(AssemblyVersion) - 1.51.1 + 1.53.0-alpha-1744741041000 $(AssemblyVersion) $(AssemblyVersion) true diff --git a/src/Playwright.TestingHarnessTest/package-lock.json b/src/Playwright.TestingHarnessTest/package-lock.json index 3ee46e1c8e..1fc2ba80b4 100644 --- a/src/Playwright.TestingHarnessTest/package-lock.json +++ b/src/Playwright.TestingHarnessTest/package-lock.json @@ -7,18 +7,18 @@ "": { "name": "playwright.testingharnesstest", "devDependencies": { - "@playwright/test": "1.51.0-beta-1741166263000", + "@playwright/test": "1.53.0-alpha-1744741041000", "@types/node": "^22.12.0", "fast-xml-parser": "^4.5.0" } }, "node_modules/@playwright/test": { - "version": "1.51.0-beta-1741166263000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.0-beta-1741166263000.tgz", - "integrity": "sha512-aNaXN3s3x4x12FnJDzi8mYEGOTNTbufQAjkgxsnklwv6hurAfObeDQ8LC2i4Bv6blfd4EweVVeT5IOm1IXwHoA==", + "version": "1.53.0-alpha-1744741041000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0-alpha-1744741041000.tgz", + "integrity": "sha512-+qGDwOWX9ndcHNO+psE/gi9czgTeJKOiva/wcRVjOQprrTfqiWmFTT++BRq67nTsSTpULrk1nEVF4Bua6wx+jQ==", "dev": true, "dependencies": { - "playwright": "1.51.0-beta-1741166263000" + "playwright": "1.53.0-alpha-1744741041000" }, "bin": { "playwright": "cli.js" @@ -75,12 +75,12 @@ } }, "node_modules/playwright": { - "version": "1.51.0-beta-1741166263000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.0-beta-1741166263000.tgz", - "integrity": "sha512-WiaXkGp7bXE3S5Q6E5W00hQvzB4JGXr9uItwM5CmuQV1/SrKcCyp4ECu2TLi3I20vPihNmATvdWq+fbE71Ih9w==", + "version": "1.53.0-alpha-1744741041000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0-alpha-1744741041000.tgz", + "integrity": "sha512-ZNQDPpRM4EGxRF7yIa7u0Wf3R8yy59CWQIBrPlMRCyaDbBrOY/j0ppvK6kGuwA8w5b128BKF6+MmXC43BDsZFg==", "dev": true, "dependencies": { - "playwright-core": "1.51.0-beta-1741166263000" + "playwright-core": "1.53.0-alpha-1744741041000" }, "bin": { "playwright": "cli.js" @@ -93,9 +93,9 @@ } }, "node_modules/playwright-core": { - "version": "1.51.0-beta-1741166263000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.0-beta-1741166263000.tgz", - "integrity": "sha512-lbRKDbtGPalFp9dR9CF+B3PULyYa+VhuZWIlnb/brY/bwrudMeM9PIgg/zJNDz0w74/WVGVoVclmqibuRzl1jA==", + "version": "1.53.0-alpha-1744741041000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0-alpha-1744741041000.tgz", + "integrity": "sha512-TTbinBOSYGCzLCSLJMRvnu+iBeMgRa3hgl7m2Ek8hKqBWywsIoUWJgBE5lqfyXUfh4Px1326/RIMcAG9v3VdNA==", "dev": true, "bin": { "playwright-core": "cli.js" @@ -120,12 +120,12 @@ }, "dependencies": { "@playwright/test": { - "version": "1.51.0-beta-1741166263000", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.0-beta-1741166263000.tgz", - "integrity": "sha512-aNaXN3s3x4x12FnJDzi8mYEGOTNTbufQAjkgxsnklwv6hurAfObeDQ8LC2i4Bv6blfd4EweVVeT5IOm1IXwHoA==", + "version": "1.53.0-alpha-1744741041000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0-alpha-1744741041000.tgz", + "integrity": "sha512-+qGDwOWX9ndcHNO+psE/gi9czgTeJKOiva/wcRVjOQprrTfqiWmFTT++BRq67nTsSTpULrk1nEVF4Bua6wx+jQ==", "dev": true, "requires": { - "playwright": "1.51.0-beta-1741166263000" + "playwright": "1.53.0-alpha-1744741041000" } }, "@types/node": { @@ -154,19 +154,19 @@ "optional": true }, "playwright": { - "version": "1.51.0-beta-1741166263000", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.0-beta-1741166263000.tgz", - "integrity": "sha512-WiaXkGp7bXE3S5Q6E5W00hQvzB4JGXr9uItwM5CmuQV1/SrKcCyp4ECu2TLi3I20vPihNmATvdWq+fbE71Ih9w==", + "version": "1.53.0-alpha-1744741041000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0-alpha-1744741041000.tgz", + "integrity": "sha512-ZNQDPpRM4EGxRF7yIa7u0Wf3R8yy59CWQIBrPlMRCyaDbBrOY/j0ppvK6kGuwA8w5b128BKF6+MmXC43BDsZFg==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.51.0-beta-1741166263000" + "playwright-core": "1.53.0-alpha-1744741041000" } }, "playwright-core": { - "version": "1.51.0-beta-1741166263000", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.0-beta-1741166263000.tgz", - "integrity": "sha512-lbRKDbtGPalFp9dR9CF+B3PULyYa+VhuZWIlnb/brY/bwrudMeM9PIgg/zJNDz0w74/WVGVoVclmqibuRzl1jA==", + "version": "1.53.0-alpha-1744741041000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0-alpha-1744741041000.tgz", + "integrity": "sha512-TTbinBOSYGCzLCSLJMRvnu+iBeMgRa3hgl7m2Ek8hKqBWywsIoUWJgBE5lqfyXUfh4Px1326/RIMcAG9v3VdNA==", "dev": true }, "strnum": { diff --git a/src/Playwright.TestingHarnessTest/package.json b/src/Playwright.TestingHarnessTest/package.json index 31f5e4441f..9014675b58 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.51.0-beta-1741166263000", + "@playwright/test": "1.53.0-alpha-1744741041000", "@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 aaf6507ec4..de62c35373 100644 --- a/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs +++ b/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs @@ -553,6 +553,31 @@ public async Task ShouldSupportToHaveClass() } } + [PlaywrightTest("playwright-test/playwright.expect.spec.ts", "should support toContainClass")] + public async Task ShouldSupportToContainClass() + { + { + await Page.SetContentAsync("
"); + var locator = Page.Locator("div"); + await Expect(locator).ToContainClassAsync(""); + await Expect(locator).ToContainClassAsync("bar"); + await Expect(locator).ToContainClassAsync("baz bar"); + await Expect(locator).ToContainClassAsync(" bar foo "); + await Expect(locator).Not.ToContainClassAsync("baz not-matching"); + 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("LocatorAssertions.ToContainClassAsync with timeout 300ms", exception.Message); + } + { + await Page.SetContentAsync("
"); + var locator = Page.Locator("div"); + await Expect(locator).ToContainClassAsync(new string[] { "foo", "hello", "baz" }); + await Expect(locator).Not.ToContainClassAsync(new string[] { "not-there", "hello", "baz" }); + await Expect(locator).Not.ToContainClassAsync(new string[] { "foo", "hello" }); + } + } + [PlaywrightTest("playwright-test/playwright.expect.spec.ts", "should support toHaveCount")] public async Task ShouldSupportToHaveCount() { diff --git a/src/Playwright.Tests/GlobalFetchTests.cs b/src/Playwright.Tests/GlobalFetchTests.cs index a22b712db1..c3003221f1 100644 --- a/src/Playwright.Tests/GlobalFetchTests.cs +++ b/src/Playwright.Tests/GlobalFetchTests.cs @@ -408,6 +408,22 @@ public async Task ShouldNotFollowRedirectsWhenMaxRedirectsIsSetTo0() await request.DisposeAsync(); } + [PlaywrightTest("global-fetch.spec.ts", "should not follow redirects when maxRedirects is set to 0 in newContext")] + public async Task ShouldNotFollowRedirectsWhenMaxRedirectsIsSetTo0InNewContext() + { + Server.SetRedirect("/a/redirect1", "/b/c/redirect2"); + Server.SetRedirect("/b/c/redirect2", "/simple.json"); + + var request = await Playwright.APIRequest.NewContextAsync(new() { MaxRedirects = 0 }); + foreach (var method in new[] { "GET", "PUT", "POST", "OPTIONS", "HEAD", "PATCH" }) + { + var response = await request.FetchAsync($"{Server.Prefix}/a/redirect1", new() { Method = method }); + Assert.AreEqual("/b/c/redirect2", response.Headers["location"]); + Assert.AreEqual(302, response.Status); + } + await request.DisposeAsync(); + } + [PlaywrightTest("global-fetch.spec.ts", "should throw an error when maxRedirects is less than 0")] public async Task ShouldThrowAnErrorWhenMaxRedirectsIsLessThan0() { diff --git a/src/Playwright.Tests/PageAriaSnapshotTests.cs b/src/Playwright.Tests/PageAriaSnapshotTests.cs index 27f9e3fe8e..d58fc5beb8 100644 --- a/src/Playwright.Tests/PageAriaSnapshotTests.cs +++ b/src/Playwright.Tests/PageAriaSnapshotTests.cs @@ -99,7 +99,64 @@ await Page.SetContentAsync(@" await CheckAndMatchSnapshot(Page.Locator("body"), @" - list: - listitem: - - link ""link"" + - link ""link"": + - /url: about:blank + "); + } + + [PlaywrightTest("to-match-aria-snapshot.spec.ts", "should detect unexpected children: equal")] + public async Task ShouldDetectUnexpectedChildrenEqual() + { + await Page.SetContentAsync(@" + + "); + await Expect(Page.Locator("body")).ToMatchAriaSnapshotAsync(@" + - list: + - listitem: ""One"" + - listitem: ""Three"" + "); + var exception = await PlaywrightAssert.ThrowsAsync(() => + { + return Expect(Page.Locator("body")).ToMatchAriaSnapshotAsync(@" + - list: + - /children: equal + - listitem: ""One"" + - listitem: ""Three"" + ", new() { Timeout = 300 }); + }); + StringAssert.Contains("LocatorAssertions.ToMatchAriaSnapshotAsync with timeout 300ms", exception.Message); + StringAssert.Contains("- unexpected value", exception.Message); + } + + [PlaywrightTest("page-aria-snapshot.spec.ts", "should generate refs")] + public async Task ShouldGenerateRefs() + { + await Page.SetContentAsync(@" + + + + "); + var snapshot = await Page.Locator("body").AriaSnapshotAsync(new() { Ref = true }); + Assert.AreEqual(_unshift(@" + - button ""One"" [ref=s1e3] + - button ""Two"" [ref=s1e4] + - button ""Three"" [ref=s1e5] + "), snapshot); + } + + [PlaywrightTest("to-match-aria-snapshot.spec.ts", "should match url")] + public async Task ShouldMatchUrl() + { + await Page.SetContentAsync(@" + Link + "); + await Expect(Page.Locator("body")).ToMatchAriaSnapshotAsync(@" + - link: + - /url: /.*example.com/ "); } } diff --git a/src/Playwright.Tests/PageClockTests.cs b/src/Playwright.Tests/PageClockTests.cs index 6a675a00e0..60b798e367 100644 --- a/src/Playwright.Tests/PageClockTests.cs +++ b/src/Playwright.Tests/PageClockTests.cs @@ -462,8 +462,7 @@ public async Task ShouldPause() await Page.Clock.InstallAsync(new() { TimeDate = DateTimeOffset.FromUnixTimeMilliseconds(0).UtcDateTime }); await Page.GotoAsync("data:text/html,"); await Page.Clock.PauseAtAsync(DateTimeOffset.FromUnixTimeMilliseconds(1000).UtcDateTime); - await Page.WaitForTimeoutAsync(1000); - await Page.Clock.ResumeAsync(); + await Page.WaitForTimeoutAsync(1111); var now = await Page.EvaluateAsync("Date.now()"); Assert.GreaterOrEqual(now, 0); Assert.LessOrEqual(now, 1000); diff --git a/src/Playwright.Tests/PageEvaluateTests.cs b/src/Playwright.Tests/PageEvaluateTests.cs index b5538a2e61..b5fed1f962 100644 --- a/src/Playwright.Tests/PageEvaluateTests.cs +++ b/src/Playwright.Tests/PageEvaluateTests.cs @@ -74,6 +74,22 @@ public async Task ShouldTransferNegativeInfinity() Assert.AreEqual(double.NegativeInfinity, result); } + [PlaywrightTest("page-evaluate.spec.ts", "should transfer typed arrays")] + public async Task ShouldTransferTypedArrays() + { + Assert.AreEqual(new sbyte[] { 1, 2, 3 }, await Page.EvaluateAsync("() => new Int8Array([1, 2, 3])")); + Assert.AreEqual(new byte[] { 1, 2, 3 }, await Page.EvaluateAsync("() => new Uint8Array([1, 2, 3])")); + Assert.AreEqual(new byte[] { 1, 2, 3 }, await Page.EvaluateAsync("() => new Uint8ClampedArray([1, 2, 3])")); + Assert.AreEqual(new short[] { 1, 2, 3 }, await Page.EvaluateAsync("() => new Int16Array([1, 2, 3])")); + Assert.AreEqual(new ushort[] { 1, 2, 3 }, await Page.EvaluateAsync("() => new Uint16Array([1, 2, 3])")); + Assert.AreEqual(new int[] { 1, 2, 3 }, await Page.EvaluateAsync("() => new Int32Array([1, 2, 3])")); + Assert.AreEqual(new uint[] { 1, 2, 3 }, await Page.EvaluateAsync("() => new Uint32Array([1, 2, 3])")); + Assert.AreEqual(new float[] { 1.5F, 2.5F, 3.5F }, await Page.EvaluateAsync("() => new Float32Array([1.5, 2.5, 3.5])")); + Assert.AreEqual(new double[] { 1.5, 2.5, 3.5 }, await Page.EvaluateAsync("() => new Float64Array([1.5, 2.5, 3.5])")); + Assert.AreEqual(new long[] { 1, 2, 3 }, await Page.EvaluateAsync("() => new BigInt64Array([1n, 2n, 3n])")); + Assert.AreEqual(new ulong[] { 1, 2, 3 }, await Page.EvaluateAsync("() => new BigUint64Array([1n, 2n, 3n])")); + } + [PlaywrightTest("page-evaluate.spec.ts", "should transfer bigint")] public async Task ShouldTransferBigInt() { diff --git a/src/Playwright.Tests/PageRouteTests.cs b/src/Playwright.Tests/PageRouteTests.cs index e39ebe462d..55c4ad03ed 100644 --- a/src/Playwright.Tests/PageRouteTests.cs +++ b/src/Playwright.Tests/PageRouteTests.cs @@ -99,12 +99,13 @@ void handler4(IRoute route) Assert.AreEqual(new[] { 1 }, intercepted.ToArray()); } - [PlaywrightTest("page-route.spec.ts", "should support ? in glob pattern")] - public async Task ShouldSupportInGlobPattern() + [PlaywrightTest("page-route.spec.ts", "should not support ? in glob pattern")] + public async Task ShouldNotSupportQuestionMarkInGlobPattern() { Server.SetRoute("/index", context => context.Response.WriteAsync("index-no-hello")); Server.SetRoute("/index123hello", context => context.Response.WriteAsync("index123hello")); Server.SetRoute("/index?hello", context => context.Response.WriteAsync("index?hello")); + Server.SetRoute("/index1hello", context => context.Response.WriteAsync("index1hello")); await Page.RouteAsync("**/index?hello", (route) => route.FulfillAsync(new() { Body = "intercepted any character" })); await Page.RouteAsync("**/index\\?hello", (route) => route.FulfillAsync(new() { Body = "intercepted question mark" })); @@ -116,7 +117,7 @@ public async Task ShouldSupportInGlobPattern() StringAssert.Contains("index-no-hello", await Page.ContentAsync()); await Page.GotoAsync(Server.Prefix + "/index1hello"); - StringAssert.Contains("intercepted any character", await Page.ContentAsync()); + StringAssert.Contains("index1hello", await Page.ContentAsync()); await Page.GotoAsync(Server.Prefix + "/index123hello"); StringAssert.Contains("index123hello", await Page.ContentAsync()); @@ -642,7 +643,7 @@ await Page.RouteAsync("**/cars*", (route) => method: 'POST', headers: { 'Content-Type': 'application/json' }, mode: 'cors', - body: JSON.stringify({ 'number': 1 }) + body: JSON.stringify({ 'number': 1 }) }); return response.json(); }"); @@ -670,7 +671,7 @@ await Page.RouteAsync("**/cars*", (route) => method: 'POST', headers: { 'Content-Type': 'application/json' }, mode: 'cors', - body: JSON.stringify({ 'number': 1 }) + body: JSON.stringify({ 'number': 1 }) }); return response.json(); }"); @@ -682,7 +683,7 @@ await Page.RouteAsync("**/cars*", (route) => method: 'DELETE', headers: { 'Content-Type': 'application/json' }, mode: 'cors', - body: JSON.stringify({ 'number': 1 }) + body: JSON.stringify({ 'number': 1 }) }); return response.json(); }"); diff --git a/src/Playwright/API/Generated/IBrowserContext.cs b/src/Playwright/API/Generated/IBrowserContext.cs index e25a4f7662..0c95b6c232 100644 --- a/src/Playwright/API/Generated/IBrowserContext.cs +++ b/src/Playwright/API/Generated/IBrowserContext.cs @@ -333,7 +333,24 @@ public partial interface IBrowserContext /// /// /// Optional list of URLs. - Task> CookiesAsync(IEnumerable? urls = default); + Task> CookiesAsync(string urls); + + /// + /// + /// If no URLs are specified, this method returns all cookies. If URLs are specified, + /// only cookies that affect those URLs are returned. + /// + /// + /// Optional list of URLs. + Task> CookiesAsync(IEnumerable urls); + + /// + /// + /// If no URLs are specified, this method returns all cookies. If URLs are specified, + /// only cookies that affect those URLs are returned. + /// + /// + Task> CookiesAsync(); /// /// @@ -556,9 +573,10 @@ public partial interface IBrowserContext /// Enabling routing disables http cache. /// /// - /// A glob pattern, regex pattern or predicate receiving to match - /// while routing. When a via the context options - /// was provided and the passed URL is a path, it gets merged via the new + /// A glob pattern, regex pattern, or predicate that receives a to + /// match during routing. If is set in the context + /// options and the provided URL is a string that does not start with *, it is + /// resolved using the new /// URL() constructor. /// /// handler function to route the request. @@ -624,9 +642,10 @@ public partial interface IBrowserContext /// Enabling routing disables http cache. /// /// - /// A glob pattern, regex pattern or predicate receiving to match - /// while routing. When a via the context options - /// was provided and the passed URL is a path, it gets merged via the new + /// A glob pattern, regex pattern, or predicate that receives a to + /// match during routing. If is set in the context + /// options and the provided URL is a string that does not start with *, it is + /// resolved using the new /// URL() constructor. /// /// handler function to route the request. @@ -692,9 +711,10 @@ public partial interface IBrowserContext /// Enabling routing disables http cache. /// /// - /// A glob pattern, regex pattern or predicate receiving to match - /// while routing. When a via the context options - /// was provided and the passed URL is a path, it gets merged via the new + /// A glob pattern, regex pattern, or predicate that receives a to + /// match during routing. If is set in the context + /// options and the provided URL is a string that does not start with *, it is + /// resolved using the new /// URL() constructor. /// /// handler function to route the request. diff --git a/src/Playwright/API/Generated/IBrowserType.cs b/src/Playwright/API/Generated/IBrowserType.cs index 8bc1d07f45..cba2009178 100644 --- a/src/Playwright/API/Generated/IBrowserType.cs +++ b/src/Playwright/API/Generated/IBrowserType.cs @@ -169,11 +169,13 @@ public partial interface IBrowserType /// /// /// Path to a User Data Directory, which stores browser session data like cookies and - /// local storage. More details for Chromium - /// and Firefox. - /// Note that Chromium's user data directory is the **parent** directory of the "Profile - /// Path" seen at chrome://version. Pass an empty string to use a temporary directory - /// instead. + /// local storage. Pass an empty string to create a temporary directory. + /// More details for Chromium + /// and Firefox. + /// Chromium's user data directory is the **parent** directory of the "Profile Path" + /// seen at chrome://version. + /// Note that browsers do not allow launching multiple instances with the same User + /// Data Directory. /// /// Call options Task LaunchPersistentContextAsync(string userDataDir, BrowserTypeLaunchPersistentContextOptions? options = default); diff --git a/src/Playwright/API/Generated/ILocatorAssertions.cs b/src/Playwright/API/Generated/ILocatorAssertions.cs index 22427b8a83..e27d2bbe77 100644 --- a/src/Playwright/API/Generated/ILocatorAssertions.cs +++ b/src/Playwright/API/Generated/ILocatorAssertions.cs @@ -212,6 +212,68 @@ public partial interface ILocatorAssertions /// Call options Task ToBeVisibleAsync(LocatorAssertionsToBeVisibleOptions? options = default); + /// + /// + /// Ensures the points to an element with given CSS classes. + /// All classes from the asserted value, separated by spaces, must be present in the + /// Element.classList + /// in any order. + /// + /// **Usage** + /// + /// var locator = Page.Locator("#component");
+ /// await Expect(locator).ToContainClassAsync("middle selected row");
+ /// await Expect(locator).ToContainClassAsync("selected");
+ /// await Expect(locator).ToContainClassAsync("row middle"); + ///
+ /// + /// When an array is passed, the method asserts that the list of elements located matches + /// the corresponding list of expected class lists. Each element's class attribute is + /// matched against the corresponding class in the array: + /// + /// + /// var locator = Page.Locator("list > .component");
+ /// await Expect(locator).ToContainClassAsync(new string[]{"inactive", "active", "inactive"}); + ///
+ ///
+ /// + /// A string containing expected class names, separated by spaces, or a list of such + /// strings to assert multiple elements. + /// + /// Call options + Task ToContainClassAsync(string expected, LocatorAssertionsToContainClassOptions? options = default); + + /// + /// + /// Ensures the points to an element with given CSS classes. + /// All classes from the asserted value, separated by spaces, must be present in the + /// Element.classList + /// in any order. + /// + /// **Usage** + /// + /// var locator = Page.Locator("#component");
+ /// await Expect(locator).ToContainClassAsync("middle selected row");
+ /// await Expect(locator).ToContainClassAsync("selected");
+ /// await Expect(locator).ToContainClassAsync("row middle"); + ///
+ /// + /// When an array is passed, the method asserts that the list of elements located matches + /// the corresponding list of expected class lists. Each element's class attribute is + /// matched against the corresponding class in the array: + /// + /// + /// var locator = Page.Locator("list > .component");
+ /// await Expect(locator).ToContainClassAsync(new string[]{"inactive", "active", "inactive"}); + ///
+ ///
+ /// + /// A string containing expected class names, separated by spaces, or a list of such + /// strings to assert multiple elements. + /// + /// Call options + Task ToContainClassAsync(IEnumerable expected, LocatorAssertionsToContainClassOptions? options = default); + /// /// /// Ensures the points to an element that contains the given @@ -512,13 +574,13 @@ public partial interface ILocatorAssertions /// /// Ensures the points to an element with given CSS classes. /// When a string is provided, it must fully match the element's class attribute. - /// To match individual classes or perform partial matches, use a regular expression: + /// To match individual classes use . /// /// **Usage** /// /// var locator = Page.Locator("#component");
- /// await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)"));
- /// await Expect(locator).ToHaveClassAsync("middle selected row"); + /// await Expect(locator).ToHaveClassAsync("middle selected row");
+ /// await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)")); ///
/// /// When an array is passed, the method asserts that the list of elements located matches @@ -538,13 +600,13 @@ public partial interface ILocatorAssertions /// /// Ensures the points to an element with given CSS classes. /// When a string is provided, it must fully match the element's class attribute. - /// To match individual classes or perform partial matches, use a regular expression: + /// To match individual classes use . /// /// **Usage** /// /// var locator = Page.Locator("#component");
- /// await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)"));
- /// await Expect(locator).ToHaveClassAsync("middle selected row"); + /// await Expect(locator).ToHaveClassAsync("middle selected row");
+ /// await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)")); ///
/// /// When an array is passed, the method asserts that the list of elements located matches @@ -564,13 +626,13 @@ public partial interface ILocatorAssertions /// /// Ensures the points to an element with given CSS classes. /// When a string is provided, it must fully match the element's class attribute. - /// To match individual classes or perform partial matches, use a regular expression: + /// To match individual classes use . /// /// **Usage** /// /// var locator = Page.Locator("#component");
- /// await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)"));
- /// await Expect(locator).ToHaveClassAsync("middle selected row"); + /// await Expect(locator).ToHaveClassAsync("middle selected row");
+ /// await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)")); ///
/// /// When an array is passed, the method asserts that the list of elements located matches @@ -590,13 +652,13 @@ public partial interface ILocatorAssertions /// /// Ensures the points to an element with given CSS classes. /// When a string is provided, it must fully match the element's class attribute. - /// To match individual classes or perform partial matches, use a regular expression: + /// To match individual classes use . /// /// **Usage** /// /// var locator = Page.Locator("#component");
- /// await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)"));
- /// await Expect(locator).ToHaveClassAsync("middle selected row"); + /// await Expect(locator).ToHaveClassAsync("middle selected row");
+ /// await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)")); ///
/// /// When an array is passed, the method asserts that the list of elements located matches diff --git a/src/Playwright/API/Generated/IPage.cs b/src/Playwright/API/Generated/IPage.cs index 269901453c..4f6758b258 100644 --- a/src/Playwright/API/Generated/IPage.cs +++ b/src/Playwright/API/Generated/IPage.cs @@ -1986,9 +1986,10 @@ public partial interface IPage /// Enabling routing disables http cache. /// /// - /// A glob pattern, regex pattern or predicate receiving to match - /// while routing. When a via the context options - /// was provided and the passed URL is a path, it gets merged via the new + /// A glob pattern, regex pattern, or predicate that receives a to + /// match during routing. If is set in the context + /// options and the provided URL is a string that does not start with *, it is + /// resolved using the new /// URL() constructor. /// /// handler function to route the request. @@ -2063,9 +2064,10 @@ public partial interface IPage /// Enabling routing disables http cache. /// /// - /// A glob pattern, regex pattern or predicate receiving to match - /// while routing. When a via the context options - /// was provided and the passed URL is a path, it gets merged via the new + /// A glob pattern, regex pattern, or predicate that receives a to + /// match during routing. If is set in the context + /// options and the provided URL is a string that does not start with *, it is + /// resolved using the new /// URL() constructor. /// /// handler function to route the request. @@ -2140,9 +2142,10 @@ public partial interface IPage /// Enabling routing disables http cache. /// /// - /// A glob pattern, regex pattern or predicate receiving to match - /// while routing. When a via the context options - /// was provided and the passed URL is a path, it gets merged via the new + /// A glob pattern, regex pattern, or predicate that receives a to + /// match during routing. If is set in the context + /// options and the provided URL is a string that does not start with *, it is + /// resolved using the new /// URL() constructor. /// /// handler function to route the request. diff --git a/src/Playwright/API/Generated/IRoute.cs b/src/Playwright/API/Generated/IRoute.cs index cac163b2a2..15eff2caec 100644 --- a/src/Playwright/API/Generated/IRoute.cs +++ b/src/Playwright/API/Generated/IRoute.cs @@ -95,7 +95,19 @@ public partial interface IRoute /// other matching handlers won't be invoked. Use /// If you want next matching handler in the chain to be invoked. /// + /// + /// The Cookie header cannot be overridden using this method. If a value is provided, + /// it will be ignored, and the cookie will be loaded from the browser's cookie store. + /// To set custom cookies, use . + /// ///
+ /// + /// + /// The Cookie header cannot be overridden using this method. If a value is provided, + /// it will be ignored, and the cookie will be loaded from the browser's cookie store. + /// To set custom cookies, use . + /// + /// /// Call options Task ContinueAsync(RouteContinueOptions? options = default); diff --git a/src/Playwright/API/Generated/Options/APIRequestNewContextOptions.cs b/src/Playwright/API/Generated/Options/APIRequestNewContextOptions.cs index 9cef02540f..c8ff4a6de9 100644 --- a/src/Playwright/API/Generated/Options/APIRequestNewContextOptions.cs +++ b/src/Playwright/API/Generated/Options/APIRequestNewContextOptions.cs @@ -46,6 +46,7 @@ public APIRequestNewContextOptions(APIRequestNewContextOptions clone) FailOnStatusCode = clone.FailOnStatusCode; HttpCredentials = clone.HttpCredentials; IgnoreHTTPSErrors = clone.IgnoreHTTPSErrors; + MaxRedirects = clone.MaxRedirects; Proxy = clone.Proxy; StorageState = clone.StorageState; StorageStatePath = clone.StorageStatePath; @@ -138,6 +139,16 @@ public APIRequestNewContextOptions(APIRequestNewContextOptions clone) [JsonPropertyName("ignoreHTTPSErrors")] public bool? IgnoreHTTPSErrors { get; set; } + /// + /// + /// Maximum number of request redirects that will be followed automatically. An error + /// will be thrown if the number is exceeded. Defaults to 20. Pass 0 to + /// not follow redirects. This can be overwritten for each request individually. + /// + /// + [JsonPropertyName("maxRedirects")] + public int? MaxRedirects { get; set; } + /// Network proxy settings. [JsonPropertyName("proxy")] public Proxy? Proxy { get; set; } diff --git a/src/Playwright/API/Generated/Options/BrowserContextStorageStateOptions.cs b/src/Playwright/API/Generated/Options/BrowserContextStorageStateOptions.cs index bfd613c398..14ee24f8e5 100644 --- a/src/Playwright/API/Generated/Options/BrowserContextStorageStateOptions.cs +++ b/src/Playwright/API/Generated/Options/BrowserContextStorageStateOptions.cs @@ -49,9 +49,7 @@ public BrowserContextStorageStateOptions(BrowserContextStorageStateOptions clone /// in the storage state snapshot. If your application uses IndexedDB to store authentication /// tokens, like Firebase Authentication, enable this. /// - /// IndexedDBs with typed arrays are currently not supported. /// - /// IndexedDBs with typed arrays are currently not supported. [JsonPropertyName("indexedDB")] public bool? IndexedDB { get; set; } diff --git a/src/Playwright/API/Generated/Options/BrowserTypeLaunchOptions.cs b/src/Playwright/API/Generated/Options/BrowserTypeLaunchOptions.cs index d4c62ea89c..5ee06643c9 100644 --- a/src/Playwright/API/Generated/Options/BrowserTypeLaunchOptions.cs +++ b/src/Playwright/API/Generated/Options/BrowserTypeLaunchOptions.cs @@ -155,7 +155,7 @@ public BrowserTypeLaunchOptions(BrowserTypeLaunchOptions clone) /// /// /// Whether to run browser in headless mode. More details for Chromium - /// and Firefox. + /// and Firefox. /// Defaults to true unless the option /// is true. /// diff --git a/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs b/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs index 14d6d6c3a9..06679f887e 100644 --- a/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs +++ b/src/Playwright/API/Generated/Options/BrowserTypeLaunchPersistentContextOptions.cs @@ -319,7 +319,7 @@ public BrowserTypeLaunchPersistentContextOptions(BrowserTypeLaunchPersistentCont /// /// /// Whether to run browser in headless mode. More details for Chromium - /// and Firefox. + /// and Firefox. /// Defaults to true unless the option /// is true. /// diff --git a/src/Playwright/API/Generated/Options/LocatorAriaSnapshotOptions.cs b/src/Playwright/API/Generated/Options/LocatorAriaSnapshotOptions.cs index db38e71f80..7b12310f87 100644 --- a/src/Playwright/API/Generated/Options/LocatorAriaSnapshotOptions.cs +++ b/src/Playwright/API/Generated/Options/LocatorAriaSnapshotOptions.cs @@ -39,9 +39,19 @@ public LocatorAriaSnapshotOptions(LocatorAriaSnapshotOptions clone) return; } + Ref = clone.Ref; Timeout = clone.Timeout; } + /// + /// + /// Generate symbolic reference for each element. One can use aria-ref=<ref> + /// locator immediately after capturing the snapshot to perform actions on the element. + /// + /// + [JsonPropertyName("ref")] + public bool? Ref { get; set; } + /// /// /// Maximum time in milliseconds. Defaults to 30000 (30 seconds). Pass 0 diff --git a/src/Playwright/API/Generated/Options/LocatorAssertionsToContainClassOptions.cs b/src/Playwright/API/Generated/Options/LocatorAssertionsToContainClassOptions.cs new file mode 100644 index 0000000000..588f6069ad --- /dev/null +++ b/src/Playwright/API/Generated/Options/LocatorAssertionsToContainClassOptions.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 LocatorAssertionsToContainClassOptions +{ + public LocatorAssertionsToContainClassOptions() { } + + public LocatorAssertionsToContainClassOptions(LocatorAssertionsToContainClassOptions 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/LocatorEvaluateHandleOptions.cs b/src/Playwright/API/Generated/Options/LocatorEvaluateHandleOptions.cs index c8f6483e89..8feccfe1fe 100644 --- a/src/Playwright/API/Generated/Options/LocatorEvaluateHandleOptions.cs +++ b/src/Playwright/API/Generated/Options/LocatorEvaluateHandleOptions.cs @@ -44,9 +44,9 @@ public LocatorEvaluateHandleOptions(LocatorEvaluateHandleOptions clone) /// /// - /// Maximum time in milliseconds. Defaults to 30000 (30 seconds). Pass 0 - /// to disable timeout. The default value can be changed by using the - /// or methods. + /// Maximum time in milliseconds to wait for the locator before evaluating. Note that + /// after locator is resolved, evaluation itself is not limited by the timeout. Defaults + /// to 30000 (30 seconds). Pass 0 to disable timeout. /// /// [JsonPropertyName("timeout")] diff --git a/src/Playwright/API/Generated/Options/LocatorEvaluateOptions.cs b/src/Playwright/API/Generated/Options/LocatorEvaluateOptions.cs index 5dfd27cae4..b812f7bf59 100644 --- a/src/Playwright/API/Generated/Options/LocatorEvaluateOptions.cs +++ b/src/Playwright/API/Generated/Options/LocatorEvaluateOptions.cs @@ -44,9 +44,9 @@ public LocatorEvaluateOptions(LocatorEvaluateOptions clone) /// /// - /// Maximum time in milliseconds. Defaults to 30000 (30 seconds). Pass 0 - /// to disable timeout. The default value can be changed by using the - /// or methods. + /// Maximum time in milliseconds to wait for the locator before evaluating. Note that + /// after locator is resolved, evaluation itself is not limited by the timeout. Defaults + /// to 30000 (30 seconds). Pass 0 to disable timeout. /// /// [JsonPropertyName("timeout")] diff --git a/src/Playwright/Core/APIRequest.cs b/src/Playwright/Core/APIRequest.cs index e97ac4a7c0..49f3afe74e 100644 --- a/src/Playwright/Core/APIRequest.cs +++ b/src/Playwright/Core/APIRequest.cs @@ -48,6 +48,7 @@ async Task IAPIRequest.NewContextAsync(APIRequestNewContextO ["ignoreHTTPSErrors"] = options?.IgnoreHTTPSErrors, ["extraHTTPHeaders"] = options?.ExtraHTTPHeaders?.ToProtocol(), ["httpCredentials"] = options?.HttpCredentials, + ["maxRedirects"] = options?.MaxRedirects, ["proxy"] = options?.Proxy, ["timeout"] = options?.Timeout, ["clientCertificates"] = Browser.ToClientCertificatesProtocol(options?.ClientCertificates), diff --git a/src/Playwright/Core/BrowserContext.cs b/src/Playwright/Core/BrowserContext.cs index 8fecf4d8e9..748ec4f2a0 100644 --- a/src/Playwright/Core/BrowserContext.cs +++ b/src/Playwright/Core/BrowserContext.cs @@ -376,11 +376,17 @@ internal void SetOptions(BrowserNewContextOptions contextOptions, string tracesD } [MethodImpl(MethodImplOptions.NoInlining)] - public async Task> CookiesAsync(IEnumerable urls = null) => (await SendMessageToServerAsync( + public Task> CookiesAsync() => CookiesAsync(Array.Empty()); + + [MethodImpl(MethodImplOptions.NoInlining)] + public Task> CookiesAsync(string url) => CookiesAsync(new string[] { url }); + + [MethodImpl(MethodImplOptions.NoInlining)] + public async Task> CookiesAsync(IEnumerable urls) => (await SendMessageToServerAsync( "cookies", new Dictionary { - ["urls"] = urls?.ToArray() ?? Array.Empty(), + ["urls"] = urls.ToArray(), }).ConfigureAwait(false))?.GetProperty("cookies").ToObject>(); [MethodImpl(MethodImplOptions.NoInlining)] diff --git a/src/Playwright/Core/Locator.cs b/src/Playwright/Core/Locator.cs index bddfd21492..c79f47459d 100644 --- a/src/Playwright/Core/Locator.cs +++ b/src/Playwright/Core/Locator.cs @@ -609,6 +609,7 @@ public async Task AriaSnapshotAsync(LocatorAriaSnapshotOptions options = { ["selector"] = _selector, ["timeout"] = options?.Timeout, + ["ref"] = options?.Ref, }).ConfigureAwait(false); return result.Value.GetProperty("snapshot").GetString(); } diff --git a/src/Playwright/Core/LocatorAssertions.cs b/src/Playwright/Core/LocatorAssertions.cs index da59bec72c..4344920658 100644 --- a/src/Playwright/Core/LocatorAssertions.cs +++ b/src/Playwright/Core/LocatorAssertions.cs @@ -147,6 +147,12 @@ public Task ToHaveClassAsync(IEnumerable expected, LocatorAssertionsToHa public Task ToHaveClassAsync(IEnumerable expected, LocatorAssertionsToHaveClassOptions options = null) => ExpectImplAsync("to.have.class.array", expected.Select(regex => ExpectedRegex(regex)).ToArray(), expected, "Locator expected to have class matching regex", ConvertToFrameExpectOptions(options)); + public Task ToContainClassAsync(string expected, LocatorAssertionsToContainClassOptions options = null) => + ExpectImplAsync("to.contain.class", new ExpectedTextValue() { String = expected }, expected, "Locator expected to contain class names", ConvertToFrameExpectOptions(options)); + + public Task ToContainClassAsync(IEnumerable expected, LocatorAssertionsToContainClassOptions options = null) => + ExpectImplAsync("to.contain.class.array", expected.Select(text => new ExpectedTextValue() { String = text }).ToArray(), expected, "Locator expected to contain class names", ConvertToFrameExpectOptions(options)); + public Task ToHaveCountAsync(int count, LocatorAssertionsToHaveCountOptions options = null) { var commonOptions = ConvertToFrameExpectOptions(options); diff --git a/src/Playwright/Transport/Converters/EvaluateArgumentValueConverter.cs b/src/Playwright/Transport/Converters/EvaluateArgumentValueConverter.cs index aed915e39a..a07c0bd456 100644 --- a/src/Playwright/Transport/Converters/EvaluateArgumentValueConverter.cs +++ b/src/Playwright/Transport/Converters/EvaluateArgumentValueConverter.cs @@ -353,6 +353,26 @@ private static object ParseEvaluateResultToExpando(JsonElement result, IDictiona return new Regex(regex.GetProperty("p").ToString(), RegexOptionsExtensions.FromInlineFlags(regex.GetProperty("f").ToString())); } + if (result.TryGetProperty("ta", out var ta)) + { + byte[] bytes = Convert.FromBase64String(ta.GetProperty("b").ToString()); + return ta.GetProperty("k").ToString() switch + { + "i8" => bytes.Select(b => unchecked((sbyte)b)).ToArray(), + "ui8" => bytes, + "ui8c" => bytes, + "i16" => Enumerable.Range(0, bytes.Length / 2).Select(i => BitConverter.ToInt16(bytes, i * 2)).ToArray(), + "ui16" => Enumerable.Range(0, bytes.Length / 2).Select(i => BitConverter.ToUInt16(bytes, i * 2)).ToArray(), + "i32" => Enumerable.Range(0, bytes.Length / 4).Select(i => BitConverter.ToInt32(bytes, i * 4)).ToArray(), + "ui32" => Enumerable.Range(0, bytes.Length / 4).Select(i => BitConverter.ToUInt32(bytes, i * 4)).ToArray(), + "f32" => Enumerable.Range(0, bytes.Length / 4).Select(i => BitConverter.ToSingle(bytes, i * 4)).ToArray(), + "f64" => Enumerable.Range(0, bytes.Length / 8).Select(i => BitConverter.ToDouble(bytes, i * 8)).ToArray(), + "bi64" => Enumerable.Range(0, bytes.Length / 8).Select(i => BitConverter.ToInt64(bytes, i * 8)).ToArray(), + "bui64" => Enumerable.Range(0, bytes.Length / 8).Select(i => BitConverter.ToUInt64(bytes, i * 8)).ToArray(), + _ => null, + }; + } + if (result.TryGetProperty("b", out var boolean)) { return boolean.ToObject(); diff --git a/src/Playwright/Transport/Protocol/Generated/SerializedValue.cs b/src/Playwright/Transport/Protocol/Generated/SerializedValue.cs index 36f3369760..522db76e55 100644 --- a/src/Playwright/Transport/Protocol/Generated/SerializedValue.cs +++ b/src/Playwright/Transport/Protocol/Generated/SerializedValue.cs @@ -50,6 +50,9 @@ internal class SerializedValue [JsonPropertyName("bi")] public string Bi { get; set; } + [JsonPropertyName("ta")] + public SerializedValueTa Ta { get; set; } + [JsonPropertyName("e")] public SerializedValueE E { get; set; } diff --git a/src/Playwright/Transport/Protocol/Generated/SerializedValueTa.cs b/src/Playwright/Transport/Protocol/Generated/SerializedValueTa.cs new file mode 100644 index 0000000000..044d716bb2 --- /dev/null +++ b/src/Playwright/Transport/Protocol/Generated/SerializedValueTa.cs @@ -0,0 +1,36 @@ +/* + * 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; + +namespace Microsoft.Playwright.Transport.Protocol; + +internal class SerializedValueTa +{ + [JsonPropertyName("b")] + public byte[] B { get; set; } + + [JsonPropertyName("k")] + public string K { get; set; } +}