diff --git a/docs/src/api/class-credentials.md b/docs/src/api/class-credentials.md index 22aa5ee6e7c18..0b73671d4b133 100644 --- a/docs/src/api/class-credentials.md +++ b/docs/src/api/class-credentials.md @@ -33,6 +33,76 @@ await page.goto('https://example.com/login'); // The page's navigator.credentials.get() is answered with the seeded passkey. ``` +```java +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. +``` + +```python async +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. +``` + +```python sync +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. +``` + +```csharp +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. +``` + **Usage: capture a passkey, then reuse it** ```js @@ -49,6 +119,65 @@ const [credential] = await context.credentials.get({ rpId: 'example.com' }); fs.writeFileSync('playwright/.auth/passkey.json', JSON.stringify(credential)); ``` +```java +// 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)); +``` + +```python async +# 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) +``` + +```python sync +# 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) +``` + +```csharp +// 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])); +``` + ```js // later test: seed the captured passkey so the app starts already enrolled. const credential = JSON.parse(fs.readFileSync('playwright/.auth/passkey.json', 'utf8')); @@ -61,6 +190,80 @@ await page.goto('https://example.com/login'); // navigator.credentials.get() resolves the captured passkey — already signed in. ``` +```java +// 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. +``` + +```python async +# 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. +``` + +```python sync +# 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. +``` + +```csharp +// 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. +``` + **Defaults** - The authenticator presents itself as a **platform** authenticator (`authenticatorAttachment` is diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md index f24cd9eaaa98b..1f1849bc12eff 100644 --- a/docs/src/release-notes-csharp.md +++ b/docs/src/release-notes-csharp.md @@ -6,6 +6,65 @@ toc_max_heading_level: 2 import LiteYouTube from '@site/src/components/LiteYouTube'; +## Version 1.61 + +### 🔑 WebAuthn passkeys + +New [Credentials] virtual authenticator, available via [`property: BrowserContext.credentials`], lets tests register passkeys and answer `navigator.credentials.create()` / `navigator.credentials.get()` ceremonies in the page — no real hardware key required, works in all browsers: + +```csharp +var context = await browser.NewContextAsync(); + +// Seed a passkey your backend provisioned for a test user. +await context.Credentials.CreateAsync("example.com", new() +{ + Id = credentialId, + UserHandle = userHandle, + PrivateKey = privateKey, + PublicKey = publicKey, +}); +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. +``` + +You can also let the app register a passkey once in a setup test, read it back with [`method: Credentials.get`], and seed it into later tests — see [Credentials] for details. + +### 🗃️ Web Storage + +New [WebStorage] API, available via [`property: Page.localStorage`] and [`property: Page.sessionStorage`], reads and writes the page's storage for the current origin: + +```csharp +await page.LocalStorage.SetItemAsync("token", "abc"); +var token = await page.LocalStorage.GetItemAsync("token"); +var items = await page.SessionStorage.ItemsAsync(); +``` + +### New APIs + +- [`method: APIResponse.securityDetails`] and [`method: APIResponse.serverAddr`] mirror the browser-side [`method: Response.securityDetails`] and [`method: Response.serverAddr`]. +- New option `ArtifactsDir` in [`method: BrowserType.connectOverCDP`] controls where artifacts such as traces and downloads are stored when attached to an existing browser. +- New option `Cursor` in [`method: Screencast.showActions`] controls the cursor decoration rendered for pointer actions. +- The `OnFrame` callback in [`method: Screencast.start`] now receives a `Timestamp` of when the frame was presented by the browser. + +### 🛠️ Other improvements + +- Playwright now supports Ubuntu 26.04. + +### Browser Versions + +- Chromium 149.0.7827.55 +- Mozilla Firefox 151.0 +- WebKit 26.5 + +This version was also tested against the following stable channels: + +- Google Chrome 149 +- Microsoft Edge 149 + + ## Version 1.60 ### 🌐 HAR recording on Tracing diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md index 70497e4a725f9..56ef387927d17 100644 --- a/docs/src/release-notes-java.md +++ b/docs/src/release-notes-java.md @@ -6,6 +6,63 @@ toc_max_heading_level: 2 import LiteYouTube from '@site/src/components/LiteYouTube'; +## Version 1.61 + +### 🔑 WebAuthn passkeys + +New [Credentials] virtual authenticator, available via [`property: BrowserContext.credentials`], lets tests register passkeys and answer `navigator.credentials.create()` / `navigator.credentials.get()` ceremonies in the page — no real hardware key required, works in all browsers: + +```java +BrowserContext context = browser.newContext(); + +// Seed a passkey your backend provisioned for a test user. +context.credentials().create("example.com", new Credentials.CreateOptions() + .setId(credentialId) + .setUserHandle(userHandle) + .setPrivateKey(privateKey) + .setPublicKey(publicKey)); +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. +``` + +You can also let the app register a passkey once in a setup test, read it back with [`method: Credentials.get`], and seed it into later tests — see [Credentials] for details. + +### 🗃️ Web Storage + +New [WebStorage] API, available via [`property: Page.localStorage`] and [`property: Page.sessionStorage`], reads and writes the page's storage for the current origin: + +```java +page.localStorage().setItem("token", "abc"); +String token = page.localStorage().getItem("token"); +List items = page.sessionStorage().items(); +``` + +### New APIs + +- [`method: APIResponse.securityDetails`] and [`method: APIResponse.serverAddr`] mirror the browser-side [`method: Response.securityDetails`] and [`method: Response.serverAddr`]. +- New option `artifactsDir` in [`method: BrowserType.connectOverCDP`] controls where artifacts such as traces and downloads are stored when attached to an existing browser. +- New option `cursor` in [`method: Screencast.showActions`] controls the cursor decoration rendered for pointer actions. +- The `onFrame` callback in [`method: Screencast.start`] now receives a `timestamp` of when the frame was presented by the browser. + +### 🛠️ Other improvements + +- Playwright now supports Ubuntu 26.04. + +### Browser Versions + +- Chromium 149.0.7827.55 +- Mozilla Firefox 151.0 +- WebKit 26.5 + +This version was also tested against the following stable channels: + +- Google Chrome 149 +- Microsoft Edge 149 + + ## Version 1.60 ### 🌐 HAR recording on Tracing diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md index 9e0454a7f491d..2cb250cd6729e 100644 --- a/docs/src/release-notes-js.md +++ b/docs/src/release-notes-js.md @@ -6,6 +6,78 @@ toc_max_heading_level: 2 import LiteYouTube from '@site/src/components/LiteYouTube'; +## Version 1.61 + +### 🔑 WebAuthn passkeys + +New [Credentials] virtual authenticator, available via [`property: BrowserContext.credentials`], lets tests register passkeys and answer `navigator.credentials.create()` / `navigator.credentials.get()` ceremonies in the page — no real hardware key required, works in all browsers: + +```js +const context = await browser.newContext(); + +// Seed a passkey your backend provisioned for a test user. +await context.credentials.create('example.com', { + id: credentialId, + userHandle, + privateKey, + publicKey, +}); +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. +``` + +You can also let the app register a passkey once in a setup test, read it back with [`method: Credentials.get`], and seed it into later tests — see [Credentials] for details. + +### 🗃️ Web Storage + +New [WebStorage] API, available via [`property: Page.localStorage`] and [`property: Page.sessionStorage`], reads and writes the page's storage for the current origin: + +```js +await page.localStorage.setItem('token', 'abc'); +const token = await page.localStorage.getItem('token'); +const items = await page.sessionStorage.items(); +``` + +### New APIs + +#### Network + +- [`method: APIResponse.securityDetails`] and [`method: APIResponse.serverAddr`] mirror the browser-side [`method: Response.securityDetails`] and [`method: Response.serverAddr`]. + +#### Browser and Screencast + +- New option `artifactsDir` in [`method: BrowserType.connectOverCDP`] controls where artifacts such as traces and downloads are stored when attached to an existing browser. +- New option `cursor` in [`method: Screencast.showActions`] controls the cursor decoration rendered for pointer actions. +- The `onFrame` callback in [`method: Screencast.start`] now receives a `timestamp` of when the frame was presented by the browser. + +#### Test runner + +- The [`property: TestOptions.video`] option now supports the same set of modes as `trace`: new `'on-all-retries'`, `'retain-on-first-failure'` and `'retain-on-failure-and-retries'` values. See the [video modes table](./test-use-options.md#video-modes) for which runs are recorded and kept in each mode. +- Supported `expect.soft.poll(...)`. +- New [`property: FullConfig.argv`] — a snapshot of `process.argv` from the runner process, handy for reading custom arguments passed after the `--` separator. +- New [`property: FullConfig.failOnFlakyTests`] mirrors the config option, so reporters can explain why a flaky run failed. +- [`property: TestInfo.errors`] now lists each sub-error of an `AggregateError` as a separate entry. +- New `-G` command line shorthand for `--grep-invert`. + +### 🛠️ Other improvements + +- Playwright now supports Ubuntu 26.04. + +### Browser Versions + +- Chromium 149.0.7827.55 +- Mozilla Firefox 151.0 +- WebKit 26.5 + +This version was also tested against the following stable channels: + +- Google Chrome 149 +- Microsoft Edge 149 + + ## Version 1.60 ### 🌐 HAR recording on Tracing diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md index 91dd0a1fed3c2..c72223d5ab293 100644 --- a/docs/src/release-notes-python.md +++ b/docs/src/release-notes-python.md @@ -6,6 +6,64 @@ toc_max_heading_level: 2 import LiteYouTube from '@site/src/components/LiteYouTube'; +## Version 1.61 + +### 🔑 WebAuthn passkeys + +New [Credentials] virtual authenticator, available via [`property: BrowserContext.credentials`], lets tests register passkeys and answer `navigator.credentials.create()` / `navigator.credentials.get()` ceremonies in the page — no real hardware key required, works in all browsers: + +```python +context = browser.new_context() + +# Seed a passkey your backend provisioned for a test user. +context.credentials.create("example.com", + id=credential_id, + user_handle=user_handle, + private_key=private_key, + public_key=public_key, +) +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. +``` + +You can also let the app register a passkey once in a setup test, read it back with [`method: Credentials.get`], and seed it into later tests — see [Credentials] for details. + +### 🗃️ Web Storage + +New [WebStorage] API, available via [`property: Page.localStorage`] and [`property: Page.sessionStorage`], reads and writes the page's storage for the current origin: + +```python +page.local_storage.set_item("token", "abc") +token = page.local_storage.get_item("token") +items = page.session_storage.items() +``` + +### New APIs + +- [`method: APIResponse.securityDetails`] and [`method: APIResponse.serverAddr`] mirror the browser-side [`method: Response.securityDetails`] and [`method: Response.serverAddr`]. +- New option `artifacts_dir` in [`method: BrowserType.connectOverCDP`] controls where artifacts such as traces and downloads are stored when attached to an existing browser. +- New option `cursor` in [`method: Screencast.showActions`] controls the cursor decoration rendered for pointer actions. +- The `on_frame` callback in [`method: Screencast.start`] now receives a `timestamp` of when the frame was presented by the browser. + +### 🛠️ Other improvements + +- Playwright now supports Ubuntu 26.04. + +### Browser Versions + +- Chromium 149.0.7827.55 +- Mozilla Firefox 151.0 +- WebKit 26.5 + +This version was also tested against the following stable channels: + +- Google Chrome 149 +- Microsoft Edge 149 + + ## Version 1.60 ### 🌐 HAR recording on Tracing