From cfc42ab147a4b059efe8c4a9a952396ad62141e2 Mon Sep 17 00:00:00 2001 From: Kurt Ruppel Date: Thu, 25 Jun 2026 12:55:17 +0000 Subject: [PATCH 1/4] Update Playwright driver to 1.61.1 Ports Frame#expect to the 1.61 expect protocol: the command now resolves on a successful match and rejects with FrameExpectErrorDetails on mismatch/timeout (upstream microsoft/playwright#40801). Carry errorDetails on Playwright::Error and translate it back into the assertion result hash. --- .circleci/config.yml | 4 +- development/CLI_VERSION | 2 +- development/api.json | 1813 +++++++++++++++-- development/generate_api/example_codes.rb | 21 +- documentation/docs/api/browser_type.md | 1 + documentation/docs/api/frame.md | 8 +- documentation/docs/api/page.md | 4 +- documentation/docs/api/touchscreen.md | 2 +- documentation/docs/include/api_coverage.md | 5 + lib/playwright/channel_owners/browser_type.rb | 3 +- lib/playwright/channel_owners/frame.rb | 53 +- lib/playwright/connection.rb | 1 + lib/playwright/errors.rb | 5 +- lib/playwright/version.rb | 4 +- 14 files changed, 1752 insertions(+), 174 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7ce9e23f..e8824e42 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,8 +4,8 @@ jobs: test-remote: docker: - image: cimg/ruby:3.4 - - image: mcr.microsoft.com/playwright:v1.60.0-noble - command: /bin/sh -c "cd /home/pwuser ; npx --yes playwright@1.60.0 install && npx playwright@1.60.0 run-server --port 8888 --host 0.0.0.0 --path /ws" + - image: mcr.microsoft.com/playwright:v1.61.1-noble + command: /bin/sh -c "cd /home/pwuser ; npx --yes playwright@1.61.1 install && npx playwright@1.61.1 run-server --port 8888 --host 0.0.0.0 --path /ws" environment: DEBUG: "pw:*" user: pwuser diff --git a/development/CLI_VERSION b/development/CLI_VERSION index 4d5fde5b..30e298c7 100644 --- a/development/CLI_VERSION +++ b/development/CLI_VERSION @@ -1 +1 @@ -1.60.0 +1.61.1 diff --git a/development/api.json b/development/api.json index c3c06616..57b87d18 100644 --- a/development/api.json +++ b/development/api.json @@ -257,7 +257,7 @@ } ], "langAliases": { - "java": "ClientCertificate" + "default": "ClientCertificate" } } ], @@ -450,7 +450,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "HttpCredentials" + "default": "HttpCredentials" } }, "spec": [ @@ -602,7 +602,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Proxy" + "default": "Proxy" } }, "spec": [ @@ -9681,8 +9681,8 @@ } ], "langAliases": { - "csharp": "Header", - "java": "HttpHeader" + "default": "HttpHeader", + "csharp": "Header" } } ], @@ -9798,6 +9798,225 @@ "overloadIndex": 0, "args": [] }, + { + "kind": "method", + "langs": {}, + "since": "v1.61", + "name": "securityDetails", + "type": { + "name": "", + "union": [ + { + "name": "null" + }, + { + "name": "Object", + "properties": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "issuer", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Common Name component of the Issuer field.↵from the certificate. This should only be used for informational purposes. Optional." + } + ], + "required": false, + "comment": "Common Name component of the Issuer field. from the certificate. This should only be used for informational\npurposes. Optional.", + "async": false, + "alias": "issuer", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "protocol", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "The specific TLS protocol used. (e.g. `TLS 1.3`). Optional." + } + ], + "required": false, + "comment": "The specific TLS protocol used. (e.g. `TLS 1.3`). Optional.", + "async": false, + "alias": "protocol", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "subjectName", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Common Name component of the Subject↵field from the certificate. This should only be used for informational purposes. Optional." + } + ], + "required": false, + "comment": "Common Name component of the Subject field from the certificate. This should only be used for informational\npurposes. Optional.", + "async": false, + "alias": "subjectName", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "validFrom", + "type": { + "name": "float", + "expression": "[float]" + }, + "spec": [ + { + "type": "text", + "text": "Unix timestamp (in seconds) specifying↵when this cert becomes valid. Optional." + } + ], + "required": false, + "comment": "Unix timestamp (in seconds) specifying when this cert becomes valid. Optional.", + "async": false, + "alias": "validFrom", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "validTo", + "type": { + "name": "float", + "expression": "[float]" + }, + "spec": [ + { + "type": "text", + "text": "Unix timestamp (in seconds) specifying↵when this cert becomes invalid. Optional." + } + ], + "required": false, + "comment": "Unix timestamp (in seconds) specifying when this cert becomes invalid. Optional.", + "async": false, + "alias": "validTo", + "overloadIndex": 0 + } + ], + "langAliases": { + "default": "SecurityDetails", + "csharp": "ResponseSecurityDetailsResult" + } + } + ], + "expression": "[null]|[Object]" + }, + "spec": [ + { + "type": "text", + "text": "Returns SSL and other security information. Resolves to `null` for non-HTTPS responses. For redirected requests, returns the information for the last request in the redirect chain." + } + ], + "required": true, + "comment": "Returns SSL and other security information. Resolves to `null` for non-HTTPS responses. For redirected requests,\nreturns the information for the last request in the redirect chain.", + "async": true, + "alias": "securityDetails", + "overloadIndex": 0, + "args": [] + }, + { + "kind": "method", + "langs": {}, + "since": "v1.61", + "name": "serverAddr", + "type": { + "name": "", + "union": [ + { + "name": "null" + }, + { + "name": "Object", + "properties": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "ipAddress", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "IPv4 or IPV6 address of the server." + } + ], + "required": true, + "comment": "IPv4 or IPV6 address of the server.", + "async": false, + "alias": "ipAddress", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "port", + "type": { + "name": "int", + "expression": "[int]" + }, + "spec": [ + { + "type": "text", + "text": "" + } + ], + "required": true, + "comment": "", + "async": false, + "alias": "port", + "overloadIndex": 0 + } + ], + "langAliases": { + "csharp": "ResponseServerAddrResult", + "java": "ServerAddr" + } + } + ], + "expression": "[null]|[Object]" + }, + "spec": [ + { + "type": "text", + "text": "Returns the IP address and port of the server. Resolves to `null` if the server address is not available. For redirected requests, returns the information for the last request in the redirect chain." + } + ], + "required": true, + "comment": "Returns the IP address and port of the server. Resolves to `null` if the server address is not available. For\nredirected requests, returns the information for the last request in the redirect chain.", + "async": true, + "alias": "serverAddr", + "overloadIndex": 0, + "args": [] + }, { "kind": "method", "langs": {}, @@ -10910,7 +11129,7 @@ } ], "langAliases": { - "java": "ClientCertificate" + "default": "ClientCertificate" } } ], @@ -11335,7 +11554,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Geolocation" + "default": "Geolocation" } }, "spec": [], @@ -11469,7 +11688,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "HttpCredentials" + "default": "HttpCredentials" } }, "spec": [ @@ -11767,7 +11986,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Proxy" + "default": "Proxy" } }, "spec": [ @@ -12213,10 +12432,7 @@ "overloadIndex": 0 } ], - "expression": "[Object]", - "langAliases": { - "csharp": "RecordVideoSize" - } + "expression": "[Object]" }, "spec": [ { @@ -12322,10 +12538,7 @@ "overloadIndex": 0 } ], - "expression": "[Object]", - "langAliases": { - "csharp": "ShowActionsOptions" - } + "expression": "[Object]" }, "spec": [ { @@ -12610,7 +12823,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "ScreenSize" + "default": "ScreenSize" } }, "spec": [ @@ -13222,7 +13435,7 @@ } ], "langAliases": { - "java": "ViewportSize" + "default": "ViewportSize" } } ], @@ -13724,7 +13937,7 @@ } ], "langAliases": { - "java": "ClientCertificate" + "default": "ClientCertificate" } } ], @@ -14149,7 +14362,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Geolocation" + "default": "Geolocation" } }, "spec": [], @@ -14283,7 +14496,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "HttpCredentials" + "default": "HttpCredentials" } }, "spec": [ @@ -14581,7 +14794,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Proxy" + "default": "Proxy" } }, "spec": [ @@ -15027,10 +15240,7 @@ "overloadIndex": 0 } ], - "expression": "[Object]", - "langAliases": { - "csharp": "RecordVideoSize" - } + "expression": "[Object]" }, "spec": [ { @@ -15136,10 +15346,7 @@ "overloadIndex": 0 } ], - "expression": "[Object]", - "langAliases": { - "csharp": "ShowActionsOptions" - } + "expression": "[Object]" }, "spec": [ { @@ -15424,7 +15631,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "ScreenSize" + "default": "ScreenSize" } }, "spec": [ @@ -16036,7 +16243,7 @@ } ], "langAliases": { - "java": "ViewportSize" + "default": "ViewportSize" } } ], @@ -16278,7 +16485,8 @@ ], "expression": "[Object]", "langAliases": { - "default": "BindResult" + "default": "BindResult", + "csharp": "BrowserBindResult" } }, "spec": [ @@ -16951,6 +17159,28 @@ "overloadIndex": 0, "args": [] }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "credentials", + "type": { + "name": "Credentials", + "expression": "[Credentials]" + }, + "spec": [ + { + "type": "text", + "text": "Virtual WebAuthn authenticator for this context. Lets tests seed credentials and intercept↵`navigator.credentials.create()` / `navigator.credentials.get()` ceremonies." + } + ], + "required": true, + "comment": "Virtual WebAuthn authenticator for this context. Lets tests seed credentials and intercept\n`navigator.credentials.create()` / `navigator.credentials.get()` ceremonies.", + "async": false, + "alias": "credentials", + "overloadIndex": 0, + "args": [] + }, { "kind": "property", "langs": {}, @@ -18783,8 +19013,8 @@ } ], "langAliases": { - "csharp": "BrowserContextCookiesResult", - "java": "Cookie" + "default": "Cookie", + "csharp": "BrowserContextCookiesResult" } } ], @@ -19035,7 +19265,7 @@ "name": "function", "expression": "[function]", "langAliases": { - "java": "BindingCallback" + "default": "BindingCallback" } }, "spec": [ @@ -19297,7 +19527,7 @@ "name": "function", "expression": "[function]", "langAliases": { - "java": "FunctionCallback" + "default": "FunctionCallback" } }, "spec": [ @@ -21079,7 +21309,7 @@ } ], "langAliases": { - "java": "Geolocation" + "default": "Geolocation" } } ], @@ -23870,6 +24100,27 @@ "type": { "name": "Object", "properties": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "artifactsDir", + "type": { + "name": "path", + "expression": "[path]" + }, + "spec": [ + { + "type": "text", + "text": "If specified, browser artifacts (such as traces and downloads) are saved into this directory." + } + ], + "required": false, + "comment": "If specified, browser artifacts (such as traces and downloads) are saved into this directory.", + "async": false, + "alias": "artifactsDir", + "overloadIndex": 0 + }, { "kind": "property", "langs": { @@ -24819,7 +25070,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Proxy" + "default": "Proxy" } }, "spec": [ @@ -25345,7 +25596,7 @@ } ], "langAliases": { - "java": "ClientCertificate" + "default": "ClientCertificate" } } ], @@ -26028,7 +26279,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Geolocation" + "default": "Geolocation" } }, "spec": [], @@ -26246,7 +26497,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "HttpCredentials" + "default": "HttpCredentials" } }, "spec": [ @@ -26649,7 +26900,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Proxy" + "default": "Proxy" } }, "spec": [ @@ -27095,10 +27346,7 @@ "overloadIndex": 0 } ], - "expression": "[Object]", - "langAliases": { - "csharp": "RecordVideoSize" - } + "expression": "[Object]" }, "spec": [ { @@ -27204,10 +27452,7 @@ "overloadIndex": 0 } ], - "expression": "[Object]", - "langAliases": { - "csharp": "ShowActionsOptions" - } + "expression": "[Object]" }, "spec": [ { @@ -27492,7 +27737,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "ScreenSize" + "default": "ScreenSize" } }, "spec": [ @@ -27740,7 +27985,7 @@ } ], "langAliases": { - "java": "ViewportSize" + "default": "ViewportSize" } } ], @@ -28667,7 +28912,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Proxy" + "default": "Proxy" } }, "spec": [ @@ -31662,6 +31907,861 @@ } ] }, + { + "name": "Credentials", + "spec": [ + { + "type": "text", + "text": "`Credentials` is a virtual WebAuthn authenticator scoped to a `BrowserContext`. It lets tests↵register passkeys and answer `navigator.credentials.create()` / `navigator.credentials.get()`↵ceremonies in the page, without a real authenticator or hardware security key." + }, + { + "type": "text", + "text": "There are two common ways to use it:" + }, + { + "type": "text", + "text": "**Usage: seed a known credential**" + }, + { + "type": "code", + "lines": [ + "const context = await browser.newContext();", + "", + "// A passkey your backend already provisioned for a test user.", + "await context.credentials.create('example.com', {", + " id: knownCredentialId, // base64url", + " userHandle: knownUserHandle, // base64url", + " privateKey: knownPrivateKey, // base64url PKCS#8 (DER)", + " publicKey: knownPublicKey, // base64url SPKI (DER)", + "});", + "await context.credentials.install();", + "", + "const page = await context.newPage();", + "await page.goto('https://example.com/login');", + "// The page's navigator.credentials.get() is answered with the seeded passkey." + ], + "codeLang": "js" + }, + { + "type": "code", + "lines": [ + "BrowserContext context = browser.newContext();", + "", + "// A passkey your backend already provisioned for a test user.", + "context.credentials().create(\"example.com\", new Credentials.CreateOptions()", + " .setId(knownCredentialId) // base64url", + " .setUserHandle(knownUserHandle) // base64url", + " .setPrivateKey(knownPrivateKey) // base64url PKCS#8 (DER)", + " .setPublicKey(knownPublicKey)); // base64url SPKI (DER)", + "context.credentials().install();", + "", + "Page page = context.newPage();", + "page.navigate(\"https://example.com/login\");", + "// The page's navigator.credentials.get() is answered with the seeded passkey." + ], + "codeLang": "java" + }, + { + "type": "code", + "lines": [ + "context = await browser.new_context()", + "", + "# A passkey your backend already provisioned for a test user.", + "await context.credentials.create(", + " \"example.com\",", + " id=known_credential_id, # base64url", + " user_handle=known_user_handle, # base64url", + " private_key=known_private_key, # base64url PKCS#8 (DER)", + " public_key=known_public_key, # base64url SPKI (DER)", + ")", + "await context.credentials.install()", + "", + "page = await context.new_page()", + "await page.goto(\"https://example.com/login\")", + "# The page's navigator.credentials.get() is answered with the seeded passkey." + ], + "codeLang": "python async" + }, + { + "type": "code", + "lines": [ + "context = browser.new_context()", + "", + "# A passkey your backend already provisioned for a test user.", + "context.credentials.create(", + " \"example.com\",", + " id=known_credential_id, # base64url", + " user_handle=known_user_handle, # base64url", + " private_key=known_private_key, # base64url PKCS#8 (DER)", + " public_key=known_public_key, # base64url SPKI (DER)", + ")", + "context.credentials.install()", + "", + "page = context.new_page()", + "page.goto(\"https://example.com/login\")", + "# The page's navigator.credentials.get() is answered with the seeded passkey." + ], + "codeLang": "python sync" + }, + { + "type": "code", + "lines": [ + "var context = await browser.NewContextAsync();", + "", + "// A passkey your backend already provisioned for a test user.", + "await context.Credentials.CreateAsync(\"example.com\", new()", + "{", + " Id = knownCredentialId, // base64url", + " UserHandle = knownUserHandle, // base64url", + " PrivateKey = knownPrivateKey, // base64url PKCS#8 (DER)", + " PublicKey = knownPublicKey, // base64url SPKI (DER)", + "});", + "await context.Credentials.InstallAsync();", + "", + "var page = await context.NewPageAsync();", + "await page.GotoAsync(\"https://example.com/login\");", + "// The page's navigator.credentials.get() is answered with the seeded passkey." + ], + "codeLang": "csharp" + }, + { + "type": "text", + "text": "**Usage: capture a passkey, then reuse it**" + }, + { + "type": "code", + "lines": [ + "// setup test: let the app register a passkey, then save it.", + "const context = await browser.newContext();", + "await context.credentials.install();", + "", + "const page = await context.newPage();", + "await page.goto('https://example.com/register');", + "await page.getByRole('button', { name: 'Create a passkey' }).click();", + "", + "// Read back the passkey the page registered — it includes the private key.", + "const [credential] = await context.credentials.get({ rpId: 'example.com' });", + "fs.writeFileSync('playwright/.auth/passkey.json', JSON.stringify(credential));" + ], + "codeLang": "js" + }, + { + "type": "code", + "lines": [ + "// setup test: let the app register a passkey, then save it.", + "BrowserContext context = browser.newContext();", + "context.credentials().install();", + "", + "Page page = context.newPage();", + "page.navigate(\"https://example.com/register\");", + "page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(\"Create a passkey\")).click();", + "", + "// Read back the passkey the page registered — it includes the private key.", + "VirtualCredential credential = context.credentials().get(", + " new Credentials.GetOptions().setRpId(\"example.com\")).get(0);", + "Files.writeString(Paths.get(\"playwright/.auth/passkey.json\"), new Gson().toJson(credential));" + ], + "codeLang": "java" + }, + { + "type": "code", + "lines": [ + "# setup test: let the app register a passkey, then save it.", + "context = await browser.new_context()", + "await context.credentials.install()", + "", + "page = await context.new_page()", + "await page.goto(\"https://example.com/register\")", + "await page.get_by_role(\"button\", name=\"Create a passkey\").click()", + "", + "# Read back the passkey the page registered — it includes the private key.", + "[credential] = await context.credentials.get(rp_id=\"example.com\")", + "with open(\"playwright/.auth/passkey.json\", \"w\") as f:", + " json.dump(credential, f)" + ], + "codeLang": "python async" + }, + { + "type": "code", + "lines": [ + "# setup test: let the app register a passkey, then save it.", + "context = browser.new_context()", + "context.credentials.install()", + "", + "page = context.new_page()", + "page.goto(\"https://example.com/register\")", + "page.get_by_role(\"button\", name=\"Create a passkey\").click()", + "", + "# Read back the passkey the page registered — it includes the private key.", + "[credential] = context.credentials.get(rp_id=\"example.com\")", + "with open(\"playwright/.auth/passkey.json\", \"w\") as f:", + " json.dump(credential, f)" + ], + "codeLang": "python sync" + }, + { + "type": "code", + "lines": [ + "// setup test: let the app register a passkey, then save it.", + "var context = await browser.NewContextAsync();", + "await context.Credentials.InstallAsync();", + "", + "var page = await context.NewPageAsync();", + "await page.GotoAsync(\"https://example.com/register\");", + "await page.GetByRole(AriaRole.Button, new() { Name = \"Create a passkey\" }).ClickAsync();", + "", + "// Read back the passkey the page registered — it includes the private key.", + "var credentials = await context.Credentials.GetAsync(new() { RpId = \"example.com\" });", + "File.WriteAllText(\"playwright/.auth/passkey.json\", JsonSerializer.Serialize(credentials[0]));" + ], + "codeLang": "csharp" + }, + { + "type": "code", + "lines": [ + "// later test: seed the captured passkey so the app starts already enrolled.", + "const credential = JSON.parse(fs.readFileSync('playwright/.auth/passkey.json', 'utf8'));", + "const context = await browser.newContext();", + "await context.credentials.create(credential.rpId, credential);", + "await context.credentials.install();", + "", + "const page = await context.newPage();", + "await page.goto('https://example.com/login');", + "// navigator.credentials.get() resolves the captured passkey — already signed in." + ], + "codeLang": "js" + }, + { + "type": "code", + "lines": [ + "// later test: seed the captured passkey so the app starts already enrolled.", + "VirtualCredential credential = new Gson().fromJson(", + " Files.readString(Paths.get(\"playwright/.auth/passkey.json\")), VirtualCredential.class);", + "BrowserContext context = browser.newContext();", + "context.credentials().create(credential.rpId, new Credentials.CreateOptions()", + " .setId(credential.id)", + " .setUserHandle(credential.userHandle)", + " .setPrivateKey(credential.privateKey)", + " .setPublicKey(credential.publicKey));", + "context.credentials().install();", + "", + "Page page = context.newPage();", + "page.navigate(\"https://example.com/login\");", + "// navigator.credentials.get() resolves the captured passkey — already signed in." + ], + "codeLang": "java" + }, + { + "type": "code", + "lines": [ + "# later test: seed the captured passkey so the app starts already enrolled.", + "with open(\"playwright/.auth/passkey.json\") as f:", + " credential = json.load(f)", + "context = await browser.new_context()", + "await context.credentials.create(", + " credential[\"rpId\"],", + " id=credential[\"id\"],", + " user_handle=credential[\"userHandle\"],", + " private_key=credential[\"privateKey\"],", + " public_key=credential[\"publicKey\"],", + ")", + "await context.credentials.install()", + "", + "page = await context.new_page()", + "await page.goto(\"https://example.com/login\")", + "# navigator.credentials.get() resolves the captured passkey — already signed in." + ], + "codeLang": "python async" + }, + { + "type": "code", + "lines": [ + "# later test: seed the captured passkey so the app starts already enrolled.", + "with open(\"playwright/.auth/passkey.json\") as f:", + " credential = json.load(f)", + "context = browser.new_context()", + "context.credentials.create(", + " credential[\"rpId\"],", + " id=credential[\"id\"],", + " user_handle=credential[\"userHandle\"],", + " private_key=credential[\"privateKey\"],", + " public_key=credential[\"publicKey\"],", + ")", + "context.credentials.install()", + "", + "page = context.new_page()", + "page.goto(\"https://example.com/login\")", + "# navigator.credentials.get() resolves the captured passkey — already signed in." + ], + "codeLang": "python sync" + }, + { + "type": "code", + "lines": [ + "// later test: seed the captured passkey so the app starts already enrolled.", + "var credential = JsonSerializer.Deserialize(", + " File.ReadAllText(\"playwright/.auth/passkey.json\"));", + "var context = await browser.NewContextAsync();", + "await context.Credentials.CreateAsync(credential.RpId, new()", + "{", + " Id = credential.Id,", + " UserHandle = credential.UserHandle,", + " PrivateKey = credential.PrivateKey,", + " PublicKey = credential.PublicKey,", + "});", + "await context.Credentials.InstallAsync();", + "", + "var page = await context.NewPageAsync();", + "await page.GotoAsync(\"https://example.com/login\");", + "// navigator.credentials.get() resolves the captured passkey — already signed in." + ], + "codeLang": "csharp" + }, + { + "type": "text", + "text": "**Defaults**" + } + ], + "langs": {}, + "comment": "`Credentials` is a virtual WebAuthn authenticator scoped to a `BrowserContext`. It lets tests register passkeys and\nanswer `navigator.credentials.create()` / `navigator.credentials.get()` ceremonies in the page, without a real\nauthenticator or hardware security key.\n\nThere are two common ways to use it:\n\n**Usage: seed a known credential**\n\n```js\nconst context = await browser.newContext();\n\n// A passkey your backend already provisioned for a test user.\nawait context.credentials.create('example.com', {\n id: knownCredentialId, // base64url\n userHandle: knownUserHandle, // base64url\n privateKey: knownPrivateKey, // base64url PKCS#8 (DER)\n publicKey: knownPublicKey, // base64url SPKI (DER)\n});\nawait context.credentials.install();\n\nconst page = await context.newPage();\nawait page.goto('https://example.com/login');\n// The page's navigator.credentials.get() is answered with the seeded passkey.\n```\n\n```java\nBrowserContext context = browser.newContext();\n\n// A passkey your backend already provisioned for a test user.\ncontext.credentials().create(\"example.com\", new Credentials.CreateOptions()\n .setId(knownCredentialId) // base64url\n .setUserHandle(knownUserHandle) // base64url\n .setPrivateKey(knownPrivateKey) // base64url PKCS#8 (DER)\n .setPublicKey(knownPublicKey)); // base64url SPKI (DER)\ncontext.credentials().install();\n\nPage page = context.newPage();\npage.navigate(\"https://example.com/login\");\n// The page's navigator.credentials.get() is answered with the seeded passkey.\n```\n\n```py\ncontext = await browser.new_context()\n\n# A passkey your backend already provisioned for a test user.\nawait context.credentials.create(\n \"example.com\",\n id=known_credential_id, # base64url\n user_handle=known_user_handle, # base64url\n private_key=known_private_key, # base64url PKCS#8 (DER)\n public_key=known_public_key, # base64url SPKI (DER)\n)\nawait context.credentials.install()\n\npage = await context.new_page()\nawait page.goto(\"https://example.com/login\")\n# The page's navigator.credentials.get() is answered with the seeded passkey.\n```\n\n```py\ncontext = browser.new_context()\n\n# A passkey your backend already provisioned for a test user.\ncontext.credentials.create(\n \"example.com\",\n id=known_credential_id, # base64url\n user_handle=known_user_handle, # base64url\n private_key=known_private_key, # base64url PKCS#8 (DER)\n public_key=known_public_key, # base64url SPKI (DER)\n)\ncontext.credentials.install()\n\npage = context.new_page()\npage.goto(\"https://example.com/login\")\n# The page's navigator.credentials.get() is answered with the seeded passkey.\n```\n\n```csharp\nvar context = await browser.NewContextAsync();\n\n// A passkey your backend already provisioned for a test user.\nawait context.Credentials.CreateAsync(\"example.com\", new()\n{\n Id = knownCredentialId, // base64url\n UserHandle = knownUserHandle, // base64url\n PrivateKey = knownPrivateKey, // base64url PKCS#8 (DER)\n PublicKey = knownPublicKey, // base64url SPKI (DER)\n});\nawait context.Credentials.InstallAsync();\n\nvar page = await context.NewPageAsync();\nawait page.GotoAsync(\"https://example.com/login\");\n// The page's navigator.credentials.get() is answered with the seeded passkey.\n```\n\n**Usage: capture a passkey, then reuse it**\n\n```js\n// setup test: let the app register a passkey, then save it.\nconst context = await browser.newContext();\nawait context.credentials.install();\n\nconst page = await context.newPage();\nawait page.goto('https://example.com/register');\nawait page.getByRole('button', { name: 'Create a passkey' }).click();\n\n// Read back the passkey the page registered — it includes the private key.\nconst [credential] = await context.credentials.get({ rpId: 'example.com' });\nfs.writeFileSync('playwright/.auth/passkey.json', JSON.stringify(credential));\n```\n\n```java\n// setup test: let the app register a passkey, then save it.\nBrowserContext context = browser.newContext();\ncontext.credentials().install();\n\nPage page = context.newPage();\npage.navigate(\"https://example.com/register\");\npage.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(\"Create a passkey\")).click();\n\n// Read back the passkey the page registered — it includes the private key.\nVirtualCredential credential = context.credentials().get(\n new Credentials.GetOptions().setRpId(\"example.com\")).get(0);\nFiles.writeString(Paths.get(\"playwright/.auth/passkey.json\"), new Gson().toJson(credential));\n```\n\n```py\n# setup test: let the app register a passkey, then save it.\ncontext = await browser.new_context()\nawait context.credentials.install()\n\npage = await context.new_page()\nawait page.goto(\"https://example.com/register\")\nawait page.get_by_role(\"button\", name=\"Create a passkey\").click()\n\n# Read back the passkey the page registered — it includes the private key.\n[credential] = await context.credentials.get(rp_id=\"example.com\")\nwith open(\"playwright/.auth/passkey.json\", \"w\") as f:\n json.dump(credential, f)\n```\n\n```py\n# setup test: let the app register a passkey, then save it.\ncontext = browser.new_context()\ncontext.credentials.install()\n\npage = context.new_page()\npage.goto(\"https://example.com/register\")\npage.get_by_role(\"button\", name=\"Create a passkey\").click()\n\n# Read back the passkey the page registered — it includes the private key.\n[credential] = context.credentials.get(rp_id=\"example.com\")\nwith open(\"playwright/.auth/passkey.json\", \"w\") as f:\n json.dump(credential, f)\n```\n\n```csharp\n// setup test: let the app register a passkey, then save it.\nvar context = await browser.NewContextAsync();\nawait context.Credentials.InstallAsync();\n\nvar page = await context.NewPageAsync();\nawait page.GotoAsync(\"https://example.com/register\");\nawait page.GetByRole(AriaRole.Button, new() { Name = \"Create a passkey\" }).ClickAsync();\n\n// Read back the passkey the page registered — it includes the private key.\nvar credentials = await context.Credentials.GetAsync(new() { RpId = \"example.com\" });\nFile.WriteAllText(\"playwright/.auth/passkey.json\", JsonSerializer.Serialize(credentials[0]));\n```\n\n```js\n// later test: seed the captured passkey so the app starts already enrolled.\nconst credential = JSON.parse(fs.readFileSync('playwright/.auth/passkey.json', 'utf8'));\nconst context = await browser.newContext();\nawait context.credentials.create(credential.rpId, credential);\nawait context.credentials.install();\n\nconst page = await context.newPage();\nawait page.goto('https://example.com/login');\n// navigator.credentials.get() resolves the captured passkey — already signed in.\n```\n\n```java\n// later test: seed the captured passkey so the app starts already enrolled.\nVirtualCredential credential = new Gson().fromJson(\n Files.readString(Paths.get(\"playwright/.auth/passkey.json\")), VirtualCredential.class);\nBrowserContext context = browser.newContext();\ncontext.credentials().create(credential.rpId, new Credentials.CreateOptions()\n .setId(credential.id)\n .setUserHandle(credential.userHandle)\n .setPrivateKey(credential.privateKey)\n .setPublicKey(credential.publicKey));\ncontext.credentials().install();\n\nPage page = context.newPage();\npage.navigate(\"https://example.com/login\");\n// navigator.credentials.get() resolves the captured passkey — already signed in.\n```\n\n```py\n# later test: seed the captured passkey so the app starts already enrolled.\nwith open(\"playwright/.auth/passkey.json\") as f:\n credential = json.load(f)\ncontext = await browser.new_context()\nawait context.credentials.create(\n credential[\"rpId\"],\n id=credential[\"id\"],\n user_handle=credential[\"userHandle\"],\n private_key=credential[\"privateKey\"],\n public_key=credential[\"publicKey\"],\n)\nawait context.credentials.install()\n\npage = await context.new_page()\nawait page.goto(\"https://example.com/login\")\n# navigator.credentials.get() resolves the captured passkey — already signed in.\n```\n\n```py\n# later test: seed the captured passkey so the app starts already enrolled.\nwith open(\"playwright/.auth/passkey.json\") as f:\n credential = json.load(f)\ncontext = browser.new_context()\ncontext.credentials.create(\n credential[\"rpId\"],\n id=credential[\"id\"],\n user_handle=credential[\"userHandle\"],\n private_key=credential[\"privateKey\"],\n public_key=credential[\"publicKey\"],\n)\ncontext.credentials.install()\n\npage = context.new_page()\npage.goto(\"https://example.com/login\")\n# navigator.credentials.get() resolves the captured passkey — already signed in.\n```\n\n```csharp\n// later test: seed the captured passkey so the app starts already enrolled.\nvar credential = JsonSerializer.Deserialize(\n File.ReadAllText(\"playwright/.auth/passkey.json\"));\nvar context = await browser.NewContextAsync();\nawait context.Credentials.CreateAsync(credential.RpId, new()\n{\n Id = credential.Id,\n UserHandle = credential.UserHandle,\n PrivateKey = credential.PrivateKey,\n PublicKey = credential.PublicKey,\n});\nawait context.Credentials.InstallAsync();\n\nvar page = await context.NewPageAsync();\nawait page.GotoAsync(\"https://example.com/login\");\n// navigator.credentials.get() resolves the captured passkey — already signed in.\n```\n\n**Defaults**", + "since": "v1.61", + "members": [ + { + "kind": "method", + "langs": {}, + "since": "v1.61", + "name": "install", + "type": { + "name": "void" + }, + "spec": [ + { + "type": "text", + "text": "Installs the virtual WebAuthn authenticator into the context, overriding↵`navigator.credentials.create()` and `navigator.credentials.get()` in all current↵and future pages. Call this before the page first touches `navigator.credentials`." + }, + { + "type": "text", + "text": "Required: until [`method: Credentials.install`] is called, no interception is in place and the page sees↵the platform's native (or absent) WebAuthn behaviour. Seeding credentials with↵[`method: Credentials.create`] without installing populates the authenticator, but the↵page will never see those credentials." + } + ], + "required": true, + "comment": "Installs the virtual WebAuthn authenticator into the context, overriding `navigator.credentials.create()` and\n`navigator.credentials.get()` in all current and future pages. Call this before the page first touches\n`navigator.credentials`.\n\nRequired: until [`method: Credentials.install`] is called, no interception is in place and the page sees the\nplatform's native (or absent) WebAuthn behaviour. Seeding credentials with [`method: Credentials.create`] without\ninstalling populates the authenticator, but the page will never see those credentials.", + "async": true, + "alias": "install", + "overloadIndex": 0, + "args": [] + }, + { + "kind": "method", + "langs": {}, + "since": "v1.61", + "name": "create", + "type": { + "name": "Object", + "properties": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "id", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Base64url-encoded credential id." + } + ], + "required": true, + "comment": "Base64url-encoded credential id.", + "async": false, + "alias": "id", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "rpId", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Relying party id." + } + ], + "required": true, + "comment": "Relying party id.", + "async": false, + "alias": "rpId", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "userHandle", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Base64url-encoded user handle." + } + ], + "required": true, + "comment": "Base64url-encoded user handle.", + "async": false, + "alias": "userHandle", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "privateKey", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Base64url-encoded PKCS#8 (DER) private key." + } + ], + "required": true, + "comment": "Base64url-encoded PKCS#8 (DER) private key.", + "async": false, + "alias": "privateKey", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "publicKey", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Base64url-encoded SPKI (DER) public key." + } + ], + "required": true, + "comment": "Base64url-encoded SPKI (DER) public key.", + "async": false, + "alias": "publicKey", + "overloadIndex": 0 + } + ], + "expression": "[Object]", + "langAliases": { + "default": "VirtualCredential" + } + }, + "spec": [ + { + "type": "text", + "text": "Seeds a virtual WebAuthn credential and returns it." + }, + { + "type": "text", + "text": "With only `rpId`, generates a fresh **ECDSA P-256** keypair, credential id and user handle. The↵seeded credential is discoverable (resident), so the page can resolve it from both↵username-then-passkey and usernameless passkey flows. The returned object carries the private and public keys, so it can be persisted to disk and re-seeded in a later test." + }, + { + "type": "text", + "text": "To **import a known credential**, supply all four of `id`, `userHandle`, `privateKey` and↵`publicKey` together." + }, + { + "type": "text", + "text": "Call [`method: Credentials.install`] before navigating to a page that uses WebAuthn." + } + ], + "required": true, + "comment": "Seeds a virtual WebAuthn credential and returns it.\n\nWith only `rpId`, generates a fresh **ECDSA P-256** keypair, credential id and user handle. The seeded credential\nis discoverable (resident), so the page can resolve it from both username-then-passkey and usernameless passkey\nflows. The returned object carries the private and public keys, so it can be persisted to disk and re-seeded in a\nlater test.\n\nTo **import a known credential**, supply all four of `id`, `userHandle`, `privateKey` and `publicKey` together.\n\nCall [`method: Credentials.install`] before navigating to a page that uses WebAuthn.", + "async": true, + "alias": "create", + "overloadIndex": 0, + "args": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "rpId", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Relying party id (typically the site's effective domain)." + } + ], + "required": true, + "comment": "Relying party id (typically the site's effective domain).", + "async": false, + "alias": "rpId", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "options", + "type": { + "name": "Object", + "properties": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "id", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Base64url-encoded credential id. Auto-generated if omitted." + } + ], + "required": false, + "comment": "Base64url-encoded credential id. Auto-generated if omitted.", + "async": false, + "alias": "id", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "privateKey", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted." + } + ], + "required": false, + "comment": "Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted.", + "async": false, + "alias": "privateKey", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "publicKey", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Base64url-encoded SPKI (DER) public key. Auto-generated if omitted." + } + ], + "required": false, + "comment": "Base64url-encoded SPKI (DER) public key. Auto-generated if omitted.", + "async": false, + "alias": "publicKey", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "userHandle", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Base64url-encoded user handle. Auto-generated if omitted." + } + ], + "required": false, + "comment": "Base64url-encoded user handle. Auto-generated if omitted.", + "async": false, + "alias": "userHandle", + "overloadIndex": 0 + } + ] + }, + "required": false, + "comment": "", + "async": false, + "alias": "options", + "overloadIndex": 0 + } + ] + }, + { + "kind": "method", + "langs": {}, + "since": "v1.61", + "name": "delete", + "type": { + "name": "void" + }, + "spec": [ + { + "type": "text", + "text": "Removes a credential from the authenticator by its id. Works for any credential currently held —↵both those seeded with [`method: Credentials.create`] and those the page registered itself by↵calling `navigator.credentials.create()`." + } + ], + "required": true, + "comment": "Removes a credential from the authenticator by its id. Works for any credential currently held — both those seeded\nwith [`method: Credentials.create`] and those the page registered itself by calling\n`navigator.credentials.create()`.", + "async": true, + "alias": "delete", + "overloadIndex": 0, + "args": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "id", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Base64url-encoded credential id." + } + ], + "required": true, + "comment": "Base64url-encoded credential id.", + "async": false, + "alias": "id", + "overloadIndex": 0 + } + ] + }, + { + "kind": "method", + "langs": {}, + "since": "v1.61", + "name": "get", + "type": { + "name": "Array", + "templates": [ + { + "name": "Object", + "properties": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "id", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "" + } + ], + "required": true, + "comment": "", + "async": false, + "alias": "id", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "rpId", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "" + } + ], + "required": true, + "comment": "", + "async": false, + "alias": "rpId", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "userHandle", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "" + } + ], + "required": true, + "comment": "", + "async": false, + "alias": "userHandle", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "privateKey", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "" + } + ], + "required": true, + "comment": "", + "async": false, + "alias": "privateKey", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "publicKey", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "" + } + ], + "required": true, + "comment": "", + "async": false, + "alias": "publicKey", + "overloadIndex": 0 + } + ], + "langAliases": { + "default": "VirtualCredential" + } + } + ], + "expression": "[Array]<[Object]>" + }, + "spec": [ + { + "type": "text", + "text": "Returns every credential currently held by the authenticator, optionally filtered by `rpId` or↵`id`. This includes both credentials seeded with [`method: Credentials.create`] and credentials↵the page registered itself by calling `navigator.credentials.create()`." + }, + { + "type": "text", + "text": "Each returned credential includes its private and public keys, so a passkey the app just↵registered can be saved and re-seeded into a later test with [`method: Credentials.create`] — see the second example in the class overview." + } + ], + "required": true, + "comment": "Returns every credential currently held by the authenticator, optionally filtered by `rpId` or `id`. This includes\nboth credentials seeded with [`method: Credentials.create`] and credentials the page registered itself by calling\n`navigator.credentials.create()`.\n\nEach returned credential includes its private and public keys, so a passkey the app just registered can be saved\nand re-seeded into a later test with [`method: Credentials.create`] — see the second example in the class overview.", + "async": true, + "alias": "get", + "overloadIndex": 0, + "args": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "options", + "type": { + "name": "Object", + "properties": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "id", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Only return the credential with this base64url-encoded id." + } + ], + "required": false, + "comment": "Only return the credential with this base64url-encoded id.", + "async": false, + "alias": "id", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "rpId", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Only return credentials for this relying party id." + } + ], + "required": false, + "comment": "Only return credentials for this relying party id.", + "async": false, + "alias": "rpId", + "overloadIndex": 0 + } + ] + }, + "required": false, + "comment": "", + "async": false, + "alias": "options", + "overloadIndex": 0 + } + ] + } + ] + }, { "name": "Debugger", "spec": [ @@ -31783,7 +32883,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Location" + "default": "Location" } }, "spec": [ @@ -32005,7 +33105,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Location" + "default": "Location" } }, "spec": [ @@ -33025,8 +34125,8 @@ } ], "langAliases": { - "csharp": "ElementHandleBoundingBoxResult", - "java": "BoundingBox" + "default": "BoundingBox", + "csharp": "ElementHandleBoundingBoxResult" } } ], @@ -33259,7 +34359,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -33633,7 +34733,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -34048,7 +35148,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -35227,7 +36327,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -37330,7 +38430,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -37934,7 +39034,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -38396,7 +39496,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -39740,7 +40840,7 @@ } ], "langAliases": { - "csharp": "FilePayload" + "default": "FilePayload" } } ], @@ -40106,7 +41206,7 @@ } ], "langAliases": { - "csharp": "FilePayload" + "default": "FilePayload" } } ], @@ -40807,7 +41907,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -41246,7 +42346,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -41675,7 +42775,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -43048,7 +44148,7 @@ { "type": "code", "lines": [ - "const bodyHandle = await frame.evaluate('document.body');", + "const bodyHandle = await frame.evaluateHandle('document.body');", "const html = await frame.evaluate(([body, suffix]) =>", " body.innerHTML + suffix, [bodyHandle, 'hello'],", ");", @@ -43059,7 +44159,7 @@ { "type": "code", "lines": [ - "ElementHandle bodyHandle = frame.evaluate(\"document.body\");", + "ElementHandle bodyHandle = frame.evaluateHandle(\"document.body\");", "String html = (String) frame.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", Arrays.asList(bodyHandle, \"hello\"));", "bodyHandle.dispose();" ], @@ -43068,7 +44168,7 @@ { "type": "code", "lines": [ - "body_handle = await frame.evaluate(\"document.body\")", + "body_handle = await frame.evaluate_handle(\"document.body\")", "html = await frame.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])", "await body_handle.dispose()" ], @@ -43077,7 +44177,7 @@ { "type": "code", "lines": [ - "body_handle = frame.evaluate(\"document.body\")", + "body_handle = frame.evaluate_handle(\"document.body\")", "html = frame.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])", "body_handle.dispose()" ], @@ -43086,7 +44186,7 @@ { "type": "code", "lines": [ - "var bodyHandle = await frame.EvaluateAsync(\"document.body\");", + "var bodyHandle = await frame.EvaluateHandleAsync(\"document.body\");", "var html = await frame.EvaluateAsync(\"([body, suffix]) => body.innerHTML + suffix\", new object [] { bodyHandle, \"hello\" });", "await bodyHandle.DisposeAsync();" ], @@ -43094,7 +44194,7 @@ } ], "required": true, - "comment": "Returns the return value of `expression`.\n\nIf the function passed to the [`method: Frame.evaluate`] returns a [Promise], then [`method: Frame.evaluate`] would\nwait for the promise to resolve and return its value.\n\nIf the function passed to the [`method: Frame.evaluate`] returns a non-[Serializable] value, then\n[`method: Frame.evaluate`] returns `undefined`. Playwright also supports transferring some additional values that\nare not serializable by `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`.\n\n**Usage**\n\n```js\nconst result = await frame.evaluate(([x, y]) => {\n return Promise.resolve(x * y);\n}, [7, 8]);\nconsole.log(result); // prints \"56\"\n```\n\n```java\nObject result = frame.evaluate(\"([x, y]) => {\\n\" +\n \" return Promise.resolve(x * y);\\n\" +\n \"}\", Arrays.asList(7, 8));\nSystem.out.println(result); // prints \"56\"\n```\n\n```py\nresult = await frame.evaluate(\"([x, y]) => Promise.resolve(x * y)\", [7, 8])\nprint(result) # prints \"56\"\n```\n\n```py\nresult = frame.evaluate(\"([x, y]) => Promise.resolve(x * y)\", [7, 8])\nprint(result) # prints \"56\"\n```\n\n```csharp\nvar result = await frame.EvaluateAsync(\"([x, y]) => Promise.resolve(x * y)\", new[] { 7, 8 });\nConsole.WriteLine(result);\n```\n\nA string can also be passed in instead of a function.\n\n```js\nconsole.log(await frame.evaluate('1 + 2')); // prints \"3\"\n```\n\n```java\nSystem.out.println(frame.evaluate(\"1 + 2\")); // prints \"3\"\n```\n\n```py\nprint(await frame.evaluate(\"1 + 2\")) # prints \"3\"\nx = 10\nprint(await frame.evaluate(f\"1 + {x}\")) # prints \"11\"\n```\n\n```py\nprint(frame.evaluate(\"1 + 2\")) # prints \"3\"\nx = 10\nprint(frame.evaluate(f\"1 + {x}\")) # prints \"11\"\n```\n\n```csharp\nConsole.WriteLine(await frame.EvaluateAsync(\"1 + 2\")); // prints \"3\"\n```\n\n`ElementHandle` instances can be passed as an argument to the [`method: Frame.evaluate`]:\n\n```js\nconst bodyHandle = await frame.evaluate('document.body');\nconst html = await frame.evaluate(([body, suffix]) =>\n body.innerHTML + suffix, [bodyHandle, 'hello'],\n);\nawait bodyHandle.dispose();\n```\n\n```java\nElementHandle bodyHandle = frame.evaluate(\"document.body\");\nString html = (String) frame.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", Arrays.asList(bodyHandle, \"hello\"));\nbodyHandle.dispose();\n```\n\n```py\nbody_handle = await frame.evaluate(\"document.body\")\nhtml = await frame.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])\nawait body_handle.dispose()\n```\n\n```py\nbody_handle = frame.evaluate(\"document.body\")\nhtml = frame.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])\nbody_handle.dispose()\n```\n\n```csharp\nvar bodyHandle = await frame.EvaluateAsync(\"document.body\");\nvar html = await frame.EvaluateAsync(\"([body, suffix]) => body.innerHTML + suffix\", new object [] { bodyHandle, \"hello\" });\nawait bodyHandle.DisposeAsync();\n```\n", + "comment": "Returns the return value of `expression`.\n\nIf the function passed to the [`method: Frame.evaluate`] returns a [Promise], then [`method: Frame.evaluate`] would\nwait for the promise to resolve and return its value.\n\nIf the function passed to the [`method: Frame.evaluate`] returns a non-[Serializable] value, then\n[`method: Frame.evaluate`] returns `undefined`. Playwright also supports transferring some additional values that\nare not serializable by `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`.\n\n**Usage**\n\n```js\nconst result = await frame.evaluate(([x, y]) => {\n return Promise.resolve(x * y);\n}, [7, 8]);\nconsole.log(result); // prints \"56\"\n```\n\n```java\nObject result = frame.evaluate(\"([x, y]) => {\\n\" +\n \" return Promise.resolve(x * y);\\n\" +\n \"}\", Arrays.asList(7, 8));\nSystem.out.println(result); // prints \"56\"\n```\n\n```py\nresult = await frame.evaluate(\"([x, y]) => Promise.resolve(x * y)\", [7, 8])\nprint(result) # prints \"56\"\n```\n\n```py\nresult = frame.evaluate(\"([x, y]) => Promise.resolve(x * y)\", [7, 8])\nprint(result) # prints \"56\"\n```\n\n```csharp\nvar result = await frame.EvaluateAsync(\"([x, y]) => Promise.resolve(x * y)\", new[] { 7, 8 });\nConsole.WriteLine(result);\n```\n\nA string can also be passed in instead of a function.\n\n```js\nconsole.log(await frame.evaluate('1 + 2')); // prints \"3\"\n```\n\n```java\nSystem.out.println(frame.evaluate(\"1 + 2\")); // prints \"3\"\n```\n\n```py\nprint(await frame.evaluate(\"1 + 2\")) # prints \"3\"\nx = 10\nprint(await frame.evaluate(f\"1 + {x}\")) # prints \"11\"\n```\n\n```py\nprint(frame.evaluate(\"1 + 2\")) # prints \"3\"\nx = 10\nprint(frame.evaluate(f\"1 + {x}\")) # prints \"11\"\n```\n\n```csharp\nConsole.WriteLine(await frame.EvaluateAsync(\"1 + 2\")); // prints \"3\"\n```\n\n`ElementHandle` instances can be passed as an argument to the [`method: Frame.evaluate`]:\n\n```js\nconst bodyHandle = await frame.evaluateHandle('document.body');\nconst html = await frame.evaluate(([body, suffix]) =>\n body.innerHTML + suffix, [bodyHandle, 'hello'],\n);\nawait bodyHandle.dispose();\n```\n\n```java\nElementHandle bodyHandle = frame.evaluateHandle(\"document.body\");\nString html = (String) frame.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", Arrays.asList(bodyHandle, \"hello\"));\nbodyHandle.dispose();\n```\n\n```py\nbody_handle = await frame.evaluate_handle(\"document.body\")\nhtml = await frame.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])\nawait body_handle.dispose()\n```\n\n```py\nbody_handle = frame.evaluate_handle(\"document.body\")\nhtml = frame.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])\nbody_handle.dispose()\n```\n\n```csharp\nvar bodyHandle = await frame.EvaluateHandleAsync(\"document.body\");\nvar html = await frame.EvaluateAsync(\"([body, suffix]) => body.innerHTML + suffix\", new object [] { bodyHandle, \"hello\" });\nawait bodyHandle.DisposeAsync();\n```\n", "async": true, "alias": "evaluate", "overloadIndex": 0, @@ -43270,14 +44370,14 @@ { "type": "code", "lines": [ - "a_handle = await page.evaluate_handle(\"document\") # handle for the \"document\"" + "a_handle = await frame.evaluate_handle(\"document\") # handle for the \"document\"" ], "codeLang": "python async" }, { "type": "code", "lines": [ - "a_handle = page.evaluate_handle(\"document\") # handle for the \"document\"" + "a_handle = frame.evaluate_handle(\"document\") # handle for the \"document\"" ], "codeLang": "python sync" }, @@ -43317,8 +44417,8 @@ { "type": "code", "lines": [ - "a_handle = await page.evaluate_handle(\"document.body\")", - "result_handle = await page.evaluate_handle(\"body => body.innerHTML\", a_handle)", + "a_handle = await frame.evaluate_handle(\"document.body\")", + "result_handle = await frame.evaluate_handle(\"body => body.innerHTML\", a_handle)", "print(await result_handle.json_value())", "await result_handle.dispose()" ], @@ -43327,8 +44427,8 @@ { "type": "code", "lines": [ - "a_handle = page.evaluate_handle(\"document.body\")", - "result_handle = page.evaluate_handle(\"body => body.innerHTML\", a_handle)", + "a_handle = frame.evaluate_handle(\"document.body\")", + "result_handle = frame.evaluate_handle(\"body => body.innerHTML\", a_handle)", "print(result_handle.json_value())", "result_handle.dispose()" ], @@ -43346,7 +44446,7 @@ } ], "required": true, - "comment": "Returns the return value of `expression` as a `JSHandle`.\n\nThe only difference between [`method: Frame.evaluate`] and [`method: Frame.evaluateHandle`] is that\n[`method: Frame.evaluateHandle`] returns `JSHandle`.\n\nIf the function, passed to the [`method: Frame.evaluateHandle`], returns a [Promise], then\n[`method: Frame.evaluateHandle`] would wait for the promise to resolve and return its value.\n\n**Usage**\n\n```js\n// Handle for the window object\nconst aWindowHandle = await frame.evaluateHandle(() => Promise.resolve(window));\n```\n\n```java\n// Handle for the window object.\nJSHandle aWindowHandle = frame.evaluateHandle(\"() => Promise.resolve(window)\");\n```\n\n```py\na_window_handle = await frame.evaluate_handle(\"Promise.resolve(window)\")\na_window_handle # handle for the window object.\n```\n\n```py\na_window_handle = frame.evaluate_handle(\"Promise.resolve(window)\")\na_window_handle # handle for the window object.\n```\n\n```csharp\n// Handle for the window object.\nvar aWindowHandle = await frame.EvaluateHandleAsync(\"() => Promise.resolve(window)\");\n```\n\nA string can also be passed in instead of a function.\n\n```js\nconst aHandle = await frame.evaluateHandle('document'); // Handle for the 'document'.\n```\n\n```java\nJSHandle aHandle = frame.evaluateHandle(\"document\"); // Handle for the \"document\".\n```\n\n```py\na_handle = await page.evaluate_handle(\"document\") # handle for the \"document\"\n```\n\n```py\na_handle = page.evaluate_handle(\"document\") # handle for the \"document\"\n```\n\n```csharp\nvar docHandle = await frame.EvaluateHandleAsync(\"document\"); // Handle for the `document`\n```\n\n`JSHandle` instances can be passed as an argument to the [`method: Frame.evaluateHandle`]:\n\n```js\nconst aHandle = await frame.evaluateHandle(() => document.body);\nconst resultHandle = await frame.evaluateHandle(([body, suffix]) =>\n body.innerHTML + suffix, [aHandle, 'hello'],\n);\nconsole.log(await resultHandle.jsonValue());\nawait resultHandle.dispose();\n```\n\n```java\nJSHandle aHandle = frame.evaluateHandle(\"() => document.body\");\nJSHandle resultHandle = frame.evaluateHandle(\"([body, suffix]) => body.innerHTML + suffix\", Arrays.asList(aHandle, \"hello\"));\nSystem.out.println(resultHandle.jsonValue());\nresultHandle.dispose();\n```\n\n```py\na_handle = await page.evaluate_handle(\"document.body\")\nresult_handle = await page.evaluate_handle(\"body => body.innerHTML\", a_handle)\nprint(await result_handle.json_value())\nawait result_handle.dispose()\n```\n\n```py\na_handle = page.evaluate_handle(\"document.body\")\nresult_handle = page.evaluate_handle(\"body => body.innerHTML\", a_handle)\nprint(result_handle.json_value())\nresult_handle.dispose()\n```\n\n```csharp\nvar handle = await frame.EvaluateHandleAsync(\"() => document.body\");\nvar resultHandle = await frame.EvaluateHandleAsync(\"([body, suffix]) => body.innerHTML + suffix\", new object[] { handle, \"hello\" });\nConsole.WriteLine(await resultHandle.JsonValueAsync());\nawait resultHandle.DisposeAsync();\n```\n", + "comment": "Returns the return value of `expression` as a `JSHandle`.\n\nThe only difference between [`method: Frame.evaluate`] and [`method: Frame.evaluateHandle`] is that\n[`method: Frame.evaluateHandle`] returns `JSHandle`.\n\nIf the function, passed to the [`method: Frame.evaluateHandle`], returns a [Promise], then\n[`method: Frame.evaluateHandle`] would wait for the promise to resolve and return its value.\n\n**Usage**\n\n```js\n// Handle for the window object\nconst aWindowHandle = await frame.evaluateHandle(() => Promise.resolve(window));\n```\n\n```java\n// Handle for the window object.\nJSHandle aWindowHandle = frame.evaluateHandle(\"() => Promise.resolve(window)\");\n```\n\n```py\na_window_handle = await frame.evaluate_handle(\"Promise.resolve(window)\")\na_window_handle # handle for the window object.\n```\n\n```py\na_window_handle = frame.evaluate_handle(\"Promise.resolve(window)\")\na_window_handle # handle for the window object.\n```\n\n```csharp\n// Handle for the window object.\nvar aWindowHandle = await frame.EvaluateHandleAsync(\"() => Promise.resolve(window)\");\n```\n\nA string can also be passed in instead of a function.\n\n```js\nconst aHandle = await frame.evaluateHandle('document'); // Handle for the 'document'.\n```\n\n```java\nJSHandle aHandle = frame.evaluateHandle(\"document\"); // Handle for the \"document\".\n```\n\n```py\na_handle = await frame.evaluate_handle(\"document\") # handle for the \"document\"\n```\n\n```py\na_handle = frame.evaluate_handle(\"document\") # handle for the \"document\"\n```\n\n```csharp\nvar docHandle = await frame.EvaluateHandleAsync(\"document\"); // Handle for the `document`\n```\n\n`JSHandle` instances can be passed as an argument to the [`method: Frame.evaluateHandle`]:\n\n```js\nconst aHandle = await frame.evaluateHandle(() => document.body);\nconst resultHandle = await frame.evaluateHandle(([body, suffix]) =>\n body.innerHTML + suffix, [aHandle, 'hello'],\n);\nconsole.log(await resultHandle.jsonValue());\nawait resultHandle.dispose();\n```\n\n```java\nJSHandle aHandle = frame.evaluateHandle(\"() => document.body\");\nJSHandle resultHandle = frame.evaluateHandle(\"([body, suffix]) => body.innerHTML + suffix\", Arrays.asList(aHandle, \"hello\"));\nSystem.out.println(resultHandle.jsonValue());\nresultHandle.dispose();\n```\n\n```py\na_handle = await frame.evaluate_handle(\"document.body\")\nresult_handle = await frame.evaluate_handle(\"body => body.innerHTML\", a_handle)\nprint(await result_handle.json_value())\nawait result_handle.dispose()\n```\n\n```py\na_handle = frame.evaluate_handle(\"document.body\")\nresult_handle = frame.evaluate_handle(\"body => body.innerHTML\", a_handle)\nprint(result_handle.json_value())\nresult_handle.dispose()\n```\n\n```csharp\nvar handle = await frame.EvaluateHandleAsync(\"() => document.body\");\nvar resultHandle = await frame.EvaluateHandleAsync(\"([body, suffix]) => body.innerHTML + suffix\", new object[] { handle, \"hello\" });\nConsole.WriteLine(await resultHandle.JsonValueAsync());\nawait resultHandle.DisposeAsync();\n```\n", "async": true, "alias": "evaluateHandle", "overloadIndex": 0, @@ -46210,7 +47310,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -49057,7 +50157,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -49918,7 +51018,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -50604,7 +51704,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -58784,8 +59884,8 @@ } ], "langAliases": { - "csharp": "LocatorBoundingBoxResult", - "java": "BoundingBox" + "default": "BoundingBox", + "csharp": "LocatorBoundingBoxResult" } } ], @@ -59143,7 +60243,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -59811,7 +60911,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -60274,7 +61374,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -61706,7 +62806,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -65408,7 +66508,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -69185,7 +70285,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -69928,7 +71028,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -70549,7 +71649,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -84023,7 +85123,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -84440,7 +85540,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -85024,7 +86124,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -87096,7 +88196,7 @@ { "type": "code", "lines": [ - "const bodyHandle = await page.evaluate('document.body');", + "const bodyHandle = await page.evaluateHandle('document.body');", "const html = await page.evaluate(([body, suffix]) =>", " body.innerHTML + suffix, [bodyHandle, 'hello']", ");", @@ -87107,7 +88207,7 @@ { "type": "code", "lines": [ - "ElementHandle bodyHandle = page.evaluate(\"document.body\");", + "ElementHandle bodyHandle = page.evaluateHandle(\"document.body\");", "String html = (String) page.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", Arrays.asList(bodyHandle, \"hello\"));", "bodyHandle.dispose();" ], @@ -87116,7 +88216,7 @@ { "type": "code", "lines": [ - "body_handle = await page.evaluate(\"document.body\")", + "body_handle = await page.evaluate_handle(\"document.body\")", "html = await page.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])", "await body_handle.dispose()" ], @@ -87125,7 +88225,7 @@ { "type": "code", "lines": [ - "body_handle = page.evaluate(\"document.body\")", + "body_handle = page.evaluate_handle(\"document.body\")", "html = page.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])", "body_handle.dispose()" ], @@ -87134,7 +88234,7 @@ { "type": "code", "lines": [ - "var bodyHandle = await page.EvaluateAsync(\"document.body\");", + "var bodyHandle = await page.EvaluateHandleAsync(\"document.body\");", "var html = await page.EvaluateAsync(\"([body, suffix]) => body.innerHTML + suffix\", new object [] { bodyHandle, \"hello\" });", "await bodyHandle.DisposeAsync();" ], @@ -87142,7 +88242,7 @@ } ], "required": true, - "comment": "Returns the value of the `expression` invocation.\n\nIf the function passed to the [`method: Page.evaluate`] returns a [Promise], then [`method: Page.evaluate`] would\nwait for the promise to resolve and return its value.\n\nIf the function passed to the [`method: Page.evaluate`] returns a non-[Serializable] value, then\n[`method: Page.evaluate`] resolves to `undefined`. Playwright also supports transferring some additional values\nthat are not serializable by `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`.\n\n**Usage**\n\nPassing argument to `expression`:\n\n```js\nconst result = await page.evaluate(([x, y]) => {\n return Promise.resolve(x * y);\n}, [7, 8]);\nconsole.log(result); // prints \"56\"\n```\n\n```java\nObject result = page.evaluate(\"([x, y]) => {\\n\" +\n \" return Promise.resolve(x * y);\\n\" +\n \"}\", Arrays.asList(7, 8));\nSystem.out.println(result); // prints \"56\"\n```\n\n```py\nresult = await page.evaluate(\"([x, y]) => Promise.resolve(x * y)\", [7, 8])\nprint(result) # prints \"56\"\n```\n\n```py\nresult = page.evaluate(\"([x, y]) => Promise.resolve(x * y)\", [7, 8])\nprint(result) # prints \"56\"\n```\n\n```csharp\nvar result = await page.EvaluateAsync(\"([x, y]) => Promise.resolve(x * y)\", new[] { 7, 8 });\nConsole.WriteLine(result);\n```\n\nA string can also be passed in instead of a function:\n\n```js\nconsole.log(await page.evaluate('1 + 2')); // prints \"3\"\nconst x = 10;\nconsole.log(await page.evaluate(`1 + ${x}`)); // prints \"11\"\n```\n\n```java\nSystem.out.println(page.evaluate(\"1 + 2\")); // prints \"3\"\n```\n\n```py\nprint(await page.evaluate(\"1 + 2\")) # prints \"3\"\nx = 10\nprint(await page.evaluate(f\"1 + {x}\")) # prints \"11\"\n```\n\n```py\nprint(page.evaluate(\"1 + 2\")) # prints \"3\"\nx = 10\nprint(page.evaluate(f\"1 + {x}\")) # prints \"11\"\n```\n\n```csharp\nConsole.WriteLine(await page.EvaluateAsync(\"1 + 2\")); // prints \"3\"\n```\n\n`ElementHandle` instances can be passed as an argument to the [`method: Page.evaluate`]:\n\n```js\nconst bodyHandle = await page.evaluate('document.body');\nconst html = await page.evaluate(([body, suffix]) =>\n body.innerHTML + suffix, [bodyHandle, 'hello']\n);\nawait bodyHandle.dispose();\n```\n\n```java\nElementHandle bodyHandle = page.evaluate(\"document.body\");\nString html = (String) page.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", Arrays.asList(bodyHandle, \"hello\"));\nbodyHandle.dispose();\n```\n\n```py\nbody_handle = await page.evaluate(\"document.body\")\nhtml = await page.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])\nawait body_handle.dispose()\n```\n\n```py\nbody_handle = page.evaluate(\"document.body\")\nhtml = page.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])\nbody_handle.dispose()\n```\n\n```csharp\nvar bodyHandle = await page.EvaluateAsync(\"document.body\");\nvar html = await page.EvaluateAsync(\"([body, suffix]) => body.innerHTML + suffix\", new object [] { bodyHandle, \"hello\" });\nawait bodyHandle.DisposeAsync();\n```\n", + "comment": "Returns the value of the `expression` invocation.\n\nIf the function passed to the [`method: Page.evaluate`] returns a [Promise], then [`method: Page.evaluate`] would\nwait for the promise to resolve and return its value.\n\nIf the function passed to the [`method: Page.evaluate`] returns a non-[Serializable] value, then\n[`method: Page.evaluate`] resolves to `undefined`. Playwright also supports transferring some additional values\nthat are not serializable by `JSON`: `-0`, `NaN`, `Infinity`, `-Infinity`.\n\n**Usage**\n\nPassing argument to `expression`:\n\n```js\nconst result = await page.evaluate(([x, y]) => {\n return Promise.resolve(x * y);\n}, [7, 8]);\nconsole.log(result); // prints \"56\"\n```\n\n```java\nObject result = page.evaluate(\"([x, y]) => {\\n\" +\n \" return Promise.resolve(x * y);\\n\" +\n \"}\", Arrays.asList(7, 8));\nSystem.out.println(result); // prints \"56\"\n```\n\n```py\nresult = await page.evaluate(\"([x, y]) => Promise.resolve(x * y)\", [7, 8])\nprint(result) # prints \"56\"\n```\n\n```py\nresult = page.evaluate(\"([x, y]) => Promise.resolve(x * y)\", [7, 8])\nprint(result) # prints \"56\"\n```\n\n```csharp\nvar result = await page.EvaluateAsync(\"([x, y]) => Promise.resolve(x * y)\", new[] { 7, 8 });\nConsole.WriteLine(result);\n```\n\nA string can also be passed in instead of a function:\n\n```js\nconsole.log(await page.evaluate('1 + 2')); // prints \"3\"\nconst x = 10;\nconsole.log(await page.evaluate(`1 + ${x}`)); // prints \"11\"\n```\n\n```java\nSystem.out.println(page.evaluate(\"1 + 2\")); // prints \"3\"\n```\n\n```py\nprint(await page.evaluate(\"1 + 2\")) # prints \"3\"\nx = 10\nprint(await page.evaluate(f\"1 + {x}\")) # prints \"11\"\n```\n\n```py\nprint(page.evaluate(\"1 + 2\")) # prints \"3\"\nx = 10\nprint(page.evaluate(f\"1 + {x}\")) # prints \"11\"\n```\n\n```csharp\nConsole.WriteLine(await page.EvaluateAsync(\"1 + 2\")); // prints \"3\"\n```\n\n`ElementHandle` instances can be passed as an argument to the [`method: Page.evaluate`]:\n\n```js\nconst bodyHandle = await page.evaluateHandle('document.body');\nconst html = await page.evaluate(([body, suffix]) =>\n body.innerHTML + suffix, [bodyHandle, 'hello']\n);\nawait bodyHandle.dispose();\n```\n\n```java\nElementHandle bodyHandle = page.evaluateHandle(\"document.body\");\nString html = (String) page.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", Arrays.asList(bodyHandle, \"hello\"));\nbodyHandle.dispose();\n```\n\n```py\nbody_handle = await page.evaluate_handle(\"document.body\")\nhtml = await page.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])\nawait body_handle.dispose()\n```\n\n```py\nbody_handle = page.evaluate_handle(\"document.body\")\nhtml = page.evaluate(\"([body, suffix]) => body.innerHTML + suffix\", [body_handle, \"hello\"])\nbody_handle.dispose()\n```\n\n```csharp\nvar bodyHandle = await page.EvaluateHandleAsync(\"document.body\");\nvar html = await page.EvaluateAsync(\"([body, suffix]) => body.innerHTML + suffix\", new object [] { bodyHandle, \"hello\" });\nawait bodyHandle.DisposeAsync();\n```\n", "async": true, "alias": "evaluate", "overloadIndex": 0, @@ -87698,7 +88798,7 @@ "name": "function", "expression": "[function]", "langAliases": { - "java": "BindingCallback" + "default": "BindingCallback" } }, "spec": [ @@ -87969,7 +89069,7 @@ "name": "function", "expression": "[function]", "langAliases": { - "java": "FunctionCallback" + "default": "FunctionCallback" } }, "spec": [ @@ -91543,7 +92643,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -92931,6 +94031,66 @@ "overloadIndex": 0, "args": [] }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "localStorage", + "type": { + "name": "WebStorage", + "expression": "[WebStorage]" + }, + "spec": [ + { + "type": "text", + "text": "Provides access to the page's `localStorage` for the current origin. See `WebStorage`." + }, + { + "type": "code", + "lines": [ + "await page.localStorage.setItem('token', 'abc');", + "const token = await page.localStorage.getItem('token');" + ], + "codeLang": "js" + } + ], + "required": true, + "comment": "Provides access to the page's `localStorage` for the current origin. See `WebStorage`.\n\n```js\nawait page.localStorage.setItem('token', 'abc');\nconst token = await page.localStorage.getItem('token');\n```\n", + "async": false, + "alias": "localStorage", + "overloadIndex": 0, + "args": [] + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "sessionStorage", + "type": { + "name": "WebStorage", + "expression": "[WebStorage]" + }, + "spec": [ + { + "type": "text", + "text": "Provides access to the page's `sessionStorage` for the current origin. See `WebStorage`." + }, + { + "type": "code", + "lines": [ + "await page.sessionStorage.setItem('flag', '1');", + "const flag = await page.sessionStorage.getItem('flag');" + ], + "codeLang": "js" + } + ], + "required": true, + "comment": "Provides access to the page's `sessionStorage` for the current origin. See `WebStorage`.\n\n```js\nawait page.sessionStorage.setItem('flag', '1');\nconst flag = await page.sessionStorage.getItem('flag');\n```\n", + "async": false, + "alias": "sessionStorage", + "overloadIndex": 0, + "args": [] + }, { "kind": "method", "langs": {}, @@ -98198,7 +99358,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -99454,13 +100614,13 @@ "children": [ { "type": "text", - "text": "[`method: Page.tap`] the method will throw if `hasTouch` option of the browser context is false." + "text": "[`method: Page.tap`] will throw if the `hasTouch` option of the browser context is false." } ] } ], "required": true, - "comment": "This method taps an element matching `selector` by performing the following steps:\n1. Find an element matching `selector`. If there is none, wait until a matching element is attached to the DOM.\n1. Wait for [actionability](../actionability.md) checks on the matched element, unless `force` option is set. If\n the element is detached during the checks, the whole action is retried.\n1. Scroll the element into view if needed.\n1. Use [`property: Page.touchscreen`] to tap the center of the element, or the specified `position`.\n\nWhen all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`.\nPassing zero timeout disables this.\n\n**NOTE** [`method: Page.tap`] the method will throw if `hasTouch` option of the browser context is false.\n", + "comment": "This method taps an element matching `selector` by performing the following steps:\n1. Find an element matching `selector`. If there is none, wait until a matching element is attached to the DOM.\n1. Wait for [actionability](../actionability.md) checks on the matched element, unless `force` option is set. If\n the element is detached during the checks, the whole action is retried.\n1. Scroll the element into view if needed.\n1. Use [`property: Page.touchscreen`] to tap the center of the element, or the specified `position`.\n\nWhen all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`.\nPassing zero timeout disables this.\n\n**NOTE** [`method: Page.tap`] will throw if the `hasTouch` option of the browser context is false.\n", "async": true, "alias": "tap", "overloadIndex": 0, @@ -99633,7 +100793,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -100336,7 +101496,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Position" + "default": "Position" } }, "spec": [ @@ -100868,8 +102028,8 @@ } ], "langAliases": { - "csharp": "PageViewportSizeResult", - "java": "ViewportSize" + "default": "ViewportSize", + "csharp": "PageViewportSizeResult" } } ], @@ -108966,8 +110126,8 @@ } ], "langAliases": { - "csharp": "Header", - "java": "HttpHeader" + "default": "HttpHeader", + "csharp": "Header" } } ], @@ -110831,8 +111991,8 @@ } ], "langAliases": { - "csharp": "Header", - "java": "HttpHeader" + "default": "HttpHeader", + "csharp": "Header" } } ], @@ -111214,8 +112374,8 @@ } ], "langAliases": { - "csharp": "ResponseSecurityDetailsResult", - "java": "SecurityDetails" + "default": "SecurityDetails", + "csharp": "ResponseSecurityDetailsResult" } } ], @@ -113162,8 +114322,8 @@ "lines": [ "// Capture frames", "await page.screencast.start({", - " onFrame: ({ data, viewportWidth, viewportHeight }) => {", - " console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight})`);", + " onFrame: ({ data, timestamp, viewportWidth, viewportHeight }) => {", + " console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight}) at ${timestamp}`);", " },", " size: { width: 800, height: 600 },", "});", @@ -113174,7 +114334,7 @@ } ], "required": true, - "comment": "Starts the screencast. When `path` is provided, it saves video recording to the specified file. When `onFrame` is\nprovided, delivers JPEG-encoded frames to the callback. Both can be used together.\n\n**Usage**\n\n```js\n// Record video\nawait page.screencast.start({ path: 'video.webm', size: { width: 1280, height: 800 } });\n// ... perform actions ...\nawait page.screencast.stop();\n```\n\n```js\n// Capture frames\nawait page.screencast.start({\n onFrame: ({ data, viewportWidth, viewportHeight }) => {\n console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight})`);\n },\n size: { width: 800, height: 600 },\n});\n// ... perform actions ...\nawait page.screencast.stop();\n```\n", + "comment": "Starts the screencast. When `path` is provided, it saves video recording to the specified file. When `onFrame` is\nprovided, delivers JPEG-encoded frames to the callback. Both can be used together.\n\n**Usage**\n\n```js\n// Record video\nawait page.screencast.start({ path: 'video.webm', size: { width: 1280, height: 800 } });\n// ... perform actions ...\nawait page.screencast.stop();\n```\n\n```js\n// Capture frames\nawait page.screencast.start({\n onFrame: ({ data, timestamp, viewportWidth, viewportHeight }) => {\n console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight}) at ${timestamp}`);\n },\n size: { width: 800, height: 600 },\n});\n// ... perform actions ...\nawait page.screencast.stop();\n```\n", "async": true, "alias": "start", "overloadIndex": 0, @@ -113223,6 +114383,27 @@ "alias": "data", "overloadIndex": 0 }, + { + "kind": "property", + "langs": {}, + "since": "v1.59", + "name": "timestamp", + "type": { + "name": "float", + "expression": "[float]" + }, + "spec": [ + { + "type": "text", + "text": "The timestamp of when the frame was presented by the browser, in milliseconds since the Unix epoch." + } + ], + "required": true, + "comment": "The timestamp of when the frame was presented by the browser, in milliseconds since the Unix epoch.", + "async": false, + "alias": "timestamp", + "overloadIndex": 0 + }, { "kind": "property", "langs": {}, @@ -113328,14 +114509,7 @@ }, { "kind": "property", - "langs": { - "only": [ - "js" - ], - "aliases": {}, - "types": {}, - "overrides": {} - }, + "langs": {}, "since": "v1.59", "name": "size", "type": { @@ -113643,6 +114817,35 @@ "type": { "name": "Object", "properties": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "cursor", + "type": { + "name": "ScreencastCursor", + "union": [ + { + "name": "\"none\"" + }, + { + "name": "\"pointer\"" + } + ], + "expression": "[ScreencastCursor]<\"none\"|\"pointer\">" + }, + "spec": [ + { + "type": "text", + "text": "Cursor decoration shown for pointer actions. `\"pointer\"` (the default) renders↵a mouse pointer that animates from the previous action point to the next one.↵`\"none\"` disables the cursor decoration." + } + ], + "required": false, + "comment": "Cursor decoration shown for pointer actions. `\"pointer\"` (the default) renders a mouse pointer that animates from\nthe previous action point to the next one. `\"none\"` disables the cursor decoration.", + "async": false, + "alias": "cursor", + "overloadIndex": 0 + }, { "kind": "property", "langs": {}, @@ -114344,11 +115547,11 @@ "spec": [ { "type": "text", - "text": "Test id attribute name." + "text": "Test id attribute name. To match elements with any of several attributes, pass them as a↵comma-separated list, e.g. `\"data-pw,data-ti\"`." } ], "required": true, - "comment": "Test id attribute name.", + "comment": "Test id attribute name. To match elements with any of several attributes, pass them as a comma-separated list, e.g.\n`\"data-pw,data-ti\"`.", "async": false, "alias": "attributeName", "overloadIndex": 0 @@ -114938,13 +116141,13 @@ "children": [ { "type": "text", - "text": "[`method: Page.tap`] the method will throw if `hasTouch` option of the browser context is false." + "text": "[`method: Touchscreen.tap`] will throw if the `hasTouch` option of the browser context is false." } ] } ], "required": true, - "comment": "Dispatches a `touchstart` and `touchend` event with a single touch at the position (`x`,`y`).\n\n**NOTE** [`method: Page.tap`] the method will throw if `hasTouch` option of the browser context is false.\n", + "comment": "Dispatches a `touchstart` and `touchend` event with a single touch at the position (`x`,`y`).\n\n**NOTE** [`method: Touchscreen.tap`] will throw if the `hasTouch` option of the browser context is false.\n", "async": true, "alias": "tap", "overloadIndex": 0, @@ -116018,7 +117221,7 @@ ], "expression": "[Object]", "langAliases": { - "java": "Location" + "default": "Location" } }, "spec": [ @@ -118456,6 +119659,338 @@ } ] }, + { + "name": "WebStorage", + "spec": [ + { + "type": "text", + "text": "WebStorage exposes the page's `localStorage` or `sessionStorage` for the current origin via an async,↵[browser-consistent](https://developer.mozilla.org/en-US/docs/Web/API/Storage) API." + }, + { + "type": "text", + "text": "Instances are accessed through [`property: Page.localStorage`] and [`property: Page.sessionStorage`]." + }, + { + "type": "code", + "lines": [ + "await page.goto('https://example.com');", + "await page.localStorage.setItem('token', 'abc');", + "const token = await page.localStorage.getItem('token');", + "const all = await page.localStorage.items();", + "await page.localStorage.removeItem('token');", + "await page.localStorage.clear();" + ], + "codeLang": "js" + }, + { + "type": "code", + "lines": [ + "await page.goto(\"https://example.com\")", + "await page.local_storage.set_item(\"token\", \"abc\")", + "token = await page.local_storage.get_item(\"token\")", + "all = await page.local_storage.items()", + "await page.local_storage.remove_item(\"token\")", + "await page.local_storage.clear()" + ], + "codeLang": "python async" + }, + { + "type": "code", + "lines": [ + "page.goto(\"https://example.com\")", + "page.local_storage.set_item(\"token\", \"abc\")", + "token = page.local_storage.get_item(\"token\")", + "all = page.local_storage.items()", + "page.local_storage.remove_item(\"token\")", + "page.local_storage.clear()" + ], + "codeLang": "python sync" + }, + { + "type": "code", + "lines": [ + "page.navigate(\"https://example.com\");", + "page.localStorage().setItem(\"token\", \"abc\");", + "String token = page.localStorage().getItem(\"token\");", + "List all = page.localStorage().items();", + "page.localStorage().removeItem(\"token\");", + "page.localStorage().clear();" + ], + "codeLang": "java" + }, + { + "type": "code", + "lines": [ + "await page.GotoAsync(\"https://example.com\");", + "await page.LocalStorage.SetItemAsync(\"token\", \"abc\");", + "var token = await page.LocalStorage.GetItemAsync(\"token\");", + "var all = await page.LocalStorage.ItemsAsync();", + "await page.LocalStorage.RemoveItemAsync(\"token\");", + "await page.LocalStorage.ClearAsync();" + ], + "codeLang": "csharp" + } + ], + "langs": {}, + "comment": "WebStorage exposes the page's `localStorage` or `sessionStorage` for the current origin via an async,\n[browser-consistent](https://developer.mozilla.org/en-US/docs/Web/API/Storage) API.\n\nInstances are accessed through [`property: Page.localStorage`] and [`property: Page.sessionStorage`].\n\n```js\nawait page.goto('https://example.com');\nawait page.localStorage.setItem('token', 'abc');\nconst token = await page.localStorage.getItem('token');\nconst all = await page.localStorage.items();\nawait page.localStorage.removeItem('token');\nawait page.localStorage.clear();\n```\n\n```py\nawait page.goto(\"https://example.com\")\nawait page.local_storage.set_item(\"token\", \"abc\")\ntoken = await page.local_storage.get_item(\"token\")\nall = await page.local_storage.items()\nawait page.local_storage.remove_item(\"token\")\nawait page.local_storage.clear()\n```\n\n```py\npage.goto(\"https://example.com\")\npage.local_storage.set_item(\"token\", \"abc\")\ntoken = page.local_storage.get_item(\"token\")\nall = page.local_storage.items()\npage.local_storage.remove_item(\"token\")\npage.local_storage.clear()\n```\n\n```java\npage.navigate(\"https://example.com\");\npage.localStorage().setItem(\"token\", \"abc\");\nString token = page.localStorage().getItem(\"token\");\nList all = page.localStorage().items();\npage.localStorage().removeItem(\"token\");\npage.localStorage().clear();\n```\n\n```csharp\nawait page.GotoAsync(\"https://example.com\");\nawait page.LocalStorage.SetItemAsync(\"token\", \"abc\");\nvar token = await page.LocalStorage.GetItemAsync(\"token\");\nvar all = await page.LocalStorage.ItemsAsync();\nawait page.LocalStorage.RemoveItemAsync(\"token\");\nawait page.LocalStorage.ClearAsync();\n```\n", + "since": "v1.61", + "members": [ + { + "kind": "method", + "langs": {}, + "since": "v1.61", + "name": "items", + "type": { + "name": "Array", + "templates": [ + { + "name": "Object", + "properties": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "name", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "" + } + ], + "required": true, + "comment": "", + "async": false, + "alias": "name", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "value", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "" + } + ], + "required": true, + "comment": "", + "async": false, + "alias": "value", + "overloadIndex": 0 + } + ], + "langAliases": { + "default": "WebStorageItem" + } + } + ], + "expression": "[Array]<[Object]>" + }, + "spec": [ + { + "type": "text", + "text": "Returns all items in the storage as name/value pairs." + } + ], + "required": true, + "comment": "Returns all items in the storage as name/value pairs.", + "async": true, + "alias": "items", + "overloadIndex": 0, + "args": [] + }, + { + "kind": "method", + "langs": {}, + "since": "v1.61", + "name": "getItem", + "type": { + "name": "", + "union": [ + { + "name": "null" + }, + { + "name": "string" + } + ], + "expression": "[null]|[string]" + }, + "spec": [ + { + "type": "text", + "text": "Returns the value for the given `name` if present." + } + ], + "required": true, + "comment": "Returns the value for the given `name` if present.", + "async": true, + "alias": "getItem", + "overloadIndex": 0, + "args": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "name", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Name of the item to retrieve." + } + ], + "required": true, + "comment": "Name of the item to retrieve.", + "async": false, + "alias": "name", + "overloadIndex": 0 + } + ] + }, + { + "kind": "method", + "langs": {}, + "since": "v1.61", + "name": "setItem", + "type": { + "name": "void" + }, + "spec": [ + { + "type": "text", + "text": "Sets the value for the given `name`. Overwrites any existing value for that name." + } + ], + "required": true, + "comment": "Sets the value for the given `name`. Overwrites any existing value for that name.", + "async": true, + "alias": "setItem", + "overloadIndex": 0, + "args": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "name", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Name of the item to set." + } + ], + "required": true, + "comment": "Name of the item to set.", + "async": false, + "alias": "name", + "overloadIndex": 0 + }, + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "value", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "New value for the item." + } + ], + "required": true, + "comment": "New value for the item.", + "async": false, + "alias": "value", + "overloadIndex": 0 + } + ] + }, + { + "kind": "method", + "langs": {}, + "since": "v1.61", + "name": "removeItem", + "type": { + "name": "void" + }, + "spec": [ + { + "type": "text", + "text": "Removes the item with the given `name`. No-op if the item is absent." + } + ], + "required": true, + "comment": "Removes the item with the given `name`. No-op if the item is absent.", + "async": true, + "alias": "removeItem", + "overloadIndex": 0, + "args": [ + { + "kind": "property", + "langs": {}, + "since": "v1.61", + "name": "name", + "type": { + "name": "string", + "expression": "[string]" + }, + "spec": [ + { + "type": "text", + "text": "Name of the item to remove." + } + ], + "required": true, + "comment": "Name of the item to remove.", + "async": false, + "alias": "name", + "overloadIndex": 0 + } + ] + }, + { + "kind": "method", + "langs": {}, + "since": "v1.61", + "name": "clear", + "type": { + "name": "void" + }, + "spec": [ + { + "type": "text", + "text": "Removes all items from the storage." + } + ], + "required": true, + "comment": "Removes all items from the storage.", + "async": true, + "alias": "clear", + "overloadIndex": 0, + "args": [] + } + ] + }, { "name": "Worker", "spec": [ diff --git a/development/generate_api/example_codes.rb b/development/generate_api/example_codes.rb index 50d4638b..5f0a1654 100644 --- a/development/generate_api/example_codes.rb +++ b/development/generate_api/example_codes.rb @@ -608,8 +608,8 @@ def example_9c73167b900498bca191abc2ce2627e063f84b0abc8ce3a117416cb734602760(fra end # Frame#evaluate - def example_6ebfd0a9a1f3cb61410f494ffc34a17f5c6d57280326d077fca3b0a18aef7834(frame:) - body_handle = frame.query_selector("body") + def example_1bdfda7bbd1869b76e70522b88e85bad065d776b0171cc78caa6283a04ac4153(frame:) + body_handle = frame.evaluate_handle("document.body") html = frame.evaluate("([body, suffix]) => body.innerHTML + suffix", arg: [body_handle, "hello"]) body_handle.dispose end @@ -620,6 +620,19 @@ def example_a1c8e837e826079359d01d6f7eecc64092a45d8c74280d23ee9039c379132c51(fra a_window_handle # handle for the window object. end + # Frame#evaluate_handle + def example_77cd1d8e4b3ca6967cf6c3e87ce390ce884619e33a3620d8d7de9df3a4bb54ed(frame:) + a_handle = frame.evaluate_handle("document") # handle for the "document" + end + + # Frame#evaluate_handle + def example_65c46bc1d1d437510c902d3817be49384affcbc2c03f26a1a4bc0ed1808a2baf(frame:) + a_handle = frame.evaluate_handle("document.body") + result_handle = frame.evaluate_handle("body => body.innerHTML", arg: a_handle) + puts result_handle.json_value + result_handle.dispose + end + # Frame#frame_element def example_e6b4fdef29a401d84b17acfa319bee08f39e1f28e07c435463622220c6a24747(frame:) frame_element = frame.frame_element @@ -1540,8 +1553,8 @@ def example_23b23ab6bd2ef249ab5ba8ecf6bdb1b450ce4a8a9685d23b0b050cf1b040b7da(pag end # Page#evaluate - def example_b49ac8565a94d1273fd47819ad9090736deb02feb0aea4a9eb35c68c66f22502(page:) - body_handle = page.query_selector("body") + def example_e879414b6822d8233c3c7db9c10d5c7c8f597093eaf6397b3cbbe48c68131307(page:) + body_handle = page.evaluate_handle("document.body") html = page.evaluate("([body, suffix]) => body.innerHTML + suffix", arg: [body_handle, "hello"]) body_handle.dispose end diff --git a/documentation/docs/api/browser_type.md b/documentation/docs/api/browser_type.md index 824f2e4d..f0101436 100644 --- a/documentation/docs/api/browser_type.md +++ b/documentation/docs/api/browser_type.md @@ -41,6 +41,7 @@ This method attaches Playwright to an existing browser instance created via `Bro ``` def connect_over_cdp( endpointURL, + artifactsDir: nil, headers: nil, isLocal: nil, noDefaults: nil, diff --git a/documentation/docs/api/frame.md b/documentation/docs/api/frame.md index 1495cc96..aaefbd1e 100644 --- a/documentation/docs/api/frame.md +++ b/documentation/docs/api/frame.md @@ -291,7 +291,7 @@ puts frame.evaluate("1 + #{x}") # => "11" [ElementHandle](./element_handle) instances can be passed as an argument to the [Frame#evaluate](./frame#evaluate): ```ruby -body_handle = frame.query_selector("body") +body_handle = frame.evaluate_handle("document.body") html = frame.evaluate("([body, suffix]) => body.innerHTML + suffix", arg: [body_handle, "hello"]) body_handle.dispose ``` @@ -321,14 +321,14 @@ a_window_handle # handle for the window object. A string can also be passed in instead of a function. ```ruby -a_handle = page.evaluate_handle("document") # handle for the "document" +a_handle = frame.evaluate_handle("document") # handle for the "document" ``` [JSHandle](./js_handle) instances can be passed as an argument to the [Frame#evaluate_handle](./frame#evaluate_handle): ```ruby -body_handle = page.evaluate_handle("document.body") -result_handle = page.evaluate_handle("body => body.innerHTML", arg: body_handle) +a_handle = frame.evaluate_handle("document.body") +result_handle = frame.evaluate_handle("body => body.innerHTML", arg: a_handle) puts result_handle.json_value result_handle.dispose ``` diff --git a/documentation/docs/api/page.md b/documentation/docs/api/page.md index 15386780..431f0506 100644 --- a/documentation/docs/api/page.md +++ b/documentation/docs/api/page.md @@ -413,7 +413,7 @@ puts page.evaluate("1 + #{x}") # => "11" [ElementHandle](./element_handle) instances can be passed as an argument to the [Page#evaluate](./page#evaluate): ```ruby -body_handle = page.query_selector("body") +body_handle = page.evaluate_handle("document.body") html = page.evaluate("([body, suffix]) => body.innerHTML + suffix", arg: [body_handle, "hello"]) body_handle.dispose ``` @@ -1561,7 +1561,7 @@ This method taps an element matching `selector` by performing the following step When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`. Passing zero timeout disables this. -**NOTE**: [Page#tap_point](./page#tap_point) the method will throw if `hasTouch` option of the browser context is false. +**NOTE**: [Page#tap_point](./page#tap_point) will throw if the `hasTouch` option of the browser context is false. ## text_content diff --git a/documentation/docs/api/touchscreen.md b/documentation/docs/api/touchscreen.md index 6acb1009..d0864fa6 100644 --- a/documentation/docs/api/touchscreen.md +++ b/documentation/docs/api/touchscreen.md @@ -19,4 +19,4 @@ def tap_point(x, y) Dispatches a `touchstart` and `touchend` event with a single touch at the position (`x`,`y`). -**NOTE**: [Page#tap_point](./page#tap_point) the method will throw if `hasTouch` option of the browser context is false. +**NOTE**: [Touchscreen#tap_point](./touchscreen#tap_point) will throw if the `hasTouch` option of the browser context is false. diff --git a/documentation/docs/include/api_coverage.md b/documentation/docs/include/api_coverage.md index c55c3a2c..09f9a5a8 100644 --- a/documentation/docs/include/api_coverage.md +++ b/documentation/docs/include/api_coverage.md @@ -365,6 +365,8 @@ * ~~wait_for_event~~ * clock * keyboard +* ~~local_storage~~ +* ~~session_storage~~ * mouse * request * screencast @@ -405,6 +407,7 @@ * expect_page * ~~wait_for_event~~ * clock +* ~~credentials~~ * ~~debugger~~ * request * tracing @@ -553,6 +556,8 @@ * headers_array * json * ok +* ~~security_details~~ +* ~~server_addr~~ * status * status_text * text diff --git a/lib/playwright/channel_owners/browser_type.rb b/lib/playwright/channel_owners/browser_type.rb index 5f5f4d89..6098cfe8 100644 --- a/lib/playwright/channel_owners/browser_type.rb +++ b/lib/playwright/channel_owners/browser_type.rb @@ -92,11 +92,12 @@ def connect(endpoint, exposeNetwork: nil, headers: nil, slowMo: nil, timeout: ni raise end - def connect_over_cdp(endpointURL, headers: nil, isLocal: nil, noDefaults: nil, slowMo: nil, timeout: nil, &block) + def connect_over_cdp(endpointURL, artifactsDir: nil, headers: nil, isLocal: nil, noDefaults: nil, slowMo: nil, timeout: nil, &block) raise 'Connecting over CDP is only supported in Chromium.' unless name == 'chromium' params = { endpointURL: endpointURL, + artifactsDir: artifactsDir, headers: headers, isLocal: isLocal, noDefaults: noDefaults, diff --git a/lib/playwright/channel_owners/frame.rb b/lib/playwright/channel_owners/frame.rb index 6fca4ee0..64e88cec 100644 --- a/lib/playwright/channel_owners/frame.rb +++ b/lib/playwright/channel_owners/frame.rb @@ -725,25 +725,44 @@ def expect(selector, expression, options, title) .serialize end - result = @channel.send_message_to_server_result( - title, # title - "expect", # method - { # params - selector: selector, - expression: expression, - **options, - }.compact - ) - - if result.key?('received') - if result['received'].is_a?(Hash) && result['received'].key?('value') - result['received']['value'] = JavaScript::ValueParser.new(result['received']['value']).parse - elsif !result['received'].is_a?(Hash) - result['received'] = JavaScript::ValueParser.new(result['received']).parse + is_not = options[:isNot] || false + + # Since Playwright 1.61, the `expect` command resolves on a successful + # match and rejects with FrameExpectErrorDetails on a mismatch/timeout. + begin + @channel.send_message_to_server_result( + title, # title + "expect", # method + { # params + selector: selector, + expression: expression, + **options, + }.compact + ) + { 'matches' => !is_not } + rescue ::Playwright::Error => err + details = err.details || {} + received = details['received'] + if received.is_a?(Hash) + received = received.dup + if received.key?('value') + received['value'] = JavaScript::ValueParser.new(received['value']).parse + end end - end - result + error_message = + if details['customErrorMessage'] + "Error: #{details['customErrorMessage']}" + end + + { + 'matches' => is_not, + 'received' => received, + 'log' => err.raw_log, + 'timedOut' => details['timedOut'], + 'errorMessage' => error_message, + }.compact + end end private def drop_payload_params(payload) diff --git a/lib/playwright/connection.rb b/lib/playwright/connection.rb index d7a8e8d6..d1289a63 100644 --- a/lib/playwright/connection.rb +++ b/lib/playwright/connection.rb @@ -168,6 +168,7 @@ def dispatch(msg) if error && !msg['result'] parsed_error = ::Playwright::Error.parse(error['error']) parsed_error.log = msg['log'] + parsed_error.details = msg['errorDetails'] callback.reject(parsed_error) else result = replace_guids_with_channels(msg['result']) diff --git a/lib/playwright/errors.rb b/lib/playwright/errors.rb index 6af76ad2..1394a5d8 100644 --- a/lib/playwright/errors.rb +++ b/lib/playwright/errors.rb @@ -31,10 +31,13 @@ def initialize(message:, name: nil, stack: nil) @stack = stack end - attr_reader :name, :message, :stack + attr_reader :name, :message, :stack, :raw_log + # Error details for `expect` failures (received value, timedOut, customErrorMessage). + attr_accessor :details def log=(log) return unless log + @raw_log = log format_call_log = log.join("\n - ") @message = "#{@message}\nCall log:\n#{format_call_log}\n" end diff --git a/lib/playwright/version.rb b/lib/playwright/version.rb index 26d5b23f..05c499cf 100644 --- a/lib/playwright/version.rb +++ b/lib/playwright/version.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module Playwright - VERSION = '1.60.0' - COMPATIBLE_PLAYWRIGHT_VERSION = '1.60.0' + VERSION = '1.61.1' + COMPATIBLE_PLAYWRIGHT_VERSION = '1.61.1' end From 7daa9ca0703dbb4ca0ddfdfe44e9728d5fb33bfe Mon Sep 17 00:00:00 2001 From: Kurt Ruppel Date: Fri, 26 Jun 2026 02:44:39 +0000 Subject: [PATCH 2/4] Implement Page#local_storage and #session_storage (WebStorage) Adds the 1.61 WebStorage API: page.local_storage / page.session_storage expose the current origin's localStorage / sessionStorage via items, get_item, set_item, remove_item and clear, backed by the page channel's webStorage* commands. Registers WebStorage with the API generator and ports the upstream page-localstorage spec. --- development/generate_api.rb | 2 + development/generate_api/example_codes.rb | 10 ++ documentation/docs/api/page.md | 10 ++ documentation/docs/api/web_storage.md | 65 +++++++++++ documentation/docs/include/api_coverage.md | 12 ++- lib/playwright/channel_owners/page.rb | 8 ++ lib/playwright/web_storage_impl.rb | 34 ++++++ spec/integration/page/local_storage_spec.rb | 114 ++++++++++++++++++++ 8 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 documentation/docs/api/web_storage.md create mode 100644 lib/playwright/web_storage_impl.rb create mode 100644 spec/integration/page/local_storage_spec.rb diff --git a/development/generate_api.rb b/development/generate_api.rb index 9d6bacc2..76b78ac8 100644 --- a/development/generate_api.rb +++ b/development/generate_api.rb @@ -31,6 +31,7 @@ APIRequest LocatorAssertions PageAssertions + WebStorage ] EXPERIMENTAL = %w[ ] @@ -50,6 +51,7 @@ LocatorAssertions PageAssertions Selectors + WebStorage ] require 'bundler/setup' diff --git a/development/generate_api/example_codes.rb b/development/generate_api/example_codes.rb index 5f0a1654..f33c9c6a 100644 --- a/development/generate_api/example_codes.rb +++ b/development/generate_api/example_codes.rb @@ -2056,6 +2056,16 @@ def example_9bd098f4f0838dc7408dffc68bff0351714717a1fed7a909801dc263dfff2a17(con context.tracing.group_end end + # WebStorage + def example_c19b80815d7452be0cbc5e027c2e78e36e8be59deaa35efa1d703aae54aa40f8(page:) + page.goto("https://example.com") + page.local_storage.set_item("token", "abc") + token = page.local_storage.get_item("token") + all = page.local_storage.items + page.local_storage.remove_item("token") + page.local_storage.clear + end + # Worker def example_29716fdd4471a97923a64eebeee96330ab508226a496ae8fd13f12eb07d55ee6(page:) def handle_worker(worker) diff --git a/documentation/docs/api/page.md b/documentation/docs/api/page.md index 431f0506..cef2b5ea 100644 --- a/documentation/docs/api/page.md +++ b/documentation/docs/api/page.md @@ -2001,6 +2001,16 @@ Playwright has ability to mock clock and passage of time. ## keyboard +## local_storage + + +Provides access to the page's `localStorage` for the current origin. See [WebStorage](./web_storage). + +## session_storage + + +Provides access to the page's `sessionStorage` for the current origin. See [WebStorage](./web_storage). + ## mouse ## request diff --git a/documentation/docs/api/web_storage.md b/documentation/docs/api/web_storage.md new file mode 100644 index 00000000..51142a70 --- /dev/null +++ b/documentation/docs/api/web_storage.md @@ -0,0 +1,65 @@ +--- +sidebar_position: 10 +--- + +# WebStorage + + +WebStorage exposes the page's `localStorage` or `sessionStorage` for the current origin via an async, +[browser-consistent](https://developer.mozilla.org/en-US/docs/Web/API/Storage) API. + +Instances are accessed through [Page#local_storage](./page#local_storage) and [Page#session_storage](./page#session_storage). + +```ruby +page.goto("https://example.com") +page.local_storage.set_item("token", "abc") +token = page.local_storage.get_item("token") +all = page.local_storage.items +page.local_storage.remove_item("token") +page.local_storage.clear +``` + +## items + +``` +def items +``` + + +Returns all items in the storage as name/value pairs. + +## get_item + +``` +def get_item(name) +``` + + +Returns the value for the given `name` if present. + +## set_item + +``` +def set_item(name, value) +``` + + +Sets the value for the given `name`. Overwrites any existing value for that name. + +## remove_item + +``` +def remove_item(name) +``` + + +Removes the item with the given `name`. No-op if the item is absent. + +## clear + +``` +def clear +``` + + +Removes all items from the storage. diff --git a/documentation/docs/include/api_coverage.md b/documentation/docs/include/api_coverage.md index 09f9a5a8..5e8dcb7c 100644 --- a/documentation/docs/include/api_coverage.md +++ b/documentation/docs/include/api_coverage.md @@ -365,8 +365,8 @@ * ~~wait_for_event~~ * clock * keyboard -* ~~local_storage~~ -* ~~session_storage~~ +* local_storage +* session_storage * mouse * request * screencast @@ -643,3 +643,11 @@ * not_to_match_aria_snapshot * to_have_title * to_have_url + +## WebStorage + +* items +* get_item +* set_item +* remove_item +* clear diff --git a/lib/playwright/channel_owners/page.rb b/lib/playwright/channel_owners/page.rb index 359a8c4b..f72dc28a 100644 --- a/lib/playwright/channel_owners/page.rb +++ b/lib/playwright/channel_owners/page.rb @@ -86,6 +86,14 @@ module Playwright :viewport_size, :main_frame + def local_storage + @local_storage ||= WebStorageImpl.new(self, 'local') + end + + def session_storage + @session_storage ||= WebStorageImpl.new(self, 'session') + end + private def on_frame_attached(frame) frame.send(:update_page_from_page, self) @frames << frame diff --git a/lib/playwright/web_storage_impl.rb b/lib/playwright/web_storage_impl.rb new file mode 100644 index 00000000..24768f59 --- /dev/null +++ b/lib/playwright/web_storage_impl.rb @@ -0,0 +1,34 @@ +module Playwright + # ref: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/webStorage.ts + define_api_implementation :WebStorageImpl do + # @param page [ChannelOwners::Page] + # @param kind [String] 'local' or 'session' + def initialize(page, kind) + @page = page + @kind = kind + end + + def items + @page.channel.send_message_to_server('webStorageItems', kind: @kind) + end + + def get_item(name) + @page.channel.send_message_to_server('webStorageGetItem', kind: @kind, name: name) + end + + def set_item(name, value) + @page.channel.send_message_to_server('webStorageSetItem', kind: @kind, name: name, value: value) + nil + end + + def remove_item(name) + @page.channel.send_message_to_server('webStorageRemoveItem', kind: @kind, name: name) + nil + end + + def clear + @page.channel.send_message_to_server('webStorageClear', kind: @kind) + nil + end + end +end diff --git a/spec/integration/page/local_storage_spec.rb b/spec/integration/page/local_storage_spec.rb new file mode 100644 index 00000000..7854d1a7 --- /dev/null +++ b/spec/integration/page/local_storage_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' + +# ref: https://github.com/microsoft/playwright/blob/v1.61.1/tests/page/page-localstorage.spec.ts +RSpec.describe 'Page#local_storage / #session_storage', sinatra: true do + it 'localStorage.items returns empty array on fresh origin' do + with_page do |page| + page.goto(server_empty_page) + expect(page.local_storage.items).to eq([]) + end + end + + it 'localStorage.getItem returns null for missing key' do + with_page do |page| + page.goto(server_empty_page) + expect(page.local_storage.get_item('absent')).to be_nil + end + end + + it 'localStorage.setItem persists and surfaces in items()/getItem()' do + with_page do |page| + page.goto(server_empty_page) + page.local_storage.set_item('alpha', '1') + page.local_storage.set_item('beta', '2') + + expect(page.local_storage.items).to match_array([ + { 'name' => 'alpha', 'value' => '1' }, + { 'name' => 'beta', 'value' => '2' }, + ]) + expect(page.local_storage.get_item('alpha')).to eq('1') + expect(page.evaluate("() => localStorage.getItem('alpha')")).to eq('1') + end + end + + it 'localStorage.setItem overwrites existing value' do + with_page do |page| + page.goto(server_empty_page) + page.local_storage.set_item('k', 'first') + page.local_storage.set_item('k', 'second') + expect(page.local_storage.get_item('k')).to eq('second') + end + end + + it 'localStorage.removeItem removes a single item' do + with_page do |page| + page.goto(server_empty_page) + page.local_storage.set_item('a', '1') + page.local_storage.set_item('b', '2') + + page.local_storage.remove_item('a') + expect(page.local_storage.items).to eq([{ 'name' => 'b', 'value' => '2' }]) + end + end + + it 'localStorage.clear empties storage' do + with_page do |page| + page.goto(server_empty_page) + page.local_storage.set_item('a', '1') + page.local_storage.set_item('b', '2') + + page.local_storage.clear + expect(page.local_storage.items).to eq([]) + end + end + + it 'sessionStorage round-trip' do + with_page do |page| + page.goto(server_empty_page) + expect(page.session_storage.items).to eq([]) + + page.session_storage.set_item('s1', 'v1') + page.session_storage.set_item('s2', 'v2') + expect(page.session_storage.items).to match_array([ + { 'name' => 's1', 'value' => 'v1' }, + { 'name' => 's2', 'value' => 'v2' }, + ]) + expect(page.session_storage.get_item('s1')).to eq('v1') + + page.session_storage.remove_item('s1') + expect(page.session_storage.items).to eq([{ 'name' => 's2', 'value' => 'v2' }]) + + page.session_storage.clear + expect(page.session_storage.items).to eq([]) + end + end + + it 'localStorage and sessionStorage are independent' do + with_page do |page| + page.goto(server_empty_page) + page.local_storage.set_item('shared', 'local') + page.session_storage.set_item('shared', 'session') + + expect(page.local_storage.get_item('shared')).to eq('local') + expect(page.session_storage.get_item('shared')).to eq('session') + + page.local_storage.clear + expect(page.local_storage.items).to eq([]) + expect(page.session_storage.get_item('shared')).to eq('session') + end + end + + it 'storage methods are scoped to the current origin' do + with_page do |page| + page.goto("#{server_prefix}/empty.html") + page.local_storage.set_item('k', 'origin-1') + + page.goto("#{server_cross_process_prefix}/empty.html") + expect(page.local_storage.items).to eq([]) + page.local_storage.set_item('k', 'origin-2') + + page.goto("#{server_prefix}/empty.html") + expect(page.local_storage.get_item('k')).to eq('origin-1') + end + end +end From 54c9f6fca8ce01dc78204716253f4a41ed22fc6f Mon Sep 17 00:00:00 2001 From: Kurt Ruppel Date: Fri, 26 Jun 2026 02:48:30 +0000 Subject: [PATCH 3/4] Implement BrowserContext#credentials (WebAuthn virtual authenticator) Adds the 1.61 WebAuthn API: browser_context.credentials exposes a virtual authenticator scoped to the context, with install, create, get and delete backed by the context channel's credentials* commands. Lets tests seed passkeys and answer navigator.credentials ceremonies without real hardware. Registers Credentials with the API generator, maps the class doc examples, and ports the upstream browsercontext-webauthn spec. --- development/generate_api.rb | 2 + development/generate_api/example_codes.rb | 53 ++++++ documentation/docs/api/browser_context.md | 6 + documentation/docs/api/credentials.md | 132 ++++++++++++++ documentation/docs/include/api_coverage.md | 9 +- .../channel_owners/browser_context.rb | 3 +- lib/playwright/credentials_impl.rb | 35 ++++ .../browser_context/webauthn_spec.rb | 172 ++++++++++++++++++ 8 files changed, 410 insertions(+), 2 deletions(-) create mode 100644 documentation/docs/api/credentials.md create mode 100644 lib/playwright/credentials_impl.rb create mode 100644 spec/integration/browser_context/webauthn_spec.rb diff --git a/development/generate_api.rb b/development/generate_api.rb index 76b78ac8..47cca8fc 100644 --- a/development/generate_api.rb +++ b/development/generate_api.rb @@ -32,6 +32,7 @@ LocatorAssertions PageAssertions WebStorage + Credentials ] EXPERIMENTAL = %w[ ] @@ -52,6 +53,7 @@ PageAssertions Selectors WebStorage + Credentials ] require 'bundler/setup' diff --git a/development/generate_api/example_codes.rb b/development/generate_api/example_codes.rb index f33c9c6a..b8f6a9b9 100644 --- a/development/generate_api/example_codes.rb +++ b/development/generate_api/example_codes.rb @@ -424,6 +424,59 @@ def example_585cbbd055f47a5d0d7a6197d90874436cd4a2d50a92956723fc69336f8ccee9(pag msg.args[2].json_value # => { 'foo' => 'bar' } end + # Credentials + def example_4b16944216ee5acfa4843ff0db2b6c1e50de625c9530e06362459de4998c5245(browser:) + context = browser.new_context + + # A passkey your backend already provisioned for a test user. + context.credentials.create( + "example.com", + id: known_credential_id, # base64url + userHandle: known_user_handle, # base64url + privateKey: known_private_key, # base64url PKCS#8 (DER) + publicKey: known_public_key, # base64url SPKI (DER) + ) + context.credentials.install + + page = context.new_page + page.goto("https://example.com/login") + # The page's navigator.credentials.get() is answered with the seeded passkey. + end + + # Credentials + def example_a56dcffc601af859fb6f2992dd574a0d97b9a8e42aeba15d308be51411ede1ce(browser:) + # setup test: let the app register a passkey, then save it. + context = browser.new_context + context.credentials.install + + page = context.new_page + page.goto("https://example.com/register") + page.get_by_role("button", name: "Create a passkey").click + + # Read back the passkey the page registered - it includes the private key. + credential = context.credentials.get(rpId: "example.com").first + File.write("playwright/.auth/passkey.json", JSON.generate(credential)) + end + + # Credentials + def example_4418a8e24419f983d07a8957cbd2f580f7aced3efc4f94457f8c263bb197c482(browser:) + # later test: seed the captured passkey so the app starts already enrolled. + credential = JSON.parse(File.read("playwright/.auth/passkey.json")) + context = browser.new_context + context.credentials.create( + credential["rpId"], + id: credential["id"], + userHandle: credential["userHandle"], + privateKey: credential["privateKey"], + publicKey: credential["publicKey"], + ) + context.credentials.install + + page = context.new_page + page.goto("https://example.com/login") + # navigator.credentials.get() resolves the captured passkey - already signed in. + end + # Dialog def example_a7dcc75b7aa5544237ac3a964e9196d0445308864d3ce820f8cb8396f687b04a(page:) def handle_dialog(dialog) diff --git a/documentation/docs/api/browser_context.md b/documentation/docs/api/browser_context.md index 4c979bd5..4c2350f7 100644 --- a/documentation/docs/api/browser_context.md +++ b/documentation/docs/api/browser_context.md @@ -521,6 +521,12 @@ Will throw an error if the context closes before new [Page](./page) is created. Playwright has ability to mock clock and passage of time. +## credentials + + +Virtual WebAuthn authenticator for this context. Lets tests seed credentials and intercept +`navigator.credentials.create()` / `navigator.credentials.get()` ceremonies. + ## request diff --git a/documentation/docs/api/credentials.md b/documentation/docs/api/credentials.md new file mode 100644 index 00000000..109978fb --- /dev/null +++ b/documentation/docs/api/credentials.md @@ -0,0 +1,132 @@ +--- +sidebar_position: 10 +--- + +# Credentials + + +[Credentials](./credentials) is a virtual WebAuthn authenticator scoped to a [BrowserContext](./browser_context). It lets tests +register passkeys and answer `navigator.credentials.create()` / `navigator.credentials.get()` +ceremonies in the page, without a real authenticator or hardware security key. + +There are two common ways to use it: + +**Usage: seed a known credential** + +```ruby +context = browser.new_context + +# A passkey your backend already provisioned for a test user. +context.credentials.create( + "example.com", + id: known_credential_id, # base64url + userHandle: known_user_handle, # base64url + privateKey: known_private_key, # base64url PKCS#8 (DER) + publicKey: known_public_key, # base64url SPKI (DER) +) +context.credentials.install + +page = context.new_page +page.goto("https://example.com/login") +# The page's navigator.credentials.get() is answered with the seeded passkey. +``` + +**Usage: capture a passkey, then reuse it** + +```ruby +# setup test: let the app register a passkey, then save it. +context = browser.new_context +context.credentials.install + +page = context.new_page +page.goto("https://example.com/register") +page.get_by_role("button", name: "Create a passkey").click + +# Read back the passkey the page registered - it includes the private key. +credential = context.credentials.get(rpId: "example.com").first +File.write("playwright/.auth/passkey.json", JSON.generate(credential)) +``` + +```ruby +# later test: seed the captured passkey so the app starts already enrolled. +credential = JSON.parse(File.read("playwright/.auth/passkey.json")) +context = browser.new_context +context.credentials.create( + credential["rpId"], + id: credential["id"], + userHandle: credential["userHandle"], + privateKey: credential["privateKey"], + publicKey: credential["publicKey"], +) +context.credentials.install + +page = context.new_page +page.goto("https://example.com/login") +# navigator.credentials.get() resolves the captured passkey - already signed in. +``` + +**Defaults** + +## install + +``` +def install +``` + + +Installs the virtual WebAuthn authenticator into the context, overriding +`navigator.credentials.create()` and `navigator.credentials.get()` in all current +and future pages. Call this before the page first touches `navigator.credentials`. + +Required: until [Credentials#install](./credentials#install) is called, no interception is in place and the page sees +the platform's native (or absent) WebAuthn behaviour. Seeding credentials with +[Credentials#create](./credentials#create) without installing populates the authenticator, but the +page will never see those credentials. + +## create + +``` +def create( + rpId, + id: nil, + privateKey: nil, + publicKey: nil, + userHandle: nil) +``` + + +Seeds a virtual WebAuthn credential and returns it. + +With only `rpId`, generates a fresh **ECDSA P-256** keypair, credential id and user handle. The +seeded credential is discoverable (resident), so the page can resolve it from both +username-then-passkey and usernameless passkey flows. The returned object carries the private and public keys, so it can be persisted to disk and re-seeded in a later test. + +To **import a known credential**, supply all four of `id`, `userHandle`, `privateKey` and +`publicKey` together. + +Call [Credentials#install](./credentials#install) before navigating to a page that uses WebAuthn. + +## delete + +``` +def delete(id) +``` + + +Removes a credential from the authenticator by its id. Works for any credential currently held — +both those seeded with [Credentials#create](./credentials#create) and those the page registered itself by +calling `navigator.credentials.create()`. + +## get + +``` +def get(id: nil, rpId: nil) +``` + + +Returns every credential currently held by the authenticator, optionally filtered by `rpId` or +`id`. This includes both credentials seeded with [Credentials#create](./credentials#create) and credentials +the page registered itself by calling `navigator.credentials.create()`. + +Each returned credential includes its private and public keys, so a passkey the app just +registered can be saved and re-seeded into a later test with [Credentials#create](./credentials#create) — see the second example in the class overview. diff --git a/documentation/docs/include/api_coverage.md b/documentation/docs/include/api_coverage.md index 5e8dcb7c..dd414ab1 100644 --- a/documentation/docs/include/api_coverage.md +++ b/documentation/docs/include/api_coverage.md @@ -407,7 +407,7 @@ * expect_page * ~~wait_for_event~~ * clock -* ~~credentials~~ +* credentials * ~~debugger~~ * request * tracing @@ -651,3 +651,10 @@ * set_item * remove_item * clear + +## Credentials + +* install +* create +* delete +* get diff --git a/lib/playwright/channel_owners/browser_context.rb b/lib/playwright/channel_owners/browser_context.rb index 28622141..af723e61 100644 --- a/lib/playwright/channel_owners/browser_context.rb +++ b/lib/playwright/channel_owners/browser_context.rb @@ -5,7 +5,7 @@ module Playwright attr_accessor :browser attr_writer :owner_page, :options - attr_reader :clock, :tracing, :request + attr_reader :clock, :tracing, :request, :credentials private def after_initialize @options = @initializer['options'] @@ -20,6 +20,7 @@ module Playwright @request = ChannelOwners::APIRequestContext.from(@initializer['requestContext']) @request.send(:_update_timeout_settings, @timeout_settings) @clock = ClockImpl.new(self) + @credentials = CredentialsImpl.new(self) @channel.on('bindingCall', ->(params) { on_binding(ChannelOwners::BindingCall.from(params['binding'])) }) @channel.once('close', ->(_) { on_close }) diff --git a/lib/playwright/credentials_impl.rb b/lib/playwright/credentials_impl.rb new file mode 100644 index 00000000..52aa6554 --- /dev/null +++ b/lib/playwright/credentials_impl.rb @@ -0,0 +1,35 @@ +module Playwright + # ref: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/client/credentials.ts + define_api_implementation :CredentialsImpl do + # @param browser_context [ChannelOwners::BrowserContext] + def initialize(browser_context) + @browser_context = browser_context + end + + def install + @browser_context.channel.send_message_to_server('credentialsInstall') + nil + end + + def create(rpId, id: nil, userHandle: nil, privateKey: nil, publicKey: nil) + params = { + rpId: rpId, + id: id, + userHandle: userHandle, + privateKey: privateKey, + publicKey: publicKey, + }.compact + @browser_context.channel.send_message_to_server('credentialsCreate', params) + end + + def get(id: nil, rpId: nil) + params = { id: id, rpId: rpId }.compact + @browser_context.channel.send_message_to_server('credentialsGet', params) + end + + def delete(id) + @browser_context.channel.send_message_to_server('credentialsDelete', id: id) + nil + end + end +end diff --git a/spec/integration/browser_context/webauthn_spec.rb b/spec/integration/browser_context/webauthn_spec.rb new file mode 100644 index 00000000..81e82193 --- /dev/null +++ b/spec/integration/browser_context/webauthn_spec.rb @@ -0,0 +1,172 @@ +require 'spec_helper' + +# ref: https://github.com/microsoft/playwright/blob/v1.61.1/tests/library/browsercontext-webauthn.spec.ts +RSpec.describe 'BrowserContext#credentials (WebAuthn virtual authenticator)', sinatra: true do + # localhost is a secure context, so navigator.credentials is available. + let(:hostname) { 'localhost' } + + # Shared JS helper: WebAuthn assertion (sign-in) ceremony. + GET_ASSERTION_JS = <<~JS + async ({ rpId, credentialId }) => { + const b64UrlToBytes = (s) => { + let str = s.replace(/-/g, '+').replace(/_/g, '/'); + while (str.length % 4) + str += '='; + const bin = atob(str); + const u8 = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) + u8[i] = bin.charCodeAt(i); + return u8; + }; + const challenge = crypto.getRandomValues(new Uint8Array(32)); + const allowCredentials = credentialId + ? [{ type: 'public-key', id: b64UrlToBytes(credentialId) }] + : undefined; + const cred = await navigator.credentials.get({ + publicKey: { challenge, rpId, allowCredentials, userVerification: 'preferred' }, + }); + const resp = cred.response; + return { + id: cred.id, + type: cred.type, + hasClientData: resp.clientDataJSON.byteLength > 0, + hasAuthData: resp.authenticatorData.byteLength > 0, + hasSignature: resp.signature.byteLength > 0, + authDataFlags: new Uint8Array(resp.authenticatorData)[32], + }; + } + JS + + it 'should not intercept navigator.credentials without install()' do + context = browser.new_context + begin + # Seed a credential, but do not install the interceptor. + context.credentials.create(hostname) + page = context.new_page + page.goto(server_empty_page) + + intercepted = page.evaluate("() => globalThis.__pwWebAuthnInstalled === true") + expect(intercepted).to eq(false) + ensure + context.close + end + end + + it 'should seed a known credential and authenticate' do + source = browser.new_context + context = browser.new_context + begin + # The easiest way to create credentials; in practice this comes from the environment. + known = source.credentials.create(hostname) + + # A fresh context imports the known credential and signs in with it. + context.credentials.create( + known['rpId'], + id: known['id'], + userHandle: known['userHandle'], + privateKey: known['privateKey'], + publicKey: known['publicKey'], + ) + context.credentials.install + page = context.new_page + page.goto(server_empty_page) + + result = page.evaluate(GET_ASSERTION_JS, arg: { rpId: hostname, credentialId: known['id'] }) + + expect(result['id']).to eq(known['id']) + expect(result['type']).to eq('public-key') + expect(result['hasClientData']).to eq(true) + expect(result['hasAuthData']).to eq(true) + expect(result['hasSignature']).to eq(true) + # UP (0x01) | UV (0x04) = 0x05 + expect(result['authDataFlags'] & 0x05).to eq(0x05) + + # After the credential is deleted, the page can no longer authenticate with it. + context.credentials.delete(known['id']) + expect(context.credentials.get).to be_empty + + error = page.evaluate(<<~JS, arg: { rpId: hostname, credentialId: known['id'] }) + async ({ rpId, credentialId }) => { + const b64UrlToBytes = (s) => { + let str = s.replace(/-/g, '+').replace(/_/g, '/'); + while (str.length % 4) + str += '='; + const bin = atob(str); + const u8 = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) + u8[i] = bin.charCodeAt(i); + return u8; + }; + const challenge = crypto.getRandomValues(new Uint8Array(32)); + try { + await navigator.credentials.get({ + publicKey: { + challenge, + rpId, + allowCredentials: [{ type: 'public-key', id: b64UrlToBytes(credentialId) }], + }, + }); + return 'no-error'; + } catch (e) { + return e.name; + } + } + JS + expect(error).to eq('NotAllowedError') + ensure + source.close + context.close + end + end + + it 'should capture a page-created credential and reuse it in another context' do + setup_context = browser.new_context + context = browser.new_context + begin + # Setup context: the app registers a passkey via navigator.credentials.create(). + setup_context.credentials.install + setup_page = setup_context.new_page + setup_page.goto(server_empty_page) + + created_id = setup_page.evaluate(<<~JS, arg: { rpId: hostname }) + async ({ rpId }) => { + const challenge = crypto.getRandomValues(new Uint8Array(32)); + const created = await navigator.credentials.create({ + publicKey: { + challenge, + rp: { id: rpId, name: 'Test RP' }, + user: { id: new Uint8Array([1, 2, 3, 4]), name: 'u', displayName: 'User' }, + pubKeyCredParams: [{ type: 'public-key', alg: -7 }], + authenticatorSelection: { residentKey: 'required', userVerification: 'preferred' }, + }, + }); + return created.id; + } + JS + + captured = setup_context.credentials.get(rpId: hostname).first + expect(captured['id']).to eq(created_id) + expect(captured['privateKey']).to match(/\A[A-Za-z0-9_-]+\z/) + expect(captured['publicKey']).to match(/\A[A-Za-z0-9_-]+\z/) + + # Reuse the captured passkey in a fresh context and sign in with it. + context.credentials.create( + captured['rpId'], + id: captured['id'], + userHandle: captured['userHandle'], + privateKey: captured['privateKey'], + publicKey: captured['publicKey'], + ) + context.credentials.install + page = context.new_page + page.goto(server_empty_page) + + # No allowCredentials - relies on the re-seeded credential being discoverable. + got_id = page.evaluate(GET_ASSERTION_JS, arg: { rpId: hostname, credentialId: nil }) + expect(got_id['id']).to eq(created_id) + ensure + setup_context.close + context.close + end + end +end From f6583cb8dadb1ed9cacf69c0564f0753983b22bc Mon Sep 17 00:00:00 2001 From: Kurt Ruppel Date: Fri, 26 Jun 2026 02:52:28 +0000 Subject: [PATCH 4/4] Implement APIResponse#security_details and #server_addr Exposes the 1.61 APIResponse fields: security_details returns the TLS certificate details for HTTPS responses (nil for plain HTTP) and server_addr returns the resolved IP address and port, both read from the fetch response initializer. Ports the upstream global-fetch specs covering these methods. --- documentation/docs/api/api_response.md | 18 ++++++++ documentation/docs/include/api_coverage.md | 4 +- lib/playwright/api_response_impl.rb | 8 ++++ spec/integration/api_response_spec.rb | 50 ++++++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 spec/integration/api_response_spec.rb diff --git a/documentation/docs/api/api_response.md b/documentation/docs/api/api_response.md index d223c54a..7fc3af3a 100644 --- a/documentation/docs/api/api_response.md +++ b/documentation/docs/api/api_response.md @@ -77,6 +77,24 @@ def ok Contains a boolean stating whether the response was successful (status in the range 200-299) or not. +## security_details + +``` +def security_details +``` + + +Returns SSL and other security information. Resolves to `null` for non-HTTPS responses. For redirected requests, returns the information for the last request in the redirect chain. + +## server_addr + +``` +def server_addr +``` + + +Returns the IP address and port of the server. Resolves to `null` if the server address is not available. For redirected requests, returns the information for the last request in the redirect chain. + ## status ``` diff --git a/documentation/docs/include/api_coverage.md b/documentation/docs/include/api_coverage.md index dd414ab1..22863139 100644 --- a/documentation/docs/include/api_coverage.md +++ b/documentation/docs/include/api_coverage.md @@ -556,8 +556,8 @@ * headers_array * json * ok -* ~~security_details~~ -* ~~server_addr~~ +* security_details +* server_addr * status * status_text * text diff --git a/lib/playwright/api_response_impl.rb b/lib/playwright/api_response_impl.rb index a0d3ec91..5e0ee7aa 100644 --- a/lib/playwright/api_response_impl.rb +++ b/lib/playwright/api_response_impl.rb @@ -39,6 +39,14 @@ def headers_array @headers.headers_array end + def security_details + @initializer['securityDetails'] + end + + def server_addr + @initializer['serverAddr'] + end + class AlreadyDisposedError < StandardError def initialize super('Response has been disposed') diff --git a/spec/integration/api_response_spec.rb b/spec/integration/api_response_spec.rb new file mode 100644 index 00000000..214682e0 --- /dev/null +++ b/spec/integration/api_response_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +# ref: https://github.com/microsoft/playwright/blob/v1.61.1/tests/library/global-fetch.spec.ts +RSpec.describe 'APIResponse#server_addr / #security_details', sinatra: true do + def asset(path) + File.join(__dir__, '../', 'assets', path) + end + + it 'should return server address from response' do + with_context do |context| + # The second request reuses the keep-alive socket and should report the address as well. + 2.times do + response = context.request.get(server_empty_page) + addr = response.server_addr + expect(addr['ipAddress']).to match(/\A(127\.0\.0\.1|::1)\z/) + expect(addr['port']).to eq(server_port) + end + end + end + + it 'should return null security details for http response' do + with_context do |context| + response = context.request.get(server_empty_page) + expect(response.security_details).to be_nil + end + end + + context 'over https', tls: true do + # The local TLS server requires mutual TLS, so supply trusted client certs. + it 'should return security details from response' do + options = { + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: server_prefix, + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + } + with_context(**options) do |context| + # The second request reuses the keep-alive socket and should report the details as well. + 2.times do + response = context.request.get(server_empty_page) + details = response.security_details + expect(details).not_to be_nil + expect(details.keys).to include('protocol', 'subjectName', 'issuer', 'validFrom', 'validTo') + end + end + end + end +end