From 5a681f51f78055b7e95db029aa40b167171e38dd Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 26 Jun 2026 00:40:49 -0700 Subject: [PATCH 1/7] chore: roll to Playwright v1.61.1 Roll the bundled driver from v1.60.0 to v1.61.1 and port the new client surface. New APIs (mirroring playwright-python #3102 / playwright-java #1929): - WebStorage class + Page.LocalStorage()/SessionStorage() - Credentials class (WebAuthn virtual authenticator) + BrowserContext.Credentials() - APIResponse.SecurityDetails()/ServerAddr() - Screencast cursor option + ScreencastFrame timestamp Behavior changes ported: - Page.close/runBeforeUnload protocol split (upstream #40780) - assertion expect failures now arrive as protocol errors carrying errorDetails instead of a { matches: false } result (upstream); handled in both expect call-sites (locator + page assertions) - client-certificate interceptor now scopes certs to the matching origin, so an origin-mismatch navigation aborts the TLS handshake; updated the test Also fixes scripts/apply-patch.sh to always check out the pinned tag (previously the first roll on a fresh submodule patched the old version). --- .gitignore | 2 +- README.md | 8 +- browser_context.go | 6 + connection.go | 10 ++ credentials.go | 46 ++++++ errors.go | 14 ++ fetch.go | 22 +++ generated-enums.go | 12 ++ generated-interfaces.go | 96 +++++++++++- generated-structs.go | 90 ++++++++--- locator.go | 35 +++-- page.go | 22 ++- page_assertions.go | 26 ++-- patches/main.patch | 145 ++++++++++-------- playwright | 2 +- run.go | 2 +- screencast.go | 16 +- scripts/apply-patch.sh | 8 +- ...rowser_context_client_certificates_test.go | 41 +++-- tests/browser_context_credentials_test.go | 44 ++++++ tests/fetch_test.go | 20 +++ tests/page_web_storage_test.go | 90 +++++++++++ tests/screencast_test.go | 77 ++++++++++ transport.go | 3 + web_storage.go | 55 +++++++ 25 files changed, 750 insertions(+), 142 deletions(-) create mode 100644 credentials.go create mode 100644 tests/browser_context_credentials_test.go create mode 100644 tests/page_web_storage_test.go create mode 100644 tests/screencast_test.go create mode 100644 web_storage.go diff --git a/.gitignore b/.gitignore index abd23e70..01f82330 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ _site/ flakiness-report/ .vscode/settings.json -.claude/ \ No newline at end of file +.claude/worktrees/ \ No newline at end of file diff --git a/README.md b/README.md index 3be92fa8..1d7d81ca 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![PkgGoDev](https://pkg.go.dev/badge/github.com/playwright-community/playwright-go)](https://pkg.go.dev/github.com/playwright-community/playwright-go) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](http://opensource.org/licenses/MIT) [![Go Report Card](https://goreportcard.com/badge/github.com/playwright-community/playwright-go)](https://goreportcard.com/report/github.com/playwright-community/playwright-go) ![Build Status](https://github.com/playwright-community/playwright-go/workflows/Go/badge.svg) [![Tests](https://img.shields.io/endpoint?url=https%3A%2F%2Fflakiness.io%2Fapi%2Fbadge%3Finput%3D%257B%2522badgeToken%2522%253A%2522badge-6g4pNCL3d8qZbDdJEqFhSI%2522%257D)](https://flakiness.io/playwright-community/playwright-go) -[![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) [![Coverage Status](https://coveralls.io/repos/github/playwright-community/playwright-go/badge.svg?branch=main)](https://coveralls.io/github/playwright-community/playwright-go?branch=main) [![Chromium version](https://img.shields.io/badge/chromium-148.0.7778.96-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-150.0.2-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.4-blue.svg?logo=safari)](https://webkit.org/) +[![Join Discord](https://img.shields.io/badge/join-discord-informational)](https://aka.ms/playwright/discord) [![Coverage Status](https://coveralls.io/repos/github/playwright-community/playwright-go/badge.svg?branch=main)](https://coveralls.io/github/playwright-community/playwright-go?branch=main) [![Chromium version](https://img.shields.io/badge/chromium-149.0.7827.55-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-151.0-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-26.5-blue.svg?logo=safari)](https://webkit.org/) [API reference](https://playwright.dev/docs/api/class-playwright) | [Example recipes](https://github.com/playwright-community/playwright-go/tree/main/examples) @@ -13,9 +13,9 @@ Playwright is a Go library to automate [Chromium](https://www.chromium.org/Home) | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 148.0.7778.96 | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| WebKit 26.4 | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Firefox 150.0.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 149.0.7827.55 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| WebKit 26.5 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 151.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | Headless execution is supported for all the browsers on all platforms. diff --git a/browser_context.go b/browser_context.go index 634bfcb7..321e5a11 100644 --- a/browser_context.go +++ b/browser_context.go @@ -38,12 +38,17 @@ type browserContextImpl struct { closeReason *string harRouters []*harRouter clock Clock + credentials Credentials } func (b *browserContextImpl) Clock() Clock { return b.clock } +func (b *browserContextImpl) Credentials() Credentials { + return b.credentials +} + func (b *browserContextImpl) SetDefaultNavigationTimeout(timeout float64) { b.setDefaultNavigationTimeoutImpl(&timeout) } @@ -923,6 +928,7 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini // upstream (this.request._timeoutSettings = this._timeoutSettings). bt.request.timeoutSettings = bt.timeoutSettings bt.clock = newClock(bt) + bt.credentials = newCredentials(bt) // Register this context with the selectors manager for custom selector engines if bt.browser != nil && bt.browser.browserType != nil { diff --git a/connection.go b/connection.go index b9ab2ba8..e6f58468 100644 --- a/connection.go +++ b/connection.go @@ -131,6 +131,16 @@ func (c *connection) Dispatch(msg *message) { if log := formatCallLog(msg.Log); log != "" { err = fmt.Errorf("%w%s", err, log) } + // Preserve structured errorDetails (e.g. assertion `expect` failures + // in v1.61+) so callers can reconstruct the result via errors.As. + if msg.ErrorDetails != nil { + details, derr := c.replaceGuidsWithChannels(msg.ErrorDetails) + if derr == nil { + if detailsMap, ok := details.(map[string]any); ok { + err = &errorWithDetails{err: err, details: detailsMap, log: msg.Log} + } + } + } cb.SetError(err) } else { // Always resolve GUIDs in responses, regardless of connection type diff --git a/credentials.go b/credentials.go new file mode 100644 index 00000000..7387a331 --- /dev/null +++ b/credentials.go @@ -0,0 +1,46 @@ +package playwright + +type credentialsImpl struct { + browserCtx *browserContextImpl +} + +func newCredentials(bCtx *browserContextImpl) Credentials { + return &credentialsImpl{browserCtx: bCtx} +} + +func (c *credentialsImpl) Install() error { + _, err := c.browserCtx.channel.Send("credentialsInstall") + return err +} + +func (c *credentialsImpl) Create(rpId string, options ...CredentialsCreateOptions) (*VirtualCredential, error) { + overrides := map[string]any{"rpId": rpId} + result, err := c.browserCtx.channel.SendReturnAsDict("credentialsCreate", options, overrides) + if err != nil { + return nil, err + } + credential := &VirtualCredential{} + remapMapToStruct(result["credential"], credential) + return credential, nil +} + +func (c *credentialsImpl) Delete(id string) error { + _, err := c.browserCtx.channel.Send("credentialsDelete", map[string]any{"id": id}) + return err +} + +func (c *credentialsImpl) Get(options ...CredentialsGetOptions) ([]VirtualCredential, error) { + result, err := c.browserCtx.channel.SendReturnAsDict("credentialsGet", options) + if err != nil { + return nil, err + } + credentials := make([]VirtualCredential, 0) + if rawCredentials, ok := result["credentials"].([]any); ok { + for _, raw := range rawCredentials { + credential := VirtualCredential{} + remapMapToStruct(raw, &credential) + credentials = append(credentials, credential) + } + } + return credentials, nil +} diff --git a/errors.go b/errors.go index 7c514af9..e8161e99 100644 --- a/errors.go +++ b/errors.go @@ -57,3 +57,17 @@ func targetClosedError(reason *string) error { } return fmt.Errorf("%w: %s", ErrTargetClosed, *reason) } + +// errorWithDetails wraps a server error that also carries structured +// errorDetails and a call log. Since v1.61 assertion `expect` failures are +// reported this way instead of as a `{ matches: false }` result; +// locatorImpl.expect unwraps it via errors.As to rebuild the expect result. +type errorWithDetails struct { + err error + details map[string]any + log []string +} + +func (e *errorWithDetails) Error() string { return e.err.Error() } + +func (e *errorWithDetails) Unwrap() error { return e.err } diff --git a/fetch.go b/fetch.go index d2ceda84..b6cfd4e2 100644 --- a/fetch.go +++ b/fetch.go @@ -452,6 +452,28 @@ func (r *apiResponseImpl) URL() string { return r.initializer["url"].(string) } +// SecurityDetails and ServerAddr read from the response initializer (unlike +// network Response, which calls back over the channel), mirroring upstream. +func (r *apiResponseImpl) SecurityDetails() (*ResponseSecurityDetailsResult, error) { + details, ok := r.initializer["securityDetails"] + if !ok || details == nil { + return nil, nil + } + result := &ResponseSecurityDetailsResult{} + remapMapToStruct(details, result) + return result, nil +} + +func (r *apiResponseImpl) ServerAddr() (*ResponseServerAddrResult, error) { + addr, ok := r.initializer["serverAddr"] + if !ok || addr == nil { + return nil, nil + } + result := &ResponseServerAddrResult{} + remapMapToStruct(addr, result) + return result, nil +} + func (r *apiResponseImpl) fetchUid() string { return r.initializer["fetchUid"].(string) } diff --git a/generated-enums.go b/generated-enums.go index 6b8d0143..36dd1b3c 100644 --- a/generated-enums.go +++ b/generated-enums.go @@ -427,6 +427,18 @@ var ( ConsoleMessagesFilterSinceNavigation = getConsoleMessagesFilter("since-navigation") ) +func getScreencastCursor(in string) *ScreencastCursor { + v := ScreencastCursor(in) + return &v +} + +type ScreencastCursor string + +var ( + ScreencastCursorNone *ScreencastCursor = getScreencastCursor("none") + ScreencastCursorPointer = getScreencastCursor("pointer") +) + func getAnnotatePosition(in string) *AnnotatePosition { v := AnnotatePosition(in) return &v diff --git a/generated-interfaces.go b/generated-interfaces.go index 6e7540f2..4b5f70bd 100644 --- a/generated-interfaces.go +++ b/generated-interfaces.go @@ -114,6 +114,14 @@ type APIResponse interface { // Contains a boolean stating whether the response was successful (status in the range 200-299) or not. Ok() bool + // 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. + SecurityDetails() (*ResponseSecurityDetailsResult, error) + + // 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. + ServerAddr() (*ResponseServerAddrResult, error) + // Contains the status code of the response (e.g., 200 for a success). Status() int @@ -235,6 +243,10 @@ type BrowserContext interface { // Playwright has ability to mock clock and passage of time. Clock() Clock + // Virtual WebAuthn authenticator for this context. Lets tests seed credentials and intercept + // `navigator.credentials.create()` / `navigator.credentials.get()` ceremonies. + Credentials() Credentials + // Debugger allows to pause and resume the execution. Debugger() (Debugger, error) @@ -723,6 +735,48 @@ type ConsoleMessage interface { Worker() (Worker, error) } +// `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. +// There are two common ways to use it: +// **Usage: seed a known credential** +// **Usage: capture a passkey, then reuse it** +// **Defaults** +type Credentials interface { + // 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] is called, no interception is in place and the page sees the platform's + // native (or absent) WebAuthn behaviour. Seeding credentials with [Credentials.Create] without installing populates + // the authenticator, but the page will never see those credentials. + Install() error + + // Seeds a virtual WebAuthn credential and returns it. + // With only “[object Object]”, 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 “[object Object]”, “[object Object]”, “[object Object]” and + // “[object Object]” together. + // Call [Credentials.Install] before navigating to a page that uses WebAuthn. + // + // rpId: Relying party id (typically the site's effective domain). + Create(rpId string, options ...CredentialsCreateOptions) (*VirtualCredential, error) + + // Removes a credential from the authenticator by its id. Works for any credential currently held — both those seeded + // with [Credentials.Create] and those the page registered itself by calling `navigator.credentials.create()`. + // + // id: Base64url-encoded credential id. + Delete(id string) error + + // Returns every credential currently held by the authenticator, optionally filtered by “[object Object]” or + // “[object Object]”. This includes both credentials seeded with [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] — see the second example in the class overview. + Get(options ...CredentialsGetOptions) ([]VirtualCredential, error) +} + // API for controlling the Playwright debugger. The debugger allows pausing script execution and inspecting the page. // Obtain the debugger instance via [BrowserContext.Debugger]. type Debugger interface { @@ -3819,6 +3873,12 @@ type Page interface { // after the clear. ClearPageErrors() error + // Provides access to the page's `localStorage` for the current origin. See [WebStorage]. + LocalStorage() WebStorage + + // Provides access to the page's `sessionStorage` for the current origin. See [WebStorage]. + SessionStorage() WebStorage + // Returns up to (currently) 200 last console messages from this page. See [Page.OnConsole] for more details. ConsoleMessages(options ...PageConsoleMessagesOptions) ([]ConsoleMessage, error) @@ -4128,7 +4188,7 @@ type Page interface { // 4. Use [Page.Touchscreen] to tap the center of the element, or the specified “[object Object]”. // When all steps combined have not finished during the specified “[object Object]”, this method throws a // [TimeoutError]. Passing zero timeout disables this. - // **NOTE** [Page.Tap] the method will throw if “[object Object]” option of the browser context is false. + // **NOTE** [Page.Tap] will throw if the “[object Object]” option of the browser context is false. // // Deprecated: Use locator-based [Locator.Tap] instead. Read more about [locators]. // @@ -4654,7 +4714,8 @@ type Selectors interface { // Defines custom attribute name to be used in [Page.GetByTestId]. `data-testid` is used by default. // - // attributeName: Test id attribute name. + // attributeName: 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"`. SetTestIdAttribute(attributeName string) } @@ -4667,7 +4728,7 @@ type Selectors interface { type Touchscreen interface { // Dispatches a `touchstart` and `touchend` event with a single touch at the position // (“[object Object]”,“[object Object]”). - // **NOTE** [Page.Tap] the method will throw if “[object Object]” option of the browser context is false. + // **NOTE** [Touchscreen.Tap] will throw if the “[object Object]” option of the browser context is false. // // 1. x: X coordinate relative to the main frame's viewport in CSS pixels. // 2. y: Y coordinate relative to the main frame's viewport in CSS pixels. @@ -4883,6 +4944,35 @@ type WebSocketRoute interface { URL() string } +// WebStorage exposes the page's `localStorage` or `sessionStorage` for the current origin via an async, +// [browser-consistent] API. +// Instances are accessed through [Page.LocalStorage] and [Page.SessionStorage]. +// +// [browser-consistent]: https://developer.mozilla.org/en-US/docs/Web/API/Storage +type WebStorage interface { + // Returns all items in the storage as name/value pairs. + Items() ([]WebStorageItem, error) + + // Returns the value for the given “[object Object]” if present. + // + // name: Name of the item to retrieve. + GetItem(name string) (string, error) + + // Sets the value for the given “[object Object]”. Overwrites any existing value for that name. + // + // 1. name: Name of the item to set. + // 2. value: New value for the item. + SetItem(name string, value string) error + + // Removes the item with the given “[object Object]”. No-op if the item is absent. + // + // name: Name of the item to remove. + RemoveItem(name string) error + + // Removes all items from the storage. + Clear() error +} + // The Worker class represents a [WebWorker]. // `worker` event is emitted on the page object to signal a worker creation. `close` event is emitted on the worker // object when the worker is gone. diff --git a/generated-structs.go b/generated-structs.go index e6f187be..652511f2 100644 --- a/generated-structs.go +++ b/generated-structs.go @@ -350,6 +350,27 @@ type NameValue struct { Value string `json:"value"` } +type ResponseSecurityDetailsResult struct { + // Common Name component of the Issuer field. from the certificate. This should only be used for informational + // purposes. Optional. + Issuer *string `json:"issuer"` + // The specific TLS protocol used. (e.g. `TLS 1.3`). Optional. + Protocol *string `json:"protocol"` + // Common Name component of the Subject field from the certificate. This should only be used for informational + // purposes. Optional. + SubjectName *string `json:"subjectName"` + // Unix timestamp (in seconds) specifying when this cert becomes valid. Optional. + ValidFrom *float64 `json:"validFrom"` + // Unix timestamp (in seconds) specifying when this cert becomes invalid. Optional. + ValidTo *float64 `json:"validTo"` +} + +type ResponseServerAddrResult struct { + // IPv4 or IPV6 address of the server. + IpAddress string `json:"ipAddress"` + Port int `json:"port"` +} + type BrowserCloseOptions struct { // The reason to be reported to the operations interrupted by the browser closure. Reason *string `json:"reason"` @@ -861,6 +882,8 @@ type BrowserTypeConnectOptions struct { } type BrowserTypeConnectOverCDPOptions struct { + // If specified, browser artifacts (such as traces and downloads) are saved into this directory. + ArtifactsDir *string `json:"artifactsDir"` // Additional HTTP headers to be sent with connect request. Optional. Headers map[string]string `json:"headers"` // Tells Playwright that it runs on the same host as the CDP server. It will enable certain optimizations that rely @@ -1178,6 +1201,37 @@ type ConsoleMessageLocation struct { ColumnNumber int `json:"columnNumber"` } +type VirtualCredential struct { + // Base64url-encoded credential id. + Id string `json:"id"` + // Relying party id. + RpId string `json:"rpId"` + // Base64url-encoded user handle. + UserHandle string `json:"userHandle"` + // Base64url-encoded PKCS#8 (DER) private key. + PrivateKey string `json:"privateKey"` + // Base64url-encoded SPKI (DER) public key. + PublicKey string `json:"publicKey"` +} + +type CredentialsCreateOptions struct { + // Base64url-encoded credential id. Auto-generated if omitted. + Id *string `json:"id"` + // Base64url-encoded PKCS#8 (DER) private key. Auto-generated if omitted. + PrivateKey *string `json:"privateKey"` + // Base64url-encoded SPKI (DER) public key. Auto-generated if omitted. + PublicKey *string `json:"publicKey"` + // Base64url-encoded user handle. Auto-generated if omitted. + UserHandle *string `json:"userHandle"` +} + +type CredentialsGetOptions struct { + // Only return the credential with this base64url-encoded id. + Id *string `json:"id"` + // Only return credentials for this relying party id. + RpId *string `json:"rpId"` +} + type PausedDetail struct { Location *PausedDetailLocation `json:"location"` Title string `json:"title"` @@ -4329,27 +4383,6 @@ type RequestTiming struct { ResponseEnd float64 `json:"responseEnd"` } -type ResponseSecurityDetailsResult struct { - // Common Name component of the Issuer field. from the certificate. This should only be used for informational - // purposes. Optional. - Issuer *string `json:"issuer"` - // The specific TLS protocol used. (e.g. `TLS 1.3`). Optional. - Protocol *string `json:"protocol"` - // Common Name component of the Subject field from the certificate. This should only be used for informational - // purposes. Optional. - SubjectName *string `json:"subjectName"` - // Unix timestamp (in seconds) specifying when this cert becomes valid. Optional. - ValidFrom *float64 `json:"validFrom"` - // Unix timestamp (in seconds) specifying when this cert becomes invalid. Optional. - ValidTo *float64 `json:"validTo"` -} - -type ResponseServerAddrResult struct { - // IPv4 or IPV6 address of the server. - IpAddress string `json:"ipAddress"` - Port int `json:"port"` -} - type RouteContinueOptions struct { // If set changes the request HTTP headers. Header values will be converted to a string. Headers map[string]string `json:"headers"` @@ -4416,6 +4449,11 @@ type ScreencastStartOptions struct { Path *string `json:"path"` // The quality of the image, between 0-100. Quality *int `json:"quality"` + // Specifies the dimensions of screencast frames. The actual frame is scaled to preserve the page's aspect ratio and + // may be smaller than these bounds. If a screencast is already active (e.g. started by tracing or video recording), + // the existing configuration takes precedence and the frame size may exceed these bounds or this option may be + // ignored. If not specified the size will be equal to page viewport scaled down to fit into 800×800. + Size *Size `json:"size"` } type ScreencastShowOverlayOptions struct { @@ -4432,6 +4470,9 @@ type ScreencastShowChapterOptions struct { } type ScreencastShowActionsOptions struct { + // 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. + Cursor *ScreencastCursor `json:"cursor"` // How long each annotation is displayed in milliseconds. Defaults to `500`. Duration *float64 `json:"duration"` // Font size of the action title in pixels. Defaults to `24`. @@ -4535,6 +4576,11 @@ type WebSocketRouteCloseOptions struct { Reason *string `json:"reason"` } +type WebStorageItem struct { + Name string `json:"name"` + Value string `json:"value"` +} + type ClientCertificate struct { // Exact origin that the certificate is valid for. Origin includes `https` protocol, a hostname and optionally a port. Origin string `json:"origin"` @@ -4627,6 +4673,8 @@ type Margin struct { type OnFrame struct { // JPEG-encoded frame data. Data []byte `json:"data"` + // The timestamp of when the frame was presented by the browser, in milliseconds since the Unix epoch. + Timestamp float64 `json:"timestamp"` // Width of the page viewport at the time the frame was captured. ViewportWidth int `json:"viewportWidth"` // Height of the page viewport at the time the frame was captured. diff --git a/locator.go b/locator.go index a56cc090..8be76360 100644 --- a/locator.go +++ b/locator.go @@ -1008,26 +1008,27 @@ func (l *locatorImpl) expect(expression string, options frameExpectOptions) (*fr overrides["expectedValue"] = serializeArgument(options.ExpectedValue) options.ExpectedValue = nil } - result, err := l.frame.channel.SendReturnAsDict("expect", options, overrides) + _, err := l.frame.channel.SendReturnAsDict("expect", options, overrides) if err != nil { - return nil, err - } - var ( - received any - matches bool - log []string - ) - - received = parseExpectReceived(result["received"]) - if v, ok := result["matches"]; ok { - matches = v.(bool) - } - if v, ok := result["log"]; ok { - for _, l := range v.([]any) { - log = append(log, l.(string)) + // Since v1.61 a failed assertion is reported as a server error carrying + // structured errorDetails rather than a `{ matches: false }` result. + // Unwrap it into the negative result; any other error propagates. + var detailed *errorWithDetails + if errors.As(err, &detailed) { + result := &frameExpectResult{ + Matches: options.IsNot, + Received: parseExpectReceived(detailed.details["received"]), + Log: detailed.log, + } + if v, ok := detailed.details["timedOut"].(bool); ok { + result.TimedOut = &v + } + return result, nil } + return nil, err } - return &frameExpectResult{Received: received, Matches: matches, Log: log}, nil + // No error means the assertion matched. + return &frameExpectResult{Matches: !options.IsNot}, nil } func (l *locatorImpl) Normalize() Locator { diff --git a/page.go b/page.go index 95c7e951..d7736be4 100644 --- a/page.go +++ b/page.go @@ -35,6 +35,16 @@ type pageImpl struct { closeWasCalled atomic.Bool harRouters []*harRouter locatorHandlers map[float64]*locatorHandlerEntry + localStorage *webStorageImpl + sessionStorage *webStorageImpl +} + +func (p *pageImpl) LocalStorage() WebStorage { + return p.localStorage +} + +func (p *pageImpl) SessionStorage() WebStorage { + return p.sessionStorage } type locatorHandlerEntry struct { @@ -139,8 +149,16 @@ func (p *pageImpl) Close(options ...PageCloseOptions) error { var err error if p.ownedContext != nil { err = p.ownedContext.Close() + } else if runBeforeUnload { + // Upstream split Page.close into close and runBeforeUnload; the latter + // takes no params and the close params no longer carry runBeforeUnload. + _, err = p.channel.Send("runBeforeUnload") } else { - _, err = p.channel.Send("close", options) + params := map[string]any{} + if p.closeReason != nil { + params["reason"] = *p.closeReason + } + _, err = p.channel.Send("close", params) } if errors.Is(err, ErrTargetClosed) && !runBeforeUnload { return nil @@ -910,6 +928,8 @@ func newPage(parent *channelOwner, objectType string, guid string, initializer m bt.mouse = newMouse(bt.channel) bt.keyboard = newKeyboard(bt.channel) bt.touchscreen = newTouchscreen(bt.channel) + bt.localStorage = newWebStorage(bt, "local") + bt.sessionStorage = newWebStorage(bt, "session") bt.channel.On("bindingCall", func(params map[string]any) { bt.onBinding(fromChannel(params["binding"]).(*bindingCallImpl)) }) diff --git a/page_assertions.go b/page_assertions.go index c5b7b9c3..976b5ab4 100644 --- a/page_assertions.go +++ b/page_assertions.go @@ -1,6 +1,7 @@ package playwright import ( + "errors" "fmt" "net/url" "strings" @@ -43,25 +44,26 @@ func (pa *pageAssertionsImpl) expectOnFrame( overrides := map[string]any{ "expression": expression, } - result, err := frame.channel.SendReturnAsDict("expect", options, overrides) - if err != nil { - return err - } var ( received any matches bool log []string ) - - received = parseExpectReceived(result["received"]) - if v, ok := result["matches"]; ok { - matches = v.(bool) - } - if v, ok := result["log"]; ok { - for _, l := range v.([]any) { - log = append(log, l.(string)) + _, err := frame.channel.SendReturnAsDict("expect", options, overrides) + if err != nil { + // Since v1.61 a failed assertion is reported as a server error carrying + // structured errorDetails rather than a `{ matches: false }` result. + var detailed *errorWithDetails + if !errors.As(err, &detailed) { + return err } + matches = options.IsNot + received = parseExpectReceived(detailed.details["received"]) + log = detailed.log + } else { + // No error means the assertion matched. + matches = !options.IsNot } if matches == pa.isNot { diff --git a/patches/main.patch b/patches/main.patch index ff54d586..ee04690f 100644 --- a/patches/main.patch +++ b/patches/main.patch @@ -259,7 +259,7 @@ index 2ddc3815f..6e80f44c0 100644 Set to `true` to include IndexedDB in the storage state snapshot. diff --git a/docs/src/api/class-apiresponse.md b/docs/src/api/class-apiresponse.md -index 8a8b46392..f362c30f4 100644 +index 48ca44eb8..6fc5d7d2c 100644 --- a/docs/src/api/class-apiresponse.md +++ b/docs/src/api/class-apiresponse.md @@ -68,7 +68,7 @@ Headers with multiple entries, such as `Set-Cookie`, appear in the array multipl @@ -285,7 +285,7 @@ index 720b3dcd4..16ff648da 100644 Makes the assertion check for the opposite condition. diff --git a/docs/src/api/class-browser.md b/docs/src/api/class-browser.md -index b676c64dc..4a8c9b4a1 100644 +index 2c07e98c5..9dac60a4b 100644 --- a/docs/src/api/class-browser.md +++ b/docs/src/api/class-browser.md @@ -267,6 +267,9 @@ await browser.CloseAsync(); @@ -308,7 +308,7 @@ index b676c64dc..4a8c9b4a1 100644 ### option: Browser.newPage.storageState = %%-csharp-java-context-option-storage-state-%% * since: v1.8 -@@ -356,7 +362,7 @@ Allows to wait for async listeners to complete or to ignore subsequent errors fr +@@ -357,7 +363,7 @@ Allows to wait for async listeners to complete or to ignore subsequent errors fr ## async method: Browser.startTracing * since: v1.11 @@ -317,7 +317,7 @@ index b676c64dc..4a8c9b4a1 100644 :::note This API controls [Chromium Tracing](https://www.chromium.org/developers/how-tos/trace-event-profiling-tool) which is a low-level chromium-specific debugging tool. API to control [Playwright Tracing](../trace-viewer) could be found [here](./class-tracing). -@@ -394,10 +400,18 @@ browser.stop_tracing() +@@ -395,10 +401,18 @@ browser.stop_tracing() ### param: Browser.startTracing.page * since: v1.11 @@ -336,7 +336,7 @@ index b676c64dc..4a8c9b4a1 100644 ### option: Browser.startTracing.path * since: v1.11 - `path` <[path]> -@@ -418,7 +432,7 @@ specify custom categories to use instead of default. +@@ -419,7 +433,7 @@ specify custom categories to use instead of default. ## async method: Browser.stopTracing * since: v1.11 @@ -346,10 +346,10 @@ index b676c64dc..4a8c9b4a1 100644 :::note diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md -index e839f13a0..0fb881677 100644 +index c5e504b48..1d968c7d9 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md -@@ -433,7 +433,7 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte +@@ -440,7 +440,7 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte ### param: BrowserContext.addInitScript.script * since: v1.8 @@ -358,7 +358,7 @@ index e839f13a0..0fb881677 100644 - `script` <[function]|[string]|[Object]> - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the current working directory. Optional. -@@ -1230,7 +1230,7 @@ A glob pattern, regex pattern, URL pattern, or predicate that receives a [URL] t +@@ -1237,7 +1237,7 @@ A glob pattern, regex pattern, URL pattern, or predicate that receives a [URL] t ### param: BrowserContext.route.url * since: v1.8 @@ -367,7 +367,7 @@ index e839f13a0..0fb881677 100644 - `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]> A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If [`option: Browser.newContext.baseURL`] is set in the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. -@@ -1244,7 +1244,7 @@ handler function to route the request. +@@ -1251,7 +1251,7 @@ handler function to route the request. ### param: BrowserContext.route.handler * since: v1.8 @@ -376,7 +376,7 @@ index e839f13a0..0fb881677 100644 - `handler` <[function]\([Route]\)> handler function to route the request. -@@ -1387,7 +1387,7 @@ Handler function to route the WebSocket. +@@ -1394,7 +1394,7 @@ Handler function to route the WebSocket. ### param: BrowserContext.routeWebSocket.handler * since: v1.48 @@ -385,7 +385,7 @@ index e839f13a0..0fb881677 100644 - `handler` <[function]\([WebSocketRoute]\)> Handler function to route the WebSocket. -@@ -1395,7 +1395,7 @@ Handler function to route the WebSocket. +@@ -1402,7 +1402,7 @@ Handler function to route the WebSocket. ## method: BrowserContext.serviceWorkers * since: v1.11 @@ -394,7 +394,7 @@ index e839f13a0..0fb881677 100644 - returns: <[Array]<[Worker]>> :::note -@@ -1536,6 +1536,7 @@ Whether to emulate network being offline for the browser context. +@@ -1543,6 +1543,7 @@ Whether to emulate network being offline for the browser context. - `httpOnly` <[boolean]> - `secure` <[boolean]> - `sameSite` <[SameSiteAttribute]<"Strict"|"Lax"|"None">> @@ -402,7 +402,7 @@ index e839f13a0..0fb881677 100644 - `origins` <[Array]<[Object]>> - `origin` <[string]> - `localStorage` <[Array]<[Object]>> -@@ -1554,6 +1555,7 @@ Returns storage state for this browser context, contains current cookies, local +@@ -1561,6 +1562,7 @@ Returns storage state for this browser context, contains current cookies, local ### option: BrowserContext.storageState.indexedDB * since: v1.51 @@ -410,7 +410,7 @@ index e839f13a0..0fb881677 100644 - `indexedDB` ? Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage state snapshot. -@@ -1627,7 +1629,7 @@ A glob pattern, regex pattern, URL pattern, or predicate receiving [URL] used to +@@ -1634,7 +1636,7 @@ A glob pattern, regex pattern, URL pattern, or predicate receiving [URL] used to ### param: BrowserContext.unroute.url * since: v1.8 @@ -419,7 +419,7 @@ index e839f13a0..0fb881677 100644 - `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]> A glob pattern, regex pattern, or predicate receiving [URL] used to register a routing with -@@ -1640,6 +1642,13 @@ A glob pattern, regex pattern, or predicate receiving [URL] used to register a r +@@ -1647,6 +1649,13 @@ A glob pattern, regex pattern, or predicate receiving [URL] used to register a r Optional handler function used to register a routing with [`method: BrowserContext.route`]. @@ -433,7 +433,7 @@ index e839f13a0..0fb881677 100644 ### param: BrowserContext.unroute.handler * since: v1.8 * langs: csharp, java -@@ -1681,7 +1690,8 @@ Condition to wait for. +@@ -1688,7 +1697,8 @@ Condition to wait for. ## async method: BrowserContext.waitForConsoleMessage * since: v1.34 @@ -443,7 +443,7 @@ index e839f13a0..0fb881677 100644 - alias-python: expect_console_message - alias-csharp: RunAndWaitForConsoleMessage - returns: <[ConsoleMessage]> -@@ -1712,7 +1722,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti +@@ -1719,7 +1729,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti ## async method: BrowserContext.waitForEvent * since: v1.8 @@ -453,7 +453,7 @@ index e839f13a0..0fb881677 100644 - alias-python: expect_event - returns: <[any]> -@@ -1778,7 +1789,8 @@ Either a predicate that receives an event or an options object. Optional. +@@ -1785,7 +1796,8 @@ Either a predicate that receives an event or an options object. Optional. ## async method: BrowserContext.waitForPage * since: v1.9 @@ -463,7 +463,7 @@ index e839f13a0..0fb881677 100644 - alias-python: expect_page - alias-csharp: RunAndWaitForPage - returns: <[Page]> -@@ -1797,7 +1809,7 @@ Will throw an error if the context closes before new [Page] is created. +@@ -1804,7 +1816,7 @@ Will throw an error if the context closes before new [Page] is created. ### option: BrowserContext.waitForPage.predicate * since: v1.9 @@ -472,7 +472,7 @@ index e839f13a0..0fb881677 100644 - `predicate` <[function]\([Page]\):[boolean]> Receives the [Page] object and resolves to truthy value when the waiting should resolve. -@@ -1810,7 +1822,8 @@ Receives the [Page] object and resolves to truthy value when the waiting should +@@ -1817,7 +1829,8 @@ Receives the [Page] object and resolves to truthy value when the waiting should ## async method: BrowserContext.waitForEvent2 * since: v1.8 @@ -572,7 +572,7 @@ index 020d2efba..9e15e8223 100644 One of the following values: `'log'`, `'debug'`, `'info'`, `'error'`, `'warning'`, `'dir'`, `'dirxml'`, `'table'`, diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md -index b553b7180..7a2e20e73 100644 +index c73aa88e7..413adb658 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -283,6 +283,9 @@ When all steps combined have not finished during the specified [`option: timeout @@ -615,7 +615,7 @@ index b553b7180..7a2e20e73 100644 * alias-csharp: RunAndWaitForNavigation - returns: <[null]|[Response]> diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md -index d3a90c084..cce3c638e 100644 +index 59a98671a..0ce1f8c95 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -655,7 +655,7 @@ Locator description. @@ -695,7 +695,7 @@ index dcfafda26..3940e6633 100644 Expected options currently selected. diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md -index 6f9a43f19..9676bcf88 100644 +index 606bab5d3..d8e7f9a8e 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -615,7 +615,7 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte @@ -814,7 +814,7 @@ index 6f9a43f19..9676bcf88 100644 - `url` ?<[string]|[RegExp]|[function]\([URL]\):[boolean]> A glob pattern, regex pattern or predicate receiving frame's `url` as a [URL] object. Optional. -@@ -2718,7 +2759,7 @@ Returns up to (currently) 200 last page errors from this page. See [`event: Page +@@ -2740,7 +2781,7 @@ Returns up to (currently) 200 last page errors from this page. See [`event: Page ## async method: Page.pageErrors * since: v1.56 @@ -823,7 +823,7 @@ index 6f9a43f19..9676bcf88 100644 - returns: <[Array]<[string]>> Returns up to (currently) 200 last page errors from this page. See [`event: Page.pageError`] for more details. -@@ -2972,7 +3013,7 @@ Paper width, accepts values labeled with units. +@@ -2994,7 +3035,7 @@ Paper width, accepts values labeled with units. ### option: Page.pdf.width * since: v1.8 @@ -832,7 +832,7 @@ index 6f9a43f19..9676bcf88 100644 - `width` <[string]> Paper width, accepts values labeled with units. -@@ -2986,7 +3027,7 @@ Paper height, accepts values labeled with units. +@@ -3008,7 +3049,7 @@ Paper height, accepts values labeled with units. ### option: Page.pdf.height * since: v1.8 @@ -841,16 +841,16 @@ index 6f9a43f19..9676bcf88 100644 - `height` <[string]> Paper height, accepts values labeled with units. -@@ -3004,7 +3045,7 @@ Paper margins, defaults to none. +@@ -3026,7 +3067,7 @@ Paper margins, defaults to none. ### option: Page.pdf.margin * since: v1.8 -* langs: csharp, java +* langs: csharp, java, go - `margin` <[Object]> - - alias-java: Margin + * alias-java: Margin - `top` ?<[string]> Top margin, accepts values labeled with units. Defaults to `0`. -@@ -3036,6 +3077,7 @@ Whether or not to embed the document outline into the PDF. Defaults to `false`. +@@ -3058,6 +3099,7 @@ Whether or not to embed the document outline into the PDF. Defaults to `false`. ## async method: Page.pickLocator * since: v1.59 @@ -858,7 +858,7 @@ index 6f9a43f19..9676bcf88 100644 - returns: <[Locator]> Enters pick locator mode where hovering over page elements highlights them and shows the corresponding locator. -@@ -3471,7 +3513,7 @@ Function that should be run once [`param: locator`] appears. This function shoul +@@ -3493,7 +3535,7 @@ Function that should be run once [`param: locator`] appears. This function shoul Function that should be run once [`param: locator`] appears. This function should get rid of the element that blocks actions like click. ### param: Page.addLocatorHandler.handler @@ -867,7 +867,7 @@ index 6f9a43f19..9676bcf88 100644 * since: v1.42 - `handler` <[function]\([Locator]\)> -@@ -3716,7 +3758,7 @@ A glob pattern, regex pattern, URL pattern, or predicate that receives a [URL] t +@@ -3738,7 +3780,7 @@ A glob pattern, regex pattern, URL pattern, or predicate that receives a [URL] t ### param: Page.route.url * since: v1.8 @@ -876,7 +876,7 @@ index 6f9a43f19..9676bcf88 100644 - `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]> A glob pattern, regex pattern, or predicate that receives a [URL] to match during routing. If [`option: Browser.newContext.baseURL`] is set in the context options and the provided URL is a string that does not start with `*`, it is resolved using the [`new URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) constructor. -@@ -3728,6 +3770,13 @@ A glob pattern, regex pattern, or predicate that receives a [URL] to match durin +@@ -3750,6 +3792,13 @@ A glob pattern, regex pattern, or predicate that receives a [URL] to match durin handler function to route the request. @@ -890,7 +890,7 @@ index 6f9a43f19..9676bcf88 100644 ### param: Page.route.handler * since: v1.8 * langs: csharp, java -@@ -3856,7 +3905,7 @@ Only WebSockets with the url matching this pattern will be routed. A string patt +@@ -3878,7 +3927,7 @@ Only WebSockets with the url matching this pattern will be routed. A string patt ### param: Page.routeWebSocket.url * since: v1.48 @@ -899,7 +899,7 @@ index 6f9a43f19..9676bcf88 100644 - `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]> Only WebSockets with the url matching this pattern will be routed. A string pattern can be relative to the [`option: Browser.newContext.baseURL`] context option. -@@ -3870,7 +3919,7 @@ Handler function to route the WebSocket. +@@ -3892,7 +3941,7 @@ Handler function to route the WebSocket. ### param: Page.routeWebSocket.handler * since: v1.48 @@ -908,7 +908,7 @@ index 6f9a43f19..9676bcf88 100644 - `handler` <[function]\([WebSocketRoute]\)> Handler function to route the WebSocket. -@@ -4217,14 +4266,14 @@ await page.GotoAsync("https://www.microsoft.com"); +@@ -4239,14 +4288,14 @@ await page.GotoAsync("https://www.microsoft.com"); ### param: Page.setViewportSize.width * since: v1.10 @@ -925,7 +925,7 @@ index 6f9a43f19..9676bcf88 100644 - `height` <[int]> Page height in pixels. -@@ -4439,7 +4488,7 @@ A glob pattern, regex pattern, URL pattern, or predicate receiving [URL] to matc +@@ -4461,7 +4510,7 @@ A glob pattern, regex pattern, URL pattern, or predicate receiving [URL] to matc ### param: Page.unroute.url * since: v1.8 @@ -934,7 +934,7 @@ index 6f9a43f19..9676bcf88 100644 - `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]> A glob pattern, regex pattern, or predicate receiving [URL] to match while routing. -@@ -4451,6 +4500,13 @@ A glob pattern, regex pattern, or predicate receiving [URL] to match while routi +@@ -4473,6 +4522,13 @@ A glob pattern, regex pattern, or predicate receiving [URL] to match while routi Optional handler function to route the request. @@ -948,7 +948,7 @@ index 6f9a43f19..9676bcf88 100644 ### param: Page.unroute.handler * since: v1.8 * langs: csharp, java -@@ -4491,7 +4547,8 @@ Performs action and waits for the Page to close. +@@ -4513,7 +4569,8 @@ Performs action and waits for the Page to close. ## async method: Page.waitForConsoleMessage * since: v1.9 @@ -958,7 +958,7 @@ index 6f9a43f19..9676bcf88 100644 - alias-python: expect_console_message - alias-csharp: RunAndWaitForConsoleMessage - returns: <[ConsoleMessage]> -@@ -4522,7 +4579,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti +@@ -4544,7 +4601,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti ## async method: Page.waitForDownload * since: v1.9 @@ -968,7 +968,7 @@ index 6f9a43f19..9676bcf88 100644 - alias-python: expect_download - alias-csharp: RunAndWaitForDownload - returns: <[Download]> -@@ -4553,7 +4611,8 @@ Receives the [Download] object and resolves to truthy value when the waiting sho +@@ -4575,7 +4633,8 @@ Receives the [Download] object and resolves to truthy value when the waiting sho ## async method: Page.waitForEvent * since: v1.8 @@ -978,7 +978,7 @@ index 6f9a43f19..9676bcf88 100644 - alias-python: expect_event - returns: <[any]> -@@ -4606,7 +4665,8 @@ Either a predicate that receives an event or an options object. Optional. +@@ -4628,7 +4687,8 @@ Either a predicate that receives an event or an options object. Optional. ## async method: Page.waitForFileChooser * since: v1.9 @@ -988,7 +988,7 @@ index 6f9a43f19..9676bcf88 100644 - alias-python: expect_file_chooser - alias-csharp: RunAndWaitForFileChooser - returns: <[FileChooser]> -@@ -4764,7 +4824,7 @@ await page.WaitForFunctionAsync("selector => !!document.querySelector(selector)" +@@ -4786,7 +4846,7 @@ await page.WaitForFunctionAsync("selector => !!document.querySelector(selector)" Optional argument to pass to [`param: expression`]. @@ -997,7 +997,7 @@ index 6f9a43f19..9676bcf88 100644 * since: v1.8 ### option: Page.waitForFunction.polling = %%-csharp-java-wait-for-function-polling-%% -@@ -4861,6 +4921,11 @@ Console.WriteLine(await popup.TitleAsync()); // popup is ready to use. +@@ -4883,6 +4943,11 @@ Console.WriteLine(await popup.TitleAsync()); // popup is ready to use. ``` ### param: Page.waitForLoadState.state = %%-wait-for-load-state-state-%% @@ -1009,7 +1009,7 @@ index 6f9a43f19..9676bcf88 100644 * since: v1.8 ### option: Page.waitForLoadState.timeout = %%-navigation-timeout-%% -@@ -4873,6 +4938,7 @@ Console.WriteLine(await popup.TitleAsync()); // popup is ready to use. +@@ -4895,6 +4960,7 @@ Console.WriteLine(await popup.TitleAsync()); // popup is ready to use. * since: v1.8 * deprecated: This method is inherently racy, please use [`method: Page.waitForURL`] instead. * langs: @@ -1017,7 +1017,7 @@ index 6f9a43f19..9676bcf88 100644 * alias-python: expect_navigation * alias-csharp: RunAndWaitForNavigation - returns: <[null]|[Response]> -@@ -4960,7 +5026,8 @@ a navigation. +@@ -4982,7 +5048,8 @@ a navigation. ## async method: Page.waitForPopup * since: v1.9 @@ -1027,7 +1027,7 @@ index 6f9a43f19..9676bcf88 100644 - alias-python: expect_popup - alias-csharp: RunAndWaitForPopup - returns: <[Page]> -@@ -4992,6 +5059,7 @@ Receives the [Page] object and resolves to truthy value when the waiting should +@@ -5014,6 +5081,7 @@ Receives the [Page] object and resolves to truthy value when the waiting should ## async method: Page.waitForRequest * since: v1.8 * langs: @@ -1035,7 +1035,7 @@ index 6f9a43f19..9676bcf88 100644 * alias-python: expect_request * alias-csharp: RunAndWaitForRequest - returns: <[Request]> -@@ -5099,7 +5167,8 @@ changed by using the [`method: Page.setDefaultTimeout`] method. +@@ -5121,7 +5189,8 @@ changed by using the [`method: Page.setDefaultTimeout`] method. ## async method: Page.waitForRequestFinished * since: v1.12 @@ -1045,7 +1045,7 @@ index 6f9a43f19..9676bcf88 100644 - alias-python: expect_request_finished - alias-csharp: RunAndWaitForRequestFinished - returns: <[Request]> -@@ -5131,6 +5200,7 @@ Receives the [Request] object and resolves to truthy value when the waiting shou +@@ -5153,6 +5222,7 @@ Receives the [Request] object and resolves to truthy value when the waiting shou ## async method: Page.waitForResponse * since: v1.8 * langs: @@ -1053,7 +1053,7 @@ index 6f9a43f19..9676bcf88 100644 * alias-python: expect_response * alias-csharp: RunAndWaitForResponse - returns: <[Response]> -@@ -5497,7 +5567,8 @@ await page.WaitForURLAsync("**/target.html"); +@@ -5519,7 +5589,8 @@ await page.WaitForURLAsync("**/target.html"); ## async method: Page.waitForWebSocket * since: v1.9 @@ -1063,7 +1063,7 @@ index 6f9a43f19..9676bcf88 100644 - alias-python: expect_websocket - alias-csharp: RunAndWaitForWebSocket - returns: <[WebSocket]> -@@ -5528,7 +5599,8 @@ Receives the [WebSocket] object and resolves to truthy value when the waiting sh +@@ -5550,7 +5621,8 @@ Receives the [WebSocket] object and resolves to truthy value when the waiting sh ## async method: Page.waitForWorker * since: v1.9 @@ -1073,7 +1073,7 @@ index 6f9a43f19..9676bcf88 100644 - alias-python: expect_worker - alias-csharp: RunAndWaitForWorker - returns: <[Worker]> -@@ -5570,7 +5642,8 @@ This does not contain ServiceWorkers +@@ -5592,7 +5664,8 @@ This does not contain ServiceWorkers ## async method: Page.waitForEvent2 * since: v1.8 @@ -1141,7 +1141,7 @@ index aa4e0a42a..2b822104e 100644 Creates a [PageAssertions] object for the given [Page]. diff --git a/docs/src/api/class-request.md b/docs/src/api/class-request.md -index 5f3bd280d..6f83c2a35 100644 +index a37a5d595..9e091dd83 100644 --- a/docs/src/api/class-request.md +++ b/docs/src/api/class-request.md @@ -128,6 +128,13 @@ Headers with multiple entries, such as `Set-Cookie`, appear in the array multipl @@ -1168,7 +1168,7 @@ index 5f3bd280d..6f83c2a35 100644 Returns parsed request's body for `form-urlencoded` and JSON as a fallback if any. diff --git a/docs/src/api/class-response.md b/docs/src/api/class-response.md -index a89d0bc94..3ee565f6c 100644 +index 555d2210c..4e7ecef57 100644 --- a/docs/src/api/class-response.md +++ b/docs/src/api/class-response.md @@ -23,7 +23,7 @@ Waits for this response to finish, returns always `null`. @@ -1245,7 +1245,7 @@ index 5bbe4b3f8..b7c9624c1 100644 Response body. diff --git a/docs/src/api/class-selectors.md b/docs/src/api/class-selectors.md -index 4c3011430..4ced2d60d 100644 +index fefd7ba39..1a2541a5c 100644 --- a/docs/src/api/class-selectors.md +++ b/docs/src/api/class-selectors.md @@ -187,7 +187,7 @@ contain `[a-zA-Z0-9_]` characters. @@ -1258,7 +1258,7 @@ index 4c3011430..4ced2d60d 100644 - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the current working directory. Optional. diff --git a/docs/src/api/class-tracing.md b/docs/src/api/class-tracing.md -index 1af0427bf..4f54c91e9 100644 +index 256ae513a..f65ebbe13 100644 --- a/docs/src/api/class-tracing.md +++ b/docs/src/api/class-tracing.md @@ -156,7 +156,7 @@ If this option is true tracing will @@ -1359,7 +1359,7 @@ index c4c8dfc0b..f04bca717 100644 * since: v1.48 * langs: csharp, java diff --git a/docs/src/api/params.md b/docs/src/api/params.md -index 350afc877..47bda7a0d 100644 +index 118b38b28..cb8a6b818 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -8,7 +8,7 @@ When to consider operation succeeded, defaults to `load`. Events can be either: @@ -1682,7 +1682,7 @@ index 350afc877..47bda7a0d 100644 - `recordVideo` <[Object]> - `dir` ?<[path]> Path to the directory to put videos into. If not specified, the videos will be stored in `artifactsDir` (see [`method: BrowserType.launch`] options). - `size` ?<[Object]> Optional dimensions of the recorded videos. If not specified the size will be equal to `viewport` -@@ -890,7 +958,7 @@ Specifies whether to wait for already running listeners and what to do if they t +@@ -888,7 +956,7 @@ Specifies whether to wait for already running listeners and what to do if they t * `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught ## unroute-all-options-behavior @@ -1691,16 +1691,16 @@ index 350afc877..47bda7a0d 100644 * since: v1.41 - `behavior` <[UnrouteBehavior]<"wait"|"ignoreErrors"|"default">> -@@ -901,7 +969,7 @@ Specifies whether to wait for already running handlers and what to do if they th +@@ -899,7 +967,7 @@ Specifies whether to wait for already running handlers and what to do if they th ## select-options-values -* langs: java, js, csharp +* langs: java, js, csharp, go - `values` <[null]|[string]|[ElementHandle]|[Array]<[string]>|[Object]|[Array]<[ElementHandle]>|[Array]<[Object]>> - - alias-java: SelectOption + * alias-java: SelectOption - `value` ?<[string]> Matches by `option.value`. Optional. -@@ -921,7 +989,7 @@ the parameter is a string without wildcard characters, the method will wait for +@@ -919,7 +987,7 @@ the parameter is a string without wildcard characters, the method will wait for equal to the string. ## python-csharp-java-wait-for-navigation-url @@ -1709,7 +1709,7 @@ index 350afc877..47bda7a0d 100644 - `url` <[string]|[RegExp]|[function]\([URL]\):[boolean]> A glob pattern, regex pattern, or predicate receiving [URL] to match while waiting for the navigation. Note that if -@@ -929,7 +997,7 @@ the parameter is a string without wildcard characters, the method will wait for +@@ -927,7 +995,7 @@ the parameter is a string without wildcard characters, the method will wait for equal to the string. ## wait-for-event-event @@ -1718,7 +1718,7 @@ index 350afc877..47bda7a0d 100644 - `event` <[string]> Event name, same one typically passed into `*.on(event)`. -@@ -987,7 +1055,7 @@ only the first option matching one of the passed options is selected. Optional. +@@ -985,7 +1053,7 @@ only the first option matching one of the passed options is selected. Optional. Receives the event data and resolves to truthy value when the waiting should resolve. ## wait-for-event-timeout @@ -1727,7 +1727,7 @@ index 350afc877..47bda7a0d 100644 - `timeout` <[float]> Maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. -@@ -1007,7 +1075,7 @@ using the [`method: AndroidDevice.setDefaultTimeout`] method. +@@ -1005,7 +1073,7 @@ using the [`method: AndroidDevice.setDefaultTimeout`] method. Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`. ## csharp-java-python-assertions-timeout @@ -1736,7 +1736,7 @@ index 350afc877..47bda7a0d 100644 - `timeout` <[float]> Time to retry the assertion for in milliseconds. Defaults to `5000`. -@@ -1061,12 +1129,15 @@ between the same pixel in compared images, between zero (strict) and one (lax), +@@ -1059,12 +1127,15 @@ between the same pixel in compared images, between zero (strict) and one (lax), - %%-context-option-httpcredentials-%% - %%-context-option-colorscheme-%% - %%-context-option-colorscheme-csharp-python-%% @@ -1752,7 +1752,7 @@ index 350afc877..47bda7a0d 100644 - %%-context-option-logger-%% - %%-context-option-recordhar-%% - %%-context-option-recordhar-path-%% -@@ -1151,7 +1222,7 @@ Firefox user preferences. Learn more about the Firefox user preferences at +@@ -1149,7 +1220,7 @@ Firefox user preferences. Learn more about the Firefox user preferences at You can also provide a path to a custom [`policies.json` file](https://mozilla.github.io/policy-templates/) via `PLAYWRIGHT_FIREFOX_POLICIES_JSON` environment variable. ## csharp-java-browser-option-firefoxuserprefs @@ -1763,10 +1763,10 @@ index 350afc877..47bda7a0d 100644 Firefox user preferences. Learn more about the Firefox user preferences at diff --git a/utils/doclint/generateGoApi.js b/utils/doclint/generateGoApi.js new file mode 100644 -index 000000000..0bcbee3c5 +index 000000000..0718831f4 --- /dev/null +++ b/utils/doclint/generateGoApi.js -@@ -0,0 +1,880 @@ +@@ -0,0 +1,891 @@ +/** + * Copyright (c) Microsoft Corporation. + * @@ -1855,6 +1855,7 @@ index 000000000..0bcbee3c5 + 'Clock', + 'Context', + 'Contexts', ++ 'Credentials', + 'DefaultValue', + 'Element', + 'Error', @@ -1869,6 +1870,7 @@ index 000000000..0bcbee3c5 + 'IsMultiple', + 'IsNavigationRequest', + 'Keyboard', ++ 'LocalStorage', + 'Location', + 'Locator', + 'MainFrame', @@ -1888,6 +1890,7 @@ index 000000000..0bcbee3c5 + 'Request', + 'ResourceType', + 'ServiceWorkers', ++ 'SessionStorage', + 'SetDefaultNavigationTimeout', + 'SetDefaultTimeout', + 'SetTestIdAttribute', @@ -2134,6 +2137,14 @@ index 000000000..0bcbee3c5 + attemptedName = 'ResponseSecurityDetailsResult'; + if (attemptedName === 'ServerAddr') + attemptedName = 'ResponseServerAddrResult'; ++ // WebAuthn: Credentials.Create/Get return the same shape; use the documented ++ // `* alias: VirtualCredential` (matching java/csharp) instead of the generic ++ // method-derived names Create/Get, so both share one struct. ++ if (parent.name === 'Credentials' && (attemptedName === 'Create' || attemptedName === 'Get')) ++ attemptedName = 'VirtualCredential'; ++ // WebStorage.Items: use the documented `* alias: WebStorageItem` over generic `Item`. ++ if (parent.name === 'WebStorage' && attemptedName === 'Item') ++ attemptedName = 'WebStorageItem'; + if (attemptedName === 'Timing') + attemptedName = 'RequestTiming'; + if (attemptedName === 'HeadersArray' || attemptedName == 'LocalStorage') diff --git a/playwright b/playwright index 87bb9ddb..39e3553a 160000 --- a/playwright +++ b/playwright @@ -1 +1 @@ -Subproject commit 87bb9ddbd78f329df18c2b24847bc9409240cd07 +Subproject commit 39e3553a4f283a41134d75d7e404484bd9e6865a diff --git a/run.go b/run.go index 788219e7..6968fc2e 100644 --- a/run.go +++ b/run.go @@ -19,7 +19,7 @@ import ( ) const ( - playwrightCliVersion = "1.60.0" + playwrightCliVersion = "1.61.1" // nodeVersion is the Node.js runtime downloaded alongside the driver when no // PLAYWRIGHT_NODEJS_PATH is provided. It is kept in line with the Node.js // version upstream Playwright bundles in its own driver. diff --git a/screencast.go b/screencast.go index df195c51..6238242f 100644 --- a/screencast.go +++ b/screencast.go @@ -3,6 +3,7 @@ package playwright import ( "encoding/base64" "errors" + "sync" ) type screencastImpl struct { @@ -10,6 +11,7 @@ type screencastImpl struct { started bool savePath *string artifact *artifactImpl + mu sync.Mutex // guards onFrame, read on the dispatcher goroutine onFrame func(OnFrame) listening bool } @@ -22,25 +24,33 @@ func (s *screencastImpl) Start(options ...ScreencastStartOptions) error { overrides := map[string]any{} if len(options) == 1 { if options[0].OnFrame != nil { + s.mu.Lock() s.onFrame = options[0].OnFrame + s.mu.Unlock() // Register the channel listener once and dispatch through the // mutable onFrame field, mirroring upstream. This avoids leaking a // listener (and duplicate dispatch) on every Start/Stop cycle. if !s.listening { s.listening = true s.page.channel.On("screencastFrame", func(params map[string]any) { - if s.onFrame == nil { + s.mu.Lock() + onFrame := s.onFrame + s.mu.Unlock() + if onFrame == nil { return } data, _ := base64.StdEncoding.DecodeString(params["data"].(string)) frame := OnFrame{Data: data} + if ts, ok := params["timestamp"].(float64); ok { + frame.Timestamp = ts + } if vw, ok := params["viewportWidth"].(float64); ok { frame.ViewportWidth = int(vw) } if vh, ok := params["viewportHeight"].(float64); ok { frame.ViewportHeight = int(vh) } - s.onFrame(frame) + onFrame(frame) }) } overrides["sendFrames"] = true @@ -65,7 +75,9 @@ func (s *screencastImpl) Start(options ...ScreencastStartOptions) error { func (s *screencastImpl) Stop() error { s.started = false + s.mu.Lock() s.onFrame = nil + s.mu.Unlock() if _, err := s.page.channel.Send("screencastStop"); err != nil { return err } diff --git a/scripts/apply-patch.sh b/scripts/apply-patch.sh index 2e9de8bb..302f8a60 100755 --- a/scripts/apply-patch.sh +++ b/scripts/apply-patch.sh @@ -15,9 +15,13 @@ pushd playwright git checkout HEAD --detach +# Always advance the submodule to the pinned version. Previously this only ran when +# a leftover playwright-build branch existed, so the very first roll on a fresh +# submodule patched the OLD version instead of the new tag. +git fetch --tags +git checkout "$PW_VERSION" + if git show-ref -q --heads "$BRANCH_NAME_BUILD"; then - git fetch --tags - git checkout "$PW_VERSION" git branch -D "$BRANCH_NAME_BUILD" fi diff --git a/tests/browser_context_client_certificates_test.go b/tests/browser_context_client_certificates_test.go index 7e24d73a..5531d97c 100644 --- a/tests/browser_context_client_certificates_test.go +++ b/tests/browser_context_client_certificates_test.go @@ -15,6 +15,26 @@ import ( "github.com/stretchr/testify/require" ) +// gotoSettled navigates to url, retrying while a concurrent navigation keeps +// interrupting it. Since v1.61 a client-cert handshake abort triggers an async +// navigation to chrome-error:// (and an automatic reload of the target), which +// races with the immediately-following navigation to the matching origin. +func gotoSettled(t *testing.T, p playwright.Page, url string) { + t.Helper() + var err error + for i := 0; i < 20; i++ { + _, err = p.Goto(url) + if err == nil { + return + } + if !strings.Contains(err.Error(), "is interrupted by another navigation") { + break + } + p.WaitForTimeout(50) + } + require.NoError(t, err) +} + func NewTLSServerRequireClientCert(t *testing.T) *httptest.Server { t.Helper() certPath := Asset("client-certificates/server/server_cert.pem") @@ -83,16 +103,18 @@ func TestClientCerts(t *testing.T) { }, }) - resp, err := page.Goto(strings.Replace(tlsServer.URL, "127.0.0.1", "localhost", 1)) + // Since v1.61 the client-certificate interceptor only presents the cert for + // the matching origin (upstream socksClientCertificatesInterceptor rewrite). + // Navigating to the mismatched "localhost" origin therefore sends no cert and + // the RequireAndVerifyClientCert server aborts the TLS handshake. + _, err := page.Goto(strings.Replace(tlsServer.URL, "127.0.0.1", "localhost", 1)) if tlsServer.EnableHTTP2 { require.ErrorContains(t, err, "net::ERR_CONNECTION_CLOSED") } else { - require.NoError(t, err) - require.False(t, resp.Ok()) // status code 503, client didn't provide a certificate due to origin mismatch + require.ErrorContains(t, err, "net::ERR_BAD_SSL_CLIENT_AUTH_CERT") } - _, err = page.Goto(tlsServer.URL) - require.NoError(t, err) + gotoSettled(t, page, tlsServer.URL) content, err := page.GetByTestId("message").TextContent() require.NoError(t, err) require.Equal(t, "Hello Alice, your certificate was issued by localhost!", content) @@ -119,16 +141,15 @@ func TestClientCerts(t *testing.T) { page2, err := context2.NewPage() require.NoError(t, err) - resp, err := page2.Goto(strings.Replace(tlsServer.URL, "127.0.0.1", "localhost", 1)) + // Since v1.61 a mismatched origin sends no cert; the server aborts the handshake. + _, err = page2.Goto(strings.Replace(tlsServer.URL, "127.0.0.1", "localhost", 1)) if tlsServer.EnableHTTP2 { require.ErrorContains(t, err, "net::ERR_CONNECTION_CLOSED") } else { - require.NoError(t, err) - require.False(t, resp.Ok()) // status code 503, client didn't provide a certificate due to origin mismatch + require.ErrorContains(t, err, "net::ERR_BAD_SSL_CLIENT_AUTH_CERT") } - _, err = page2.Goto(tlsServer.URL) - require.NoError(t, err) + gotoSettled(t, page2, tlsServer.URL) content, err := page2.GetByTestId("message").TextContent() require.NoError(t, err) require.Equal(t, "Hello Alice, your certificate was issued by localhost!", content) diff --git a/tests/browser_context_credentials_test.go b/tests/browser_context_credentials_test.go new file mode 100644 index 00000000..783ad327 --- /dev/null +++ b/tests/browser_context_credentials_test.go @@ -0,0 +1,44 @@ +package playwright_test + +import ( + "testing" + + "github.com/playwright-community/playwright-go" + "github.com/stretchr/testify/require" +) + +func TestBrowserContextExposesCredentialsProperty(t *testing.T) { + BeforeEach(t) + + require.NotNil(t, context.Credentials()) + // The same instance is returned on each access. + require.Same(t, context.Credentials(), context.Credentials()) +} + +func TestBrowserContextInstallCreateGetDeleteCredentials(t *testing.T) { + BeforeEach(t) + + // WebAuthn requires a secure context; the test server's localhost origin qualifies. + _, err := page.Goto(server.CROSS_PROCESS_PREFIX+"/empty.html", playwright.PageGotoOptions{ + WaitUntil: playwright.WaitUntilStateNetworkidle, + }) + require.NoError(t, err) + + creds := context.Credentials() + require.NoError(t, creds.Install()) + + created, err := creds.Create("localhost") + require.NoError(t, err) + require.Equal(t, "localhost", created.RpId) + require.NotEmpty(t, created.Id) + + list, err := creds.Get() + require.NoError(t, err) + require.Len(t, list, 1) + require.Equal(t, created.Id, list[0].Id) + + require.NoError(t, creds.Delete(created.Id)) + list, err = creds.Get() + require.NoError(t, err) + require.Empty(t, list) +} diff --git a/tests/fetch_test.go b/tests/fetch_test.go index faa859f0..732d81bf 100644 --- a/tests/fetch_test.go +++ b/tests/fetch_test.go @@ -64,6 +64,26 @@ func TestFetchShouldWork(t *testing.T) { check("DELETE", response) } +func TestAPIResponseServerAddrAndSecurityDetails(t *testing.T) { + BeforeEach(t) + + request, err := pw.Request.NewContext() + require.NoError(t, err) + response, err := request.Get(server.PREFIX + "/simple.json") + require.NoError(t, err) + + // Over plain HTTP the server address is reported and security details are absent. + addr, err := response.ServerAddr() + require.NoError(t, err) + if addr != nil { + require.Greater(t, addr.Port, 0) + } + + details, err := response.SecurityDetails() + require.NoError(t, err) + require.Nil(t, details) +} + func TestShouldDisposeGlobalRequest(t *testing.T) { BeforeEach(t) diff --git a/tests/page_web_storage_test.go b/tests/page_web_storage_test.go new file mode 100644 index 00000000..1d0584bb --- /dev/null +++ b/tests/page_web_storage_test.go @@ -0,0 +1,90 @@ +package playwright_test + +import ( + "testing" + + "github.com/playwright-community/playwright-go" + "github.com/stretchr/testify/require" +) + +func TestPageExposesWebStorageProperties(t *testing.T) { + BeforeEach(t) + + require.NotNil(t, page.LocalStorage()) + require.NotNil(t, page.SessionStorage()) + // The same instance is returned on each access. + require.Same(t, page.LocalStorage(), page.LocalStorage()) + require.Same(t, page.SessionStorage(), page.SessionStorage()) +} + +func TestPageLocalStorageSetGetAndItems(t *testing.T) { + BeforeEach(t) + + _, err := page.Goto(server.EMPTY_PAGE) + require.NoError(t, err) + + storage := page.LocalStorage() + require.NoError(t, storage.SetItem("foo", "bar")) + + value, err := storage.GetItem("foo") + require.NoError(t, err) + require.Equal(t, "bar", value) + + jsValue, err := page.Evaluate("() => localStorage.getItem('foo')") + require.NoError(t, err) + require.Equal(t, "bar", jsValue) + + require.NoError(t, storage.SetItem("baz", "qux")) + items, err := storage.Items() + require.NoError(t, err) + require.Contains(t, items, playwright.WebStorageItem{Name: "foo", Value: "bar"}) + require.Contains(t, items, playwright.WebStorageItem{Name: "baz", Value: "qux"}) +} + +func TestPageLocalStorageGetItemMissing(t *testing.T) { + BeforeEach(t) + + _, err := page.Goto(server.EMPTY_PAGE) + require.NoError(t, err) + + value, err := page.LocalStorage().GetItem("missing") + require.NoError(t, err) + require.Equal(t, "", value) +} + +func TestPageLocalStorageRemoveAndClear(t *testing.T) { + BeforeEach(t) + + _, err := page.Goto(server.EMPTY_PAGE) + require.NoError(t, err) + + storage := page.LocalStorage() + require.NoError(t, storage.SetItem("foo", "bar")) + require.NoError(t, storage.RemoveItem("foo")) + value, err := storage.GetItem("foo") + require.NoError(t, err) + require.Equal(t, "", value) + + require.NoError(t, storage.SetItem("a", "1")) + require.NoError(t, storage.Clear()) + length, err := page.Evaluate("() => localStorage.length") + require.NoError(t, err) + require.Equal(t, 0, length) +} + +func TestPageSessionStorageSetAndGet(t *testing.T) { + BeforeEach(t) + + _, err := page.Goto(server.EMPTY_PAGE) + require.NoError(t, err) + + storage := page.SessionStorage() + require.NoError(t, storage.SetItem("foo", "bar")) + value, err := storage.GetItem("foo") + require.NoError(t, err) + require.Equal(t, "bar", value) + + jsValue, err := page.Evaluate("() => sessionStorage.getItem('foo')") + require.NoError(t, err) + require.Equal(t, "bar", jsValue) +} diff --git a/tests/screencast_test.go b/tests/screencast_test.go new file mode 100644 index 00000000..fcbdc25d --- /dev/null +++ b/tests/screencast_test.go @@ -0,0 +1,77 @@ +package playwright_test + +import ( + "sync" + "testing" + + "github.com/playwright-community/playwright-go" + "github.com/stretchr/testify/require" +) + +func TestScreencastOnFrameReceivesViewportSizeAndTimestamp(t *testing.T) { + BeforeEach(t) + + ctx, err := browser.NewContext(playwright.BrowserNewContextOptions{ + Viewport: &playwright.Size{Width: 1000, Height: 400}, + }) + require.NoError(t, err) + defer ctx.Close() + p, err := ctx.NewPage() + require.NoError(t, err) + + var ( + mu sync.Mutex + received []playwright.OnFrame + ) + sc, err := p.Screencast() + require.NoError(t, err) + require.NoError(t, sc.Start(playwright.ScreencastStartOptions{ + Size: &playwright.Size{Width: 500, Height: 400}, + OnFrame: func(frame playwright.OnFrame) { + mu.Lock() + received = append(received, frame) + mu.Unlock() + }, + })) + + _, err = p.Goto(server.EMPTY_PAGE) + require.NoError(t, err) + _, err = p.Evaluate("() => document.body.style.backgroundColor = 'red'") + require.NoError(t, err) + for i := 0; i < 100; i++ { + _, err = p.Evaluate("() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))") + require.NoError(t, err) + } + _, err = p.Screenshot() + require.NoError(t, err) + require.NoError(t, sc.Stop()) + + mu.Lock() + defer mu.Unlock() + require.GreaterOrEqual(t, len(received), 1) + for _, frame := range received { + require.Equal(t, 400, frame.ViewportHeight) + // Timestamp is milliseconds since the Unix epoch; just assert it was set. + require.Greater(t, frame.Timestamp, float64(0)) + } +} + +func TestScreencastShowActionsAcceptsCursorParam(t *testing.T) { + BeforeEach(t) + + sc, err := page.Screencast() + require.NoError(t, err) + require.NoError(t, sc.Start(playwright.ScreencastStartOptions{ + OnFrame: func(playwright.OnFrame) {}, + })) + defer sc.Stop() + + require.NoError(t, sc.ShowActions(playwright.ScreencastShowActionsOptions{ + Duration: playwright.Float(100), + Cursor: playwright.ScreencastCursorPointer, + })) + require.NoError(t, sc.ShowActions(playwright.ScreencastShowActionsOptions{ + Duration: playwright.Float(100), + Cursor: playwright.ScreencastCursorNone, + })) +} diff --git a/transport.go b/transport.go index 852b4cbd..b84ee596 100644 --- a/transport.go +++ b/transport.go @@ -61,6 +61,9 @@ type message struct { Error Error `json:"error"` } `json:"error,omitempty"` Log []string `json:"log,omitempty"` + // ErrorDetails carries structured failure data (e.g. assertion `expect` + // failures in v1.61+: received value, timedOut, customErrorMessage). + ErrorDetails map[string]any `json:"errorDetails,omitempty"` } func (t *pipeTransport) Send(msg map[string]any) error { diff --git a/web_storage.go b/web_storage.go new file mode 100644 index 00000000..50691b11 --- /dev/null +++ b/web_storage.go @@ -0,0 +1,55 @@ +package playwright + +type webStorageImpl struct { + page *pageImpl + kind string // "local" or "session" +} + +func newWebStorage(page *pageImpl, kind string) *webStorageImpl { + return &webStorageImpl{page: page, kind: kind} +} + +func (w *webStorageImpl) Items() ([]WebStorageItem, error) { + result, err := w.page.channel.SendReturnAsDict("webStorageItems", map[string]any{"kind": w.kind}) + if err != nil { + return nil, err + } + items := make([]WebStorageItem, 0) + if rawItems, ok := result["items"].([]any); ok { + for _, raw := range rawItems { + if item, ok := raw.(map[string]any); ok { + items = append(items, WebStorageItem{ + Name: item["name"].(string), + Value: item["value"].(string), + }) + } + } + } + return items, nil +} + +func (w *webStorageImpl) GetItem(name string) (string, error) { + result, err := w.page.channel.SendReturnAsDict("webStorageGetItem", map[string]any{"kind": w.kind, "name": name}) + if err != nil { + return "", err + } + if value, ok := result["value"].(string); ok { + return value, nil + } + return "", nil +} + +func (w *webStorageImpl) SetItem(name string, value string) error { + _, err := w.page.channel.Send("webStorageSetItem", map[string]any{"kind": w.kind, "name": name, "value": value}) + return err +} + +func (w *webStorageImpl) RemoveItem(name string) error { + _, err := w.page.channel.Send("webStorageRemoveItem", map[string]any{"kind": w.kind, "name": name}) + return err +} + +func (w *webStorageImpl) Clear() error { + _, err := w.page.channel.Send("webStorageClear", map[string]any{"kind": w.kind}) + return err +} From 973b5ed5153be9e9c931f4674aa79b947c81e740 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 26 Jun 2026 00:40:49 -0700 Subject: [PATCH 2/7] chore: add roll-playwright Claude Code skill Repo-local skill that automates rolling playwright-go to a new upstream version: discovers API changes from the python/java/dotnet roll PRs and the upstream docs diff, edits the langs gating in patches/main.patch, regenerates, and verifies parity against the sibling clients. Includes find-changes.sh and verify-parity.sh. --- .claude/skills/roll-playwright/SKILL.md | 201 ++++++++++++++++++ .../skills/roll-playwright/find-changes.sh | 84 ++++++++ .../references/finding-changes.md | 142 +++++++++++++ .../references/patch-editing.md | 187 ++++++++++++++++ .../skills/roll-playwright/verify-parity.sh | 86 ++++++++ 5 files changed, 700 insertions(+) create mode 100644 .claude/skills/roll-playwright/SKILL.md create mode 100755 .claude/skills/roll-playwright/find-changes.sh create mode 100644 .claude/skills/roll-playwright/references/finding-changes.md create mode 100644 .claude/skills/roll-playwright/references/patch-editing.md create mode 100755 .claude/skills/roll-playwright/verify-parity.sh diff --git a/.claude/skills/roll-playwright/SKILL.md b/.claude/skills/roll-playwright/SKILL.md new file mode 100644 index 00000000..3d470925 --- /dev/null +++ b/.claude/skills/roll-playwright/SKILL.md @@ -0,0 +1,201 @@ +--- +name: roll-playwright +description: Roll playwright-go to a new upstream Playwright version. Bumps the pinned CLI version, advances the vendored submodule, finds the API changes from the sibling clients (python/java/dotnet roll PRs) and the upstream docs diff, updates patches/main.patch, regenerates the Go API, and verifies the build. Use when asked to "roll to", "bump", "update to", or "upgrade Playwright" to a version like v1.61.0. +--- + +# Roll playwright-go to a new Playwright version + +Roll the client from the currently-pinned version to a target version (e.g. `1.61.0`). Work autonomously; only stop for the decision points called out in **When to ask the user**. + +## What a roll actually is + +playwright-go vendors `microsoft/playwright` as a git submodule (`playwright/`) pinned to a tag. The public Go API (`generated-*.go`) is generated from the submodule's `docs/src/api/*.md` by `playwright/utils/doclint/generateGoApi.js`. Those `.md` files gate each API by language with a `* langs:` line; **the Go client only sees an API if `go` is in that line.** `patches/main.patch` is a diff applied onto the upstream submodule that (a) adds the Go generator and (b) adds `go` to the `* langs:` of every API the Go client exposes. + +So a roll = bump the version, re-apply the patch onto the new submodule, add `go` to the langs of any **newly-added** APIs we want, regenerate, build, test. + +## Inputs + +- Target version, e.g. `1.61.0` (the `v` is optional). If not given, default to the latest stable from `gh release list --repo microsoft/playwright --limit 5` and confirm with the user. +- Current version is read from `run.go` (`const playwrightCliVersion`). + +## Prerequisites (check, install if missing) + +`gh` (authenticated), `node`, `go`, `gofumpt` (`go install mvdan.cc/gofumpt@latest`), `jq`. The submodule must be initialized: `git submodule update --init`. + +## Step 1 — Discover what changed + +Run the helper, which prints the upstream docs diff, every new `* since: vNEW` API with its `langs:` line, and the sibling roll PRs: + +```bash +bash .claude/skills/roll-playwright/find-changes.sh +``` + +**Required: locate the actual roll PR in all three sibling clients** (python, java, dotnet) before porting — you need all three as references, not just one. They are the source of truth for which new APIs are real client surface, their exact method/param names, types, defaults, optionality, AND which upstream PRs were skipped. The helper prints candidates; for each repo open the right PR and confirm it's the one that ports the API surface (not a no-op bump): + +```bash +gh pr view --repo microsoft/playwright-python # body enumerates ported APIs +gh pr view --repo microsoft/playwright-java # body links each upstream PR ported + what was skipped +gh pr view --repo microsoft/playwright-dotnet +``` + +If a repo's helper line is missing or looks like a no-op, dig with the queries in `references/finding-changes.md` until you have a real roll PR for **each** of the three. Use python as the primary shape reference (closest to Go), java for the per-upstream-PR breakdown, dotnet for option/enum shapes. + +> **Critical:** the heavy API port usually lands in the **`-beta` / `-alpha`** roll PR, not the final `X.Y.0` PR (which is often a one-line version bump — e.g. python v1.60.0 final roll was +1/−1, the beta roll +2194/−226). Read the beta/feature roll for the real changes. + +Build a checklist of new/changed APIs to expose in Go, AND a checklist of the **new tests** each sibling added (you'll port these in Step 6b). The Go client deliberately omits some upstream APIs (e.g. some Electron/Android/JS-internal surface) — match python's choices unless there's reason not to. + +> **Watch for return-type naming:** new return-object types carry a `* alias:` in the docs (e.g. `* alias: VirtualCredential`, `* alias: WebStorageItem`). The Go generator does NOT honor it by default and falls back to a generic method-derived name (`Create`, `Get`, `Item`). If you see a generic/duplicate generated struct name, add a guarded special-case in `generateGoApi.js` `generateNameDefault` (scoped by `parent.name`) mapping it to the documented alias — see `references/patch-editing.md`. +> **Watch for pure getters:** a new no-arg getter (e.g. `Page.LocalStorage()`, `BrowserContext.Credentials()`) generates with a trailing `error` return unless its method name is in `methodNoErrArray` in `generateGoApi.js`. Add it there so it matches sibling getters like `Clock()`/`Mouse()`. + +## Step 2 — Bump the version + +1. Edit `run.go`: set `const playwrightCliVersion = ""`. +2. Download the new driver (also advances browser versions for the README step): + ```bash + go run scripts/install-browsers/main.go + ``` + +## Step 3 — Apply the patch onto the new submodule + +```bash +bash scripts/apply-patch.sh +``` + +This checks out the new tag in `playwright/`, creates branch `playwright-build`, and runs `git apply --3way patches/*`. Two outcomes: + +- **Clean apply** → continue to Step 4. +- **Merge conflicts** (`.rej` files, or `<<<<<<<` markers in `playwright/docs/src/api/*.md`) → upstream edited a line the patch also touched. Resolve them: keep the upstream change AND re-add `go` to the `* langs:` line. Then `cd playwright && git add -A && git commit -am "apply patch" && cd ..`. + +## Step 4 — Add `go` to new APIs (the real work) + +For each new API from your Step 1 checklist that you want in Go, edit the matching block in `playwright/docs/src/api/class-*.md` (and `params.md`) so its `* langs:` line includes `go`. The patch in Step 5 is regenerated from these edits. + +```bash +cd playwright +git reset --soft HEAD~1 # un-commit the applied patch so edits fold into one diff (keeps working tree) +# ...edit docs/src/api/*.md: add `go` to the `* langs:` lines of the new APIs... +git add -A && git commit -m "apply patch" +cd .. +``` + +Rules for editing langs (see `references/patch-editing.md` for details and examples): +- `* langs: js, python, csharp` → `* langs: js, python, csharp, go` (append, comma+space). +- A Go-only addition uses `* langs: go`. +- Keep edits surgical — only touch lines for APIs you're intentionally exposing. +- Mirror the python client's method/param naming and optionality; the Go generator turns these docs into `generated-*.go`. + +## Step 5 — Regenerate the patch and the Go API + +```bash +bash scripts/update-patch.sh # regenerates patches/main.patch from the playwright-build branch +go generate ./... # runs generate-api.sh: re-applies patch, runs the JS generator, gofumpt +``` + +`go generate` rewrites `generated-enums.go`, `generated-interfaces.go`, `generated-structs.go`. Review the diff — it should reflect exactly the APIs you added. + +> **Gotcha:** `generate-api.sh` ends with `git submodule update`, which resets `playwright/` back to the parent repo's *currently pinned* (old) commit. After generation you MUST bump the gitlink yourself: `(cd playwright && git checkout v)`, then confirm with `git submodule status playwright`. Otherwise the committed submodule pointer still references the old version. Do this as part of Step 9. + +## Step 6 — Hand-written wiring + +The generator produces interfaces/structs, but most new APIs need a hand-written implementation in the corresponding `*.go` file (e.g. a new `Page` method goes in `page.go`, often with a channel send). Use the sibling clients' impl (python `_impl/*.py`, java/dotnet `Core`/`impl`) as the behavior reference. Look at a previous roll commit for the pattern: + +```bash +git show $(git log --oneline --grep="[Rr]oll to" -1 --format=%H) --stat # files a past roll touched +``` + +New thin wrapper classes (not channel owners) follow `clock.go`: a struct holding the parent `*pageImpl`/`*browserContextImpl`, constructed in the parent's `newPage`/`newBrowserContext`, exposed via a getter, methods call `parent.channel.Send(...)`. Wire the getter field into the parent struct too. + +Watch for **protocol behavior changes** (not just new APIs) — these don't show as build errors but break at runtime. Check the sibling roll PR bodies and the upstream `validator.ts` diff (`gh api .../compare/vOLD...vNEW`) for renamed channel methods, split methods, or changed result shapes. Example from v1.61: assertion `expect` stopped returning `{matches}` and now throws a protocol error carrying `errorDetails` — every assertion path had to be updated. **Grep for duplicated logic** (e.g. there were TWO expect call-sites: `locatorImpl.expect` and `pageAssertionsImpl.expectOnFrame`) so you fix all of them. + +### Step 6b — Port the sibling tests + +This is required, not optional. For every new API, port the tests the siblings added (from your Step 1 test checklist) into `tests/`, following Go conventions (`BeforeEach(t)`, `require`, `server.EMPTY_PAGE`). Mirror python's test cases 1:1 where possible: + +```bash +# See exactly which test files each sibling roll added/changed: +gh pr view --repo microsoft/playwright-python --json files --jq '.files[].path' | grep -i test +gh pr view --repo microsoft/playwright-java --json files --jq '.files[].path' | grep -i [Tt]est +gh pr view --repo microsoft/playwright-dotnet --json files --jq '.files[].path' | grep -i [Tt]est +# Then read the diff of a specific test file to port its cases: +gh pr diff --repo microsoft/playwright-python # find the new test_*.py blocks +``` + +These tests are also your verification that the impl works — run them in Step 7. + +## Step 7 — Build, lint, test + +```bash +gofumpt -l -w . +go build ./... +go vet ./... +go test -race ./... # or target a package; CI runs full -race on 3 OSes x 3 browsers +``` + +The `verify_type_generation` CI job runs `go generate` and fails if it produces a diff — so committed `generated-*.go` must exactly match a fresh generation. Run `go generate ./...` once more and confirm `git diff --ignore-submodules` is clean for the generated files. + +## Step 8 — Update README versions + +Already handled inside `scripts/generate-api.sh` (it runs `update-readme-versions/main.go`), but if you ran steps manually, run `go run scripts/update-readme-versions/main.go` to refresh the browser-version badges. + +## Step 9 — Verify parity against the sibling clients + +Before committing, run the parity verifier. It cross-checks this roll's diff against what python/java/dotnet did, so you catch anything missed: a new `* since: vNEW` API the siblings expose but you didn't add `go` to, an API you exposed that no sibling did, or a sibling test you haven't ported. + +```bash +bash .claude/skills/roll-playwright/verify-parity.sh +``` + +It reports three buckets: +1. **New upstream APIs not in Go** — each `* since: vNEW` block whose `* langs:` still lacks `go`. For each, decide: intentionally omitted (matches a sibling skipping it) or a miss to fix in `patches/main.patch`. +2. **Sibling test files added this roll** — so you can confirm you ported each one into `tests/` (Step 6b). +3. **Go API symbols added vs sibling API surface** — a rough name-level diff to spot a method you exposed that no sibling did (often a naming mistake) or vice-versa. + +Treat each flagged item as needing an explicit decision: fix it, or note why Go intentionally differs. The goal is that any remaining difference from python/java/dotnet is deliberate and explainable. + +## Step 10 — Commit & PR + +Branch name should start with `roll/` (CI triggers on `roll/*`). Match the existing commit style: + +```bash +# Bump the submodule gitlink to the new tag (generate-api.sh reset it — see Step 5 gotcha): +(cd playwright && git checkout v) +git submodule status playwright # confirm it shows v +git diff --submodule=log -- playwright # confirm the pointer moves OLD -> NEW + +git checkout -b roll/v +git add -A && git commit -m "chore: roll to Playwright v" +``` + +Open a PR only if the user asks. Summarize new APIs added in the PR body. + +## Existing tests may need updating for behavior changes + +A roll can change the behavior of an *existing* API, breaking a test that asserts the old behavior — this is not a port bug. When a pre-existing test fails: +1. Confirm you didn't break it (does it fail because of a signature/impl change you made?). +2. If not, check whether upstream changed that behavior: read the relevant sibling roll-PR test diffs and the upstream source diff (`gh api .../compare/vOLD...vNEW --jq '.files[].filename'` then inspect the changed server file). Example from v1.61: `socksClientCertificatesInterceptor.ts` was rewritten so a client cert is only presented to its *matching* origin — so the client-cert test's origin-mismatch navigation went from "503, no cert" to "TLS handshake aborted (`net::ERR_BAD_SSL_CLIENT_AUTH_CERT`)". Update the assertion to the new behavior. +3. New error/abort behaviors can introduce async navigation races (an aborted nav triggers a `chrome-error://` navigation that interrupts the next `Goto` with "is interrupted by another navigation"). Retry the follow-up navigation in a small loop (see `gotoSettled` in `browser_context_client_certificates_test.go`) rather than asserting once. + +## When to ask the user + +- Target version is ambiguous or unreleased upstream. +- A new upstream API is non-trivial (new class, breaking signature change) and you're unsure whether Go should expose it or how to name it. +- A pre-existing test fails and after investigation you're genuinely unsure whether it's an intended upstream behavior change (update the test) or a port bug (fix the code). If the evidence is conclusive (e.g. an upstream source rewrite + the Go client doesn't implement that logic), proceed and flag it; otherwise ask. +- Before pushing or opening a PR. + +## Files & references + +- `find-changes.sh` — discovery helper (read-only): upstream docs diff, new `* since:` APIs, sibling roll PRs. +- `verify-parity.sh` — parity verifier (read-only, Step 9): new APIs missing `go`, sibling test files to port, Go-symbol diff. +- `references/finding-changes.md` — exact `gh` queries + roll-PR conventions for python/java/dotnet + upstream compare. +- `references/patch-editing.md` — anatomy of `patches/main.patch`, `* langs:` gating, editing the generator (`methodNoErrArray`, return-type aliases), conflict resolution. + +## Gotchas + +- `CONTRIBUTING.md` references `packages/protocol/src/protocol.yml` — **stale**. The protocol is now `packages/protocol/spec/*.yml`, and it rarely changes between minors. +- GitHub search tokenizes versions oddly: searching a bare minor like `1.61` matches nothing; use the full `1.61.0`, or list all roll PRs and filter locally (what the helper does). When filtering with `jq`, use `contains("1.61")` not `test("1.61")` — `test` is a regex where `.` matches any char (false positives like `1.3.0-next.106133`). +- Single-quote any `gh api` URL containing `?` (zsh globs on `?`). +- `apply-patch.sh` deletes and recreates the `playwright-build` branch each run — safe to re-run. (It now also always checks out the pinned tag first — a fix for the first-roll-on-fresh-submodule case where it used to patch the old version.) +- `generate-api.sh` ends with `git submodule update`, resetting `playwright/` to the OLD pinned commit. Re-checkout the new tag before committing (Step 10). +- The `.0` sibling roll PR is often just a version bump; the API port is in the preceding `-beta`/`-alpha` PR. +- New API blocks with **no `* langs:` line at all** default to all languages (incl. go) and are generated without any patch edit — the patch edits are only for blocks that explicitly exclude go. +- Run `go test -race` before finishing: a new test that exercises a previously-untested API (e.g. an event callback) can surface a **latent data race in existing impl code**, not just your test. Fix the impl (guard shared fields with a mutex), don't just adjust the test. diff --git a/.claude/skills/roll-playwright/find-changes.sh b/.claude/skills/roll-playwright/find-changes.sh new file mode 100755 index 00000000..8828357d --- /dev/null +++ b/.claude/skills/roll-playwright/find-changes.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# find-changes.sh — Discover everything that changed for a Playwright version roll. +# +# Usage: bash .claude/skills/roll-playwright/find-changes.sh [OLD_VERSION] +# Example: bash .claude/skills/roll-playwright/find-changes.sh 1.61.0 +# +# Prints, for the target version: +# 1. The upstream release notes link + whether the tag exists. +# 2. The exact upstream docs/src/api/*.md files that changed between OLD and NEW. +# 3. Every NEW API block (`* since: vNEW`) and its current `* langs:` line — these +# are the candidates that may need `go` added in patches/main.patch. +# 4. The sibling roll PRs (python / java / dotnet) to read for parity. +# +# Requires: gh (authenticated), jq. Read-only — makes no changes to the repo. +set -euo pipefail + +NEW="${1:?usage: find-changes.sh [OLD_VERSION] e.g. 1.61.0}" +NEW="${NEW#v}" # tolerate a leading v + +# Derive OLD from run.go (the currently-pinned version) unless explicitly given. +if [ -n "${2:-}" ]; then + OLD="${2#v}" +else + OLD="$(grep -oE 'playwrightCliVersion = "[0-9.]+"' run.go | grep -oE '[0-9.]+')" +fi +MINOR="$(echo "$NEW" | grep -oE '^[0-9]+\.[0-9]+')" + +hr() { printf '\n=== %s ===\n' "$1"; } + +hr "Rolling: v$OLD -> v$NEW (minor $MINOR)" + +hr "1. Upstream release" +if gh api "repos/microsoft/playwright/git/refs/tags/v$NEW" >/dev/null 2>&1; then + echo "tag v$NEW exists." + gh release view "v$NEW" --repo microsoft/playwright --json tagName,name,publishedAt --jq '" \(.tagName) published \(.publishedAt)"' 2>/dev/null || true + echo " release notes: https://github.com/microsoft/playwright/releases/tag/v$NEW" +else + echo "WARNING: tag v$NEW does not exist upstream yet. Available recent tags:" + gh release list --repo microsoft/playwright --limit 8 2>/dev/null | sed 's/^/ /' +fi + +hr "2. Changed upstream API docs + protocol (v$OLD...v$NEW)" +CMP="repos/microsoft/playwright/compare/v$OLD...v$NEW" +CHANGED="$(gh api "$CMP" --jq '.files[].filename' 2>/dev/null | grep -E '^docs/src/api/|^packages/protocol/spec/|release-notes' || true)" +if [ -z "$CHANGED" ]; then + echo "(no docs/api/protocol files changed, or compare failed — check both tags exist)" +else + echo "$CHANGED" | sed 's/^/ /' +fi + +hr "3. NEW API blocks gated to other languages (candidates for adding 'go')" +echo "For each changed class-*.md, lines added in this version with their langs line." +echo "Look for '* langs:' WITHOUT 'go' — those are not yet exposed to the Go client." +for f in $(echo "$CHANGED" | grep '^docs/src/api/class-' || true); do + PATCH="$(gh api "$CMP" --jq ".files[] | select(.filename==\"$f\") | .patch" 2>/dev/null || true)" + # Surface added method/property/option headers + their since/langs context. + HITS="$(echo "$PATCH" | grep -E '^\+' | grep -iE 'method:|property:|## |### |since: v'"$MINOR"'|langs:' || true)" + if [ -n "$HITS" ]; then + printf '\n --- %s ---\n' "$f" + echo "$HITS" | sed 's/^/ /' + fi +done + +hr "4. Sibling roll PRs to read for parity" +# Match the minor as a literal substring (contains), NOT test() — test() is a regex +# where `.` is a wildcard, which produces false positives like "1.3.0-next.106133". +echo "# python (heaviest changes are usually in the -beta roll, not the final .0):" +gh pr list --repo microsoft/playwright-python --state merged --search "in:title roll" --limit 60 \ + --json number,title,url --jq ".[] | select(.title | contains(\"$MINOR\")) | \" #\(.number) \(.title)\"" 2>/dev/null || true +echo "# java:" +gh pr list --repo microsoft/playwright-java --state all --search "in:title roll" --limit 60 \ + --json number,title,url --jq ".[] | select(.title | contains(\"$MINOR\")) | \" #\(.number) \(.title)\"" 2>/dev/null || true +echo "# dotnet:" +gh pr list --repo microsoft/playwright-dotnet --state all --search "roll in:title" --limit 60 \ + --json number,title --jq ".[] | select(.title | contains(\"$MINOR\")) | \" #\(.number) \(.title)\"" 2>/dev/null || true + +hr "Next" +cat < --repo microsoft/playwright-python # body lists every ported API + gh pr diff --repo microsoft/playwright-python + Then follow SKILL.md to bump run.go, edit patches/main.patch langs, and regenerate. +EOF diff --git a/.claude/skills/roll-playwright/references/finding-changes.md b/.claude/skills/roll-playwright/references/finding-changes.md new file mode 100644 index 00000000..42edab80 --- /dev/null +++ b/.claude/skills/roll-playwright/references/finding-changes.md @@ -0,0 +1,142 @@ +# Finding the changes for a roll + +Four sources tell you what changed. All commands below are verified against live data +(example: rolling `v1.60.0` → `v1.61.0`). `find-changes.sh` automates most of this; this +file is the manual fallback and the per-source detail. + +The discovery flow across all three sibling clients is the same: **the PR _title_ carries the +target version, branch names don't** (they're inconsistent and sometimes disagree with the +title). GitHub full-text search tokenizes a dotted version oddly — a bare minor like `1.61` +matches nothing, but the full `1.61.0` matches. The robust move is to list all `roll` PRs and +filter locally on the version. + +> **Read the `-beta`/`-alpha` roll, not just the `.0`.** Each minor typically gets 2–3 roll +> PRs (alpha → beta → stable). The stable `X.Y.0` PR is frequently a one-line driver-version +> bump; the **API port lands in the preceding beta/alpha PR**. Verified: python `1.60.0` final +> roll #3079 was +1/−1 (one line), while the beta roll #3069 was +2194/−226 across 36 files. + +--- + +## 1. microsoft/playwright (upstream monorepo) — the diff + +Upstream has no roll PRs; it ships `vX.Y.Z` tags + GitHub Releases. `docs/src/api/*.md` is the +source of truth every client generates from. + +```bash +# Confirm the tag/release exists and read notes +gh release list --repo microsoft/playwright --limit 10 +gh release view v1.61.0 --repo microsoft/playwright --json tagName,name,publishedAt,body + +# Currently-pinned version (the OLD side of the diff) +grep playwrightCliVersion run.go + +# Changed API docs + protocol between OLD and NEW +gh api repos/microsoft/playwright/compare/v1.60.0...v1.61.0 --jq '.files[].filename' \ + | grep -E '^docs/src/api/|^packages/protocol/spec/|release-notes' + +# Pull the hunk for one changed file and find newly-added APIs +gh api 'repos/microsoft/playwright/compare/v1.60.0...v1.61.0' \ + --jq '.files[] | select(.filename=="docs/src/api/class-apiresponse.md") | .patch' +``` + +**Reading langs annotations.** Each method/option/property block carries: +- `* since: vX.Y` — version it appeared. +- `* langs: js, python, csharp` — which clients expose it (comma+space separated). + +A new API gated to other languages (e.g. `* langs: js, python`) is **not** in Go until you add +`go`. Look in each hunk for added blocks containing `* since: v` and inspect their +`* langs:` line — those are the candidates. Variants: +- sub-list form for per-language naming: `* langs:\n - alias-python: and_` +- Go-only block: `* langs: go` + +**Protocol** lives under `packages/protocol/spec/*.yml` (`api.yml`, `page.yml`, `network.yml`, +…). There is **no** `packages/protocol/src/protocol.yml` (the `CONTRIBUTING.md` reference is +stale). Protocol rarely changes between minors. + +**zsh gotcha:** single-quote any `gh api` URL containing `?` (e.g. `'...?ref=v1.61.0'`). + +--- + +## 2. microsoft/playwright-python + +Closest in shape to Go — use it as the primary parity reference. + +```bash +# Robust: list roll PRs, filter on the version locally +gh pr list --repo microsoft/playwright-python --state merged \ + --search "in:title roll" --limit 60 --json number,title,url \ + --jq '.[] | select(.title | test("1\\.61\\.")) | "#\(.number)\t\(.title)\t\(.url)"' + +# Read it — the body enumerates ported APIs +gh pr view --repo microsoft/playwright-python +gh pr view --repo microsoft/playwright-python --json files --jq '.files[].path' +gh pr diff --repo microsoft/playwright-python +``` + +- **Title:** `chore: roll to X.Y.Z` / `chore: roll driver to X.Y.Z` / `chore(roll): vX.Y.Z`; + beta rolls carry `-beta-`. +- **Extract:** PR body; new files under `playwright/_impl/` (new classes); diffs of `_impl/*.py` + and the regenerated `async_api/_generated.py` / `sync_api/_generated.py` for exact method + names, param names/types, defaults, optionality, and removed/deprecated params. + +--- + +## 3. microsoft/playwright-java + +Best for a per-upstream-PR breakdown — java roll bodies link each `microsoft/playwright` PR +ported and note which needed no client change / were skipped. + +```bash +# Most robust: list all roll PRs, grep the minor locally (surfaces alpha/beta/stable) +gh pr list --repo microsoft/playwright-java --state merged --search "roll" --limit 100 | grep '1.61' + +# Exact-match shortcut — must use the FULL version incl. patch +gh search prs --repo microsoft/playwright-java "1.61.0" --limit 10 + +gh pr view --repo microsoft/playwright-java +gh pr diff --repo microsoft/playwright-java +``` + +- **Title:** `chore: roll driver to X.Y.Z` / `chore: roll X.Y.Z`; occasionally `feat:` for big + feature rolls. Version marker file: `scripts/DRIVER_VERSION`. +- **Extract:** PR body (per-PR port list); new public interfaces under + `playwright/src/main/java/com/microsoft/playwright/` and option/enum types under `.../options/` + map ~1:1 to Go bindings. + +--- + +## 4. microsoft/playwright-dotnet + +```bash +# Search by title + version token (drop the patch to the minor if patch-level is empty) +gh pr list --repo microsoft/playwright-dotnet --state all \ + --search "roll in:title 1.61.0" --json number,title,headRefName,state,mergedAt + +# List all roll PRs and eyeball +gh pr list --repo microsoft/playwright-dotnet --state all --search "roll in:title" \ + --limit 50 --json number,title,state,mergedAt + +# Confirm a candidate actually bumps the driver (some "roll"-matching PRs aren't rolls) +gh pr diff --repo microsoft/playwright-dotnet | grep DriverVersion +``` + +- **Title:** `chore: roll driver to X.Y.Z` / `chore(roll): roll Playwright to vX.Y.Z`; pre-release + rolls embed `-alpha-`/`-beta-`. **Match on title, not branch.** +- **Version marker:** `src/Common/Version.props` `` (changed in every roll). +- **Extract:** ported API surface in `src/Playwright/API/Generated/**` (new `I*.cs` members, + `Options/*Options.cs`, `Types/`, `Enums/`); wire changes in + `src/Playwright/Transport/Protocol/Generated/**`. PR body is the best changelog. + +--- + +## How these map to the Go patch + +- The upstream `v...v` docs diff tells you **which API/option blocks gained + `* since: v`** and whether their `* langs:` already reaches Go. +- The **sibling PRs tell you which of those are real client surface** (names, types, defaults, + optionality, what was skipped). Use python/java for parity decisions. +- The **Go roll mirrors this by adding `go` to the `* langs:`** of each desired new API in + `docs/src/api/*.md`, recorded as one-line edits in `patches/main.patch`. Then regenerate. + +The three sibling repos also keep their own roll runbooks at `.claude/skills/playwright-roll/SKILL.md` +— worth cross-referencing if a roll gets hairy. diff --git a/.claude/skills/roll-playwright/references/patch-editing.md b/.claude/skills/roll-playwright/references/patch-editing.md new file mode 100644 index 00000000..2d18c9fe --- /dev/null +++ b/.claude/skills/roll-playwright/references/patch-editing.md @@ -0,0 +1,187 @@ +# Editing patches/main.patch + +`patches/main.patch` is a git diff applied onto the vendored `playwright/` submodule. You never +hand-edit the `.patch` file directly — you edit the files **inside `playwright/`**, then +regenerate the patch with `scripts/update-patch.sh`. This file explains what the patch contains +and how to add/change entries. + +## What the patch contains + +24 file diffs, two kinds: + +1. **`utils/doclint/generateGoApi.js`** — a new ~880-line file: the Go API generator. Vendored + here because upstream doesn't ship a Go generator. You rarely touch this; only if the upstream + doclint framework (`documentation.js`, `api_parser.js`, `Type` shapes) changes in a way that + breaks generation, or to map a new Go type. The CI `verify_type_generation` job will surface + such breakage. + +2. **`docs/src/api/*.md` + `params.md`** — ~130 small edits, almost all of the form "add `go` to + a `* langs:` line" or "add a Go-only block". This is the gate that decides what the Go client + exposes. + +## How langs gating works + +The generator (`generateGoApi.js`) calls `documentation.filterForLanguage('go')`. Any API block +whose `* langs:` line does **not** include `go` is dropped before code generation. So: + +- An API present in `generated-*.go` ⇒ its docs block has `go` in `* langs:` (or no `* langs:` + line at all, meaning "all languages"). +- A new upstream API gated to other languages (e.g. `* langs: js, python`) is invisible to Go + until you add `go`. + +## The three edit patterns (real examples from the current patch) + +### 1. Append `go` to an existing langs line + +The most common edit. Append `, go` (comma + space) to the end: + +```diff +-* langs: js, python ++* langs: js, python, go +``` +```diff +-* langs: java, js, csharp ++* langs: java, js, csharp, go +``` + +Applies to method blocks (`## method:` / `## async method:`), option blocks (`### option:`), +param blocks (`### param:`), and property blocks (`## property:`). + +### 2. Add a langs line to a block that had none + +If an option block under a method had no `* langs:` line but you need it Go-visible distinctly, +add one (often when the surrounding method's langs was narrowed). Example from the patch — a new +struct option exposed to all langs incl. go: + +```diff + * since: v1.51 ++* langs: js, python, java, csharp, go + - `indexedDB` ? +``` + +### 3. Add a Go-only block + +When Go needs a differently-shaped signature than the shared one (e.g. an option struct where +other languages take a positional param). The pattern: leave the original block restricted to the +other languages and add a parallel `* langs: go` block. Real example (`Browser.startTracing.page`): + +```diff + ### param: Browser.startTracing.page + * since: v1.11 ++* langs: js, java, python, csharp + - `page` ?<[Page]> + + Optional, if specified, tracing includes screenshots of the given page. + ++### option: Browser.startTracing.page ++* since: v1.11 ++* langs: go ++- `page` <[Page]> ++ ++Optional, if specified, tracing includes screenshots of the given page. +``` + +Here the upstream `param` is narrowed away from `go`, and a `go`-only `option` (struct field) is +added — because the Go client passes options as a struct, not a positional param. + +## Workflow to change the patch + +From `CONTRIBUTING.md`, adapted: + +```bash +# 1. Apply the current patch onto the (newly-bumped) submodule +bash scripts/apply-patch.sh + +# 2. Un-commit it so your edits fold into the same diff (working tree is preserved) +cd playwright +git reset --soft HEAD~1 # CONTRIBUTING says `git reset HEAD~1`; --soft keeps it staged. Either works. + +# 3. Edit docs/src/api/*.md — add `go` to the langs of the new APIs (patterns above) + +# 4. Re-commit +git add -A && git commit -m "apply patch" +cd .. + +# 5. Regenerate the patch file and the Go code +bash scripts/update-patch.sh # git diff playwright-build^1..playwright-build > patches/main.patch +go generate ./... # re-applies patch, runs the JS generator, gofumpt-formats +``` + +## Resolving conflicts from `git apply --3way` + +When upstream edits a line the patch also touches (common: upstream added a language to a +`* langs:` line, or reflowed a doc paragraph), `apply-patch.sh` leaves a conflict. + +1. Find them: `cd playwright && git status` (look for `.rej`), or + `grep -rn '<<<<<<<' docs/src/api/`. +2. Resolve by **keeping the upstream content AND ensuring `go` is in the langs line.** Example: if + upstream changed `* langs: js, python` → `* langs: js, python, java` and the patch wanted + `* langs: js, python, go`, the merged result is `* langs: js, python, java, go`. +3. `git add -A && git commit -am "apply patch"`, then regenerate (Step 5 above). + +## Verifying your edits + +- `go generate ./...` then `git diff --ignore-submodules generated-interfaces.go` — the generated + diff should contain exactly the APIs you added, nothing spurious. +- The CI `verify_type_generation` job runs `go generate` and fails on any diff, so committed + generated files must match a fresh run byte-for-byte (after `gofumpt`). +- `go build ./...` then `go vet ./...` — generated interfaces compile, but new methods usually + need a hand-written impl in the matching `*.go` (see SKILL.md Step 6). + +## Editing the generator (`generateGoApi.js`) + +Sometimes the langs edits aren't enough and you must tweak the vendored generator itself (it's +part of the patch, so edit it inside `playwright/`, then `update-patch.sh`). Two cases recur on +rolls that add new APIs: + +### Pure getters returning a spurious `error` + +By default the generator gives every method a trailing `error` return. Pure no-arg getters +(`Clock()`, `Mouse()`, `Keyboard()`, …) are exempted via the `methodNoErrArray` list near the top +of the file. A new getter like `Page.LocalStorage()` / `BrowserContext.Credentials()` will +otherwise generate as `LocalStorage() (WebStorage, error)` and break the hand-written impl. Add +the method name to `methodNoErrArray` (keep it alphabetical): + +```js +'Context', +'Contexts', +'Credentials', // <- added: pure getter, no error +'DefaultValue', +... +'Keyboard', +'LocalStorage', // <- added +... +'ServiceWorkers', +'SessionStorage', // <- added +``` + +### Generic/duplicate return-type names (honor `* alias:`) + +Inline return-object types get a name derived from the method (`Create`, `Get`, `Item`) which is +generic and can collide. The docs annotate the intended name with `* alias:` (e.g. +`* alias: VirtualCredential`, `* alias: WebStorageItem`), which java/csharp honor but the Go +generator does not. Add a **scoped** special-case in `generateNameDefault` alongside the existing +ones (`SecurityDetail` → `ResponseSecurityDetailsResult`, etc.), guarded by `parent.name` so it +can't rename an unrelated type: + +```js +// WebAuthn: Credentials.Create/Get return the same shape; use the documented alias. +if (parent.name === 'Credentials' && (attemptedName === 'Create' || attemptedName === 'Get')) + attemptedName = 'VirtualCredential'; +if (parent.name === 'WebStorage' && attemptedName === 'Item') + attemptedName = 'WebStorageItem'; +``` + +Do NOT make the generator honor `langAliases` globally — 40+ existing types carry aliases and a +blanket change would rename stable public types. Scope every fix. + +After either edit: `cd playwright && git commit -am "apply patch"` (or amend the Applied-patches +commit), `update-patch.sh`, `go generate ./...`, then confirm the generated signatures. + +## Tips + +- Keep edits surgical: only flip langs for APIs you intend to expose. A stray `go` produces a + generated method with no impl → build break. +- Match python's naming/optionality; the Go method/struct names derive from these docs. +- `enumTypes`, `classNameMap`, and `methodNoErrArray` near the top of `generateGoApi.js` map names + and mark which methods don't return `error` (see the generator-editing section above). diff --git a/.claude/skills/roll-playwright/verify-parity.sh b/.claude/skills/roll-playwright/verify-parity.sh new file mode 100755 index 00000000..729f1c6e --- /dev/null +++ b/.claude/skills/roll-playwright/verify-parity.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# verify-parity.sh — Cross-check a completed (or in-progress) roll against the +# python / java / dotnet sibling clients, so nothing is silently missed. +# +# Usage: bash .claude/skills/roll-playwright/verify-parity.sh +# Example: bash .claude/skills/roll-playwright/verify-parity.sh 1.61.1 +# +# Run AFTER apply-patch + go generate, while the playwright/ submodule is on the +# new tag (the playwright-build branch is fine too). Reports three buckets: +# 1. New upstream APIs (`* since: vNEW`) whose `* langs:` still lacks `go`. +# 2. Test files each sibling roll added/changed — confirm you ported them. +# 3. New Go API symbols vs the sibling roll's added symbols (rough name diff). +# +# Requires: gh (authenticated), jq, git. Read-only. +set -euo pipefail + +NEW="${1:?usage: verify-parity.sh e.g. 1.61.1}" +NEW="${NEW#v}" +MINOR="$(echo "$NEW" | grep -oE '^[0-9]+\.[0-9]+')" +SINCE="v$MINOR" +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "$ROOT" + +hr() { printf '\n========== %s ==========\n' "$1"; } + +# Resolve the roll PR number for a sibling repo: newest merged roll PR whose +# title contains the minor (prefers a non-alpha/beta title, else the newest). +find_roll_pr() { + local repo="$1" + gh pr list --repo "$repo" --state all --search "in:title roll" --limit 80 \ + --json number,title,mergedAt \ + --jq "[.[] | select(.title | contains(\"$MINOR\"))] + | (map(select(.title | (contains(\"alpha\") or contains(\"beta\")) | not)) + .) + | .[0] | \"\(.number)\t\(.title)\"" 2>/dev/null || true +} + +hr "1. New upstream APIs ($SINCE) still missing 'go' in langs" +echo "Each block below is new this version but NOT exposed to Go. Decide per item:" +echo "intentionally omitted (a sibling also skips it) OR a miss to fix in the patch." +MISSING=0 +for f in playwright/docs/src/api/class-*.md playwright/docs/src/api/params.md; do + [ -f "$f" ] || continue + # Walk blocks: a header line (## / ###) starts a block; within it look for + # `* since: vMINOR` and a `* langs:` line lacking 'go'. awk over the file. + awk -v since="$SINCE" -v file="$(basename "$f")" ' + /^#+ (async )?(method|property|event|param|option):/ { hdr=$0; sincehit=0; langs="" } + /^\* since:/ { if ($0 ~ since) sincehit=1 } + /^\* langs:/ { langs=$0 } + /^$/ { + if (sincehit && langs != "" && langs !~ /[, ]go([, ]|$)/ && langs !~ /langs: go/) { + printf " %s\n %s\n %s\n", file, hdr, langs + } + sincehit=0; langs="" + } + ' "$f" +done +echo "(blocks with NO '* langs:' line default to all-languages incl. go — not listed)" + +hr "2. Sibling roll PRs + the test files they added" +for repo in microsoft/playwright-python microsoft/playwright-java microsoft/playwright-dotnet; do + pr="$(find_roll_pr "$repo")" + num="$(echo "$pr" | cut -f1)" + title="$(echo "$pr" | cut -f2-)" + printf '\n# %s -> PR #%s %s\n' "$repo" "${num:-?}" "$title" + if [ -n "$num" ]; then + gh pr view "$num" --repo "$repo" --json files \ + --jq '.files[].path | select(test("(?i)test|spec"))' 2>/dev/null | sed 's/^/ /' || true + else + echo " (no roll PR found — search manually, see references/finding-changes.md)" + fi +done +echo "" +echo ">> Confirm every NEW test file above has a counterpart under ./tests/ in this repo." + +hr "3. New Go API symbols this roll (for eyeball diff vs siblings above)" +echo "Interface methods / structs added to generated files vs committed HEAD:" +git diff HEAD -- generated-interfaces.go generated-structs.go 2>/dev/null \ + | grep -E '^\+' | grep -vE '^\+\+\+' \ + | grep -oE '^\+\t([A-Z][A-Za-z0-9]+\(|type [A-Z][A-Za-z0-9]+ )' \ + | sed -E 's/^\+\t/ /; s/\($//' | sort -u | head -80 +echo "" +echo ">> Compare these names against the sibling PR diffs. A Go symbol no sibling" +echo " has is often a naming mistake; a sibling symbol missing here may be a gap." + +hr "Done" +echo "Every remaining difference from python/java/dotnet should be deliberate and explainable." From 9aa8291c0ad0acc4d9ffd213ec0d45c583829078 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 26 Jun 2026 09:19:16 -0700 Subject: [PATCH 3/7] test: fix cross-browser client-cert/websocket assertions and lint - client-cert handshake-abort error wording varies by browser AND platform (chromium ERR_BAD_SSL_CLIENT_AUTH_CERT, firefox SSL_ERROR_UNKNOWN, webkit 'Certificate is required' on linux/macos / 'Failure when receiving data' on windows); match any known variant - Firefox websocket error is now ': 404' like other browsers (was CLOSE_ABNORMAL) since v1.61 - replace deprecated WaitForTimeout with time.Sleep; check Close/Stop returns --- ...rowser_context_client_certificates_test.go | 32 ++++++++++++++++--- tests/screencast_test.go | 4 +-- tests/websocket_test.go | 8 ++--- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/tests/browser_context_client_certificates_test.go b/tests/browser_context_client_certificates_test.go index 5531d97c..19d4bd4e 100644 --- a/tests/browser_context_client_certificates_test.go +++ b/tests/browser_context_client_certificates_test.go @@ -10,6 +10,7 @@ import ( "runtime" "strings" "testing" + "time" "github.com/playwright-community/playwright-go" "github.com/stretchr/testify/require" @@ -17,7 +18,7 @@ import ( // gotoSettled navigates to url, retrying while a concurrent navigation keeps // interrupting it. Since v1.61 a client-cert handshake abort triggers an async -// navigation to chrome-error:// (and an automatic reload of the target), which +// navigation to the error page (and an automatic reload of the target), which // races with the immediately-following navigation to the matching origin. func gotoSettled(t *testing.T, p playwright.Page, url string) { t.Helper() @@ -30,11 +31,34 @@ func gotoSettled(t *testing.T, p playwright.Page, url string) { if !strings.Contains(err.Error(), "is interrupted by another navigation") { break } - p.WaitForTimeout(50) + time.Sleep(50 * time.Millisecond) } require.NoError(t, err) } +// requireClientCertHandshakeError asserts that err is the TLS-handshake abort a +// browser reports when it presents no client certificate to a server that +// requires one (v1.61+ scopes the cert to its matching origin). The wording +// varies by browser and platform, so match any of the known variants. +func requireClientCertHandshakeError(t *testing.T, err error) { + t.Helper() + require.Error(t, err) + msg := err.Error() + variants := []string{ + "net::ERR_BAD_SSL_CLIENT_AUTH_CERT", // chromium + "SSL_ERROR_UNKNOWN", // firefox + "Certificate is required", // webkit (linux/macos) + "Failure when receiving data", // webkit (windows) + } + for _, v := range variants { + if strings.Contains(msg, v) { + return + } + } + require.Failf(t, "unexpected client-cert handshake error", + "error %q did not match any known TLS-handshake abort variant", msg) +} + func NewTLSServerRequireClientCert(t *testing.T) *httptest.Server { t.Helper() certPath := Asset("client-certificates/server/server_cert.pem") @@ -111,7 +135,7 @@ func TestClientCerts(t *testing.T) { if tlsServer.EnableHTTP2 { require.ErrorContains(t, err, "net::ERR_CONNECTION_CLOSED") } else { - require.ErrorContains(t, err, "net::ERR_BAD_SSL_CLIENT_AUTH_CERT") + requireClientCertHandshakeError(t, err) } gotoSettled(t, page, tlsServer.URL) @@ -146,7 +170,7 @@ func TestClientCerts(t *testing.T) { if tlsServer.EnableHTTP2 { require.ErrorContains(t, err, "net::ERR_CONNECTION_CLOSED") } else { - require.ErrorContains(t, err, "net::ERR_BAD_SSL_CLIENT_AUTH_CERT") + requireClientCertHandshakeError(t, err) } gotoSettled(t, page2, tlsServer.URL) diff --git a/tests/screencast_test.go b/tests/screencast_test.go index fcbdc25d..30f1952f 100644 --- a/tests/screencast_test.go +++ b/tests/screencast_test.go @@ -15,7 +15,7 @@ func TestScreencastOnFrameReceivesViewportSizeAndTimestamp(t *testing.T) { Viewport: &playwright.Size{Width: 1000, Height: 400}, }) require.NoError(t, err) - defer ctx.Close() + defer ctx.Close() //nolint:errcheck p, err := ctx.NewPage() require.NoError(t, err) @@ -64,7 +64,7 @@ func TestScreencastShowActionsAcceptsCursorParam(t *testing.T) { require.NoError(t, sc.Start(playwright.ScreencastStartOptions{ OnFrame: func(playwright.OnFrame) {}, })) - defer sc.Stop() + defer sc.Stop() //nolint:errcheck require.NoError(t, sc.ShowActions(playwright.ScreencastShowActionsOptions{ Duration: playwright.Float(100), diff --git a/tests/websocket_test.go b/tests/websocket_test.go index 59a876bc..b7ff8bf5 100644 --- a/tests/websocket_test.go +++ b/tests/websocket_test.go @@ -173,9 +173,7 @@ func TestWebSocketShouldEmitErrorEvent(t *testing.T) { }`, server.PORT) require.NoError(t, err) msg := <-chanMsg - if isFirefox { - require.Equal(t, msg, "CLOSE_ABNORMAL") - } else { - require.Contains(t, msg, ": 404") - } + // Since v1.61 Firefox reports the same ": 404" error as the other browsers + // instead of "CLOSE_ABNORMAL". + require.Contains(t, msg, ": 404") } From 195db2438a0fc0c1f037c40347135d196f188844 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 26 Jun 2026 10:40:57 -0700 Subject: [PATCH 4/7] feat: support comma-separated setTestIdAttribute; expand v1.61 test coverage - getByTestId JSON-quotes the test-id attribute name when it contains a comma, so a comma-separated list set via SetTestIdAttribute (upstream #40844) matches any of the named attributes instead of producing an invalid selector. Mirrors upstream encodeTestIdAttributeName. - add tests matching the sibling clients: comma-separated getByTestId, session storage items/remove/clear, local-vs-session independence. --- locator_helpers.go | 16 +++++++++++- tests/locator_get_by_test.go | 28 +++++++++++++++++++++ tests/page_web_storage_test.go | 45 ++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/locator_helpers.go b/locator_helpers.go index d0480d21..52832240 100644 --- a/locator_helpers.go +++ b/locator_helpers.go @@ -166,7 +166,21 @@ func getByTextSelector(text any, exact bool) string { } func getByTestIdSelector(testIdAttributeName string, testId any) string { - return fmt.Sprintf(`internal:testid=[%s=%s]`, testIdAttributeName, escapeForAttributeSelector(testId, true)) + return fmt.Sprintf(`internal:testid=[%s=%s]`, encodeTestIdAttributeName(testIdAttributeName), escapeForAttributeSelector(testId, true)) +} + +// encodeTestIdAttributeName JSON-quotes the attribute name when it contains a +// comma so the engine treats a comma-separated list (e.g. "data-pw,data-ti") as +// multiple attribute names rather than malforming the selector. Mirrors upstream +// encodeTestIdAttributeName (packages/isomorphic/locatorUtils.ts). +func encodeTestIdAttributeName(testIdAttributeName string) string { + if strings.Contains(testIdAttributeName, ",") { + encoded, err := json.Marshal(testIdAttributeName) + if err == nil { + return string(encoded) + } + } + return testIdAttributeName } func getByTitleSelector(text any, exact bool) string { diff --git a/tests/locator_get_by_test.go b/tests/locator_get_by_test.go index 6457639f..bd677705 100644 --- a/tests/locator_get_by_test.go +++ b/tests/locator_get_by_test.go @@ -39,6 +39,34 @@ func TestGetByTestIdEscapeId(t *testing.T) { require.Equal(t, 1, count) } +func TestGetByTestIdCommaSeparatedAttributesShouldMatchAny(t *testing.T) { + BeforeEach(t) + + // Since v1.61 setTestIdAttribute accepts a comma-separated list of attribute + // names; getByTestId matches an element carrying any of them. + pw.Selectors.SetTestIdAttribute("data-pw,data-ti") + defer pw.Selectors.SetTestIdAttribute("data-testid") + + require.NoError(t, page.SetContent(` +
+
first
+
second
+
third
+
`)) + + count, err := page.GetByTestId("Hello").Count() + require.NoError(t, err) + require.Equal(t, 2, count) + + count, err = page.MainFrame().GetByTestId("Hello").Count() + require.NoError(t, err) + require.Equal(t, 2, count) + + count, err = page.Locator("section").GetByTestId("Hello").Count() + require.NoError(t, err) + require.Equal(t, 2, count) +} + func TestGetByText(t *testing.T) { BeforeEach(t) diff --git a/tests/page_web_storage_test.go b/tests/page_web_storage_test.go index 1d0584bb..16b8478c 100644 --- a/tests/page_web_storage_test.go +++ b/tests/page_web_storage_test.go @@ -88,3 +88,48 @@ func TestPageSessionStorageSetAndGet(t *testing.T) { require.NoError(t, err) require.Equal(t, "bar", jsValue) } + +func TestPageSessionStorageItemsRemoveAndClear(t *testing.T) { + BeforeEach(t) + + _, err := page.Goto(server.EMPTY_PAGE) + require.NoError(t, err) + + storage := page.SessionStorage() + require.NoError(t, storage.SetItem("a", "1")) + require.NoError(t, storage.SetItem("b", "2")) + + items, err := storage.Items() + require.NoError(t, err) + require.Contains(t, items, playwright.WebStorageItem{Name: "a", Value: "1"}) + require.Contains(t, items, playwright.WebStorageItem{Name: "b", Value: "2"}) + + require.NoError(t, storage.RemoveItem("a")) + value, err := storage.GetItem("a") + require.NoError(t, err) + require.Equal(t, "", value) + + require.NoError(t, storage.Clear()) + length, err := page.Evaluate("() => sessionStorage.length") + require.NoError(t, err) + require.Equal(t, 0, length) +} + +// localStorage and sessionStorage are independent stores (mirrors upstream). +func TestPageLocalAndSessionStorageAreIndependent(t *testing.T) { + BeforeEach(t) + + _, err := page.Goto(server.EMPTY_PAGE) + require.NoError(t, err) + + require.NoError(t, page.LocalStorage().SetItem("key", "local")) + require.NoError(t, page.SessionStorage().SetItem("key", "session")) + + localValue, err := page.LocalStorage().GetItem("key") + require.NoError(t, err) + require.Equal(t, "local", localValue) + + sessionValue, err := page.SessionStorage().GetItem("key") + require.NoError(t, err) + require.Equal(t, "session", sessionValue) +} From 4b5e947c27a5b5f53213917402d54ffabfbb9d15 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 26 Jun 2026 10:46:01 -0700 Subject: [PATCH 5/7] chore(skill): require porting every sibling test as a Go test Make Step 6b mandatory and exhaustive: every test python/java/dotnet add in their roll must have a Go counterpart (port the union; a sibling test often exposes a Go feature gap, e.g. the v1.61 comma-testid bug). verify-parity.sh now lists each sibling's added test FUNCTION names, not just files. Step 7 requires running new tests on all three browsers (engine-specific behavior). --- .claude/skills/roll-playwright/SKILL.md | 41 ++++++++++++++----- .../skills/roll-playwright/verify-parity.sh | 23 ++++++++--- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/.claude/skills/roll-playwright/SKILL.md b/.claude/skills/roll-playwright/SKILL.md index 3d470925..768143ca 100644 --- a/.claude/skills/roll-playwright/SKILL.md +++ b/.claude/skills/roll-playwright/SKILL.md @@ -107,20 +107,32 @@ New thin wrapper classes (not channel owners) follow `clock.go`: a struct holdin Watch for **protocol behavior changes** (not just new APIs) — these don't show as build errors but break at runtime. Check the sibling roll PR bodies and the upstream `validator.ts` diff (`gh api .../compare/vOLD...vNEW`) for renamed channel methods, split methods, or changed result shapes. Example from v1.61: assertion `expect` stopped returning `{matches}` and now throws a protocol error carrying `errorDetails` — every assertion path had to be updated. **Grep for duplicated logic** (e.g. there were TWO expect call-sites: `locatorImpl.expect` and `pageAssertionsImpl.expectOnFrame`) so you fix all of them. -### Step 6b — Port the sibling tests +### Step 6b — Port EVERY sibling test (required) -This is required, not optional. For every new API, port the tests the siblings added (from your Step 1 test checklist) into `tests/`, following Go conventions (`BeforeEach(t)`, `require`, `server.EMPTY_PAGE`). Mirror python's test cases 1:1 where possible: +**Mandatory and exhaustive: every test python, java, AND dotnet added in their roll PR must have a Go counterpart in `tests/`.** Do not cherry-pick — enumerate all of them and port each, following Go conventions (`BeforeEach(t)`, `require`, `server.EMPTY_PAGE`, per-browser branches via `isChromium`/`isFirefox`/`isWebKit`). This is both for coverage parity and because **a sibling test frequently exposes a feature that is broken or missing in Go** — porting it is how you find that. + +> Real example (v1.61): java's `getByTestIdWithCommaSeparatedTestIdAttributesShouldMatchAny` had no Go equivalent. Porting it revealed that comma-separated `SetTestIdAttribute` produced an invalid selector in Go (the `encodeTestIdAttributeName` quoting was missing). The test caught a real bug — the impl had to be fixed, not just the test added. + +Build a complete checklist. List every test function each sibling added, then tick off the Go equivalent: ```bash -# See exactly which test files each sibling roll added/changed: -gh pr view --repo microsoft/playwright-python --json files --jq '.files[].path' | grep -i test -gh pr view --repo microsoft/playwright-java --json files --jq '.files[].path' | grep -i [Tt]est -gh pr view --repo microsoft/playwright-dotnet --json files --jq '.files[].path' | grep -i [Tt]est -# Then read the diff of a specific test file to port its cases: -gh pr diff --repo microsoft/playwright-python # find the new test_*.py blocks +# 1. List the test FILES each sibling roll added/changed: +for repo in microsoft/playwright-python microsoft/playwright-java microsoft/playwright-dotnet; do + echo "## $repo"; gh pr view --repo $repo --json files --jq '.files[].path' | grep -iE 'test|spec' +done + +# 2. Extract the test FUNCTION NAMES added (so nothing is missed): +gh pr diff --repo microsoft/playwright-python | grep -E '^\+\s*(async )?def test_' +gh pr diff --repo microsoft/playwright-java | grep -E '^\+\s*(public )?void ' +gh pr diff --repo microsoft/playwright-dotnet | grep -iE '^\+\s*public async Task|\[(Playwright)?Test\]' + +# 3. For each, read the body and port the cases: +gh pr diff --repo ``` -These tests are also your verification that the impl works — run them in Step 7. +For each new API, dedupe across the three (they overlap heavily) and port the **union** of their cases — e.g. python tests session-storage clear separately, java tests local/session independence; port both. If a sibling test can't be ported (feature intentionally not in Go, or needs an unavailable fixture like an HTTPS server), `log` it explicitly with the reason rather than silently dropping it. + +These tests are also your verification that the impl works — run them across all three browsers in Step 7 (a test green on chromium may fail on firefox/webkit due to engine-specific error strings or behavior). ## Step 7 — Build, lint, test @@ -128,9 +140,18 @@ These tests are also your verification that the impl works — run them in Step gofumpt -l -w . go build ./... go vet ./... -go test -race ./... # or target a package; CI runs full -race on 3 OSes x 3 browsers +golangci-lint run ./... # CI's Lint job; fails on errcheck/staticcheck/gofumpt +# Run the NEW tests on every browser — CI is a 3 OS x 3 browser matrix and +# engine-specific behavior (error strings, etc.) differs. A pass on chromium +# alone is NOT enough. +for b in chromium firefox webkit; do BROWSER=$b HEADLESS=1 go test -race ./tests/ -run ''; done +go test -race ./... # full suite at least once ``` +Lint commonly fails on test helpers: unchecked `defer x.Close()`/`Stop()` (errcheck → add `//nolint:errcheck`), deprecated `WaitForTimeout` (staticcheck → use `time.Sleep`), and gofumpt alignment. Run `golangci-lint run` locally; the CI Lint job is fast but blocking. + +Cross-browser is not optional: a v1.61 client-cert assertion passed on chromium but failed on firefox (`SSL_ERROR_UNKNOWN`), webkit-linux/macos (`Certificate is required`), and webkit-windows (`Failure when receiving data`) — each emits a different error string. Match any known variant rather than one browser's wording. + The `verify_type_generation` CI job runs `go generate` and fails if it produces a diff — so committed `generated-*.go` must exactly match a fresh generation. Run `go generate ./...` once more and confirm `git diff --ignore-submodules` is clean for the generated files. ## Step 8 — Update README versions diff --git a/.claude/skills/roll-playwright/verify-parity.sh b/.claude/skills/roll-playwright/verify-parity.sh index 729f1c6e..3cd21066 100755 --- a/.claude/skills/roll-playwright/verify-parity.sh +++ b/.claude/skills/roll-playwright/verify-parity.sh @@ -56,21 +56,32 @@ for f in playwright/docs/src/api/class-*.md playwright/docs/src/api/params.md; d done echo "(blocks with NO '* langs:' line default to all-languages incl. go — not listed)" -hr "2. Sibling roll PRs + the test files they added" +hr "2. Sibling roll PRs + the tests they added (must all have Go counterparts)" +echo "EVERY test function below must have a Go counterpart in ./tests/ (Step 6b)." +echo "Port the UNION across the three clients; they overlap but each adds unique cases." for repo in microsoft/playwright-python microsoft/playwright-java microsoft/playwright-dotnet; do pr="$(find_roll_pr "$repo")" num="$(echo "$pr" | cut -f1)" title="$(echo "$pr" | cut -f2-)" printf '\n# %s -> PR #%s %s\n' "$repo" "${num:-?}" "$title" - if [ -n "$num" ]; then - gh pr view "$num" --repo "$repo" --json files \ - --jq '.files[].path | select(test("(?i)test|spec"))' 2>/dev/null | sed 's/^/ /' || true - else + if [ -z "$num" ]; then echo " (no roll PR found — search manually, see references/finding-changes.md)" + continue fi + echo " test files changed:" + gh pr view "$num" --repo "$repo" --json files \ + --jq '.files[].path | select(test("(?i)test|spec"))' 2>/dev/null | sed 's/^/ /' || true + echo " test functions added:" + case "$repo" in + *python) gh pr diff "$num" --repo "$repo" 2>/dev/null | grep -E '^\+\s*(async )?def test_' | sed 's/^+/ /' ;; + *java) gh pr diff "$num" --repo "$repo" 2>/dev/null | grep -E '^\+\s*(public )?void [a-z]' | sed 's/^+/ /' ;; + *dotnet) gh pr diff "$num" --repo "$repo" 2>/dev/null | grep -iE '^\+\s*public async Task|^\+\s*\[(Playwright)?Test' | sed 's/^+/ /' ;; + esac | sort -u | head -40 done echo "" -echo ">> Confirm every NEW test file above has a counterpart under ./tests/ in this repo." +echo ">> Tick off each test function above against ./tests/. A sibling test with no Go" +echo " counterpart is either an unported case OR a Go feature gap it would expose —" +echo " port it (and fix the impl if it fails). Only skip with a logged reason." hr "3. New Go API symbols this roll (for eyeball diff vs siblings above)" echo "Interface methods / structs added to generated files vs committed HEAD:" From 0c0c81c30de3e14d5083ffdd4e23995626fc8732 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 26 Jun 2026 10:49:57 -0700 Subject: [PATCH 6/7] chore(skill): add CI-watching + rebase guidance (Step 11) Capture the parts of a roll that happen after the PR opens: read gh pr checks, expect cross-browser/cross-OS failures that don't reproduce locally, Flakiness.io is a reporter not a gate; and how to rebase the run.go/README.md conflicts that a roll always produces when main moves. Minor: align Step 9/refs wording with the function-name listing verify-parity.sh now emits. --- .claude/skills/roll-playwright/SKILL.md | 35 ++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/.claude/skills/roll-playwright/SKILL.md b/.claude/skills/roll-playwright/SKILL.md index 768143ca..894c0190 100644 --- a/.claude/skills/roll-playwright/SKILL.md +++ b/.claude/skills/roll-playwright/SKILL.md @@ -5,7 +5,7 @@ description: Roll playwright-go to a new upstream Playwright version. Bumps the # Roll playwright-go to a new Playwright version -Roll the client from the currently-pinned version to a target version (e.g. `1.61.0`). Work autonomously; only stop for the decision points called out in **When to ask the user**. +Roll the client from the currently-pinned version to a target version (e.g. `1.61.0`). Work autonomously; only stop for the decision points called out in **When to ask the user**. The job isn't done when the PR opens — it's done when CI is green (Step 11), so expect to iterate on cross-browser/cross-OS failures that don't reproduce locally. ## What a roll actually is @@ -168,7 +168,7 @@ bash .claude/skills/roll-playwright/verify-parity.sh It reports three buckets: 1. **New upstream APIs not in Go** — each `* since: vNEW` block whose `* langs:` still lacks `go`. For each, decide: intentionally omitted (matches a sibling skipping it) or a miss to fix in `patches/main.patch`. -2. **Sibling test files added this roll** — so you can confirm you ported each one into `tests/` (Step 6b). +2. **Sibling tests added this roll** — the test files AND the individual test function names from all three clients, so you can tick off that each has a Go counterpart in `tests/` (Step 6b). 3. **Go API symbols added vs sibling API surface** — a rough name-level diff to spot a method you exposed that no sibling did (often a naming mistake) or vice-versa. Treat each flagged item as needing an explicit decision: fix it, or note why Go intentionally differs. The goal is that any remaining difference from python/java/dotnet is deliberate and explainable. @@ -189,6 +189,35 @@ git add -A && git commit -m "chore: roll to Playwright v" Open a PR only if the user asks. Summarize new APIs added in the PR body. +## Step 11 — Watch CI and keep the branch current + +A roll PR runs a large matrix (3 OS × 3 browsers × 2 Go versions) plus Lint, verify (type generation), and test-examples. Local chromium-green is **not** sufficient — most roll failures show up only on firefox/webkit or on Windows. + +```bash +gh pr checks --repo playwright-community/playwright-go # all statuses +# For a failing job, read the failed step (not the whole log): +gh run view --repo playwright-community/playwright-go --job --log-failed | grep -iE 'FAIL|Error:|\.go:[0-9]+' +``` + +- **Flakiness.io is a reporter, not a merge gate** — it shows "fail" only because a test job it reports on failed; it goes green when the tests do. Don't chase it directly. +- Triage a failing test the same way as Step 6/“behavior changes”: is it my code, an engine-specific string, or an intended upstream change? Fix and re-push (amend the relevant commit to keep history clean; `--force-with-lease`). + +### Rebasing when `main` moves + +A roll commit always touches `run.go` (version) and `README.md` (browser-version badges), so if `main` advances during review you'll get conflicts there — resolve by **keeping main's structure and your roll's values**: +- `run.go`: if main turned the version into a `const (...)` block (e.g. added `nodeVersion`/registry constants), keep that block and just set `playwrightCliVersion` to the new version. +- `README.md`: keep main's badge text/links, keep your bumped version numbers inside the `` markers. + +```bash +git fetch origin main && git rebase origin/main +# resolve run.go / README.md as above, then: +git add run.go README.md && git rebase --continue +# re-verify generation is idempotent + submodule still pinned, then force-push: +go generate ./... && (cd playwright && git checkout v) +git diff --ignore-submodules generated-*.go # must be empty +git push --force-with-lease origin roll/v +``` + ## Existing tests may need updating for behavior changes A roll can change the behavior of an *existing* API, breaking a test that asserts the old behavior — this is not a port bug. When a pre-existing test fails: @@ -206,7 +235,7 @@ A roll can change the behavior of an *existing* API, breaking a test that assert ## Files & references - `find-changes.sh` — discovery helper (read-only): upstream docs diff, new `* since:` APIs, sibling roll PRs. -- `verify-parity.sh` — parity verifier (read-only, Step 9): new APIs missing `go`, sibling test files to port, Go-symbol diff. +- `verify-parity.sh` — parity verifier (read-only, Step 9): new APIs missing `go`, sibling test functions to port, Go-symbol diff. - `references/finding-changes.md` — exact `gh` queries + roll-PR conventions for python/java/dotnet + upstream compare. - `references/patch-editing.md` — anatomy of `patches/main.patch`, `* langs:` gating, editing the generator (`methodNoErrArray`, return-type aliases), conflict resolution. From c470ece11c218a28c3dc93b0f2179e39faf8e8cb Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 26 Jun 2026 10:51:33 -0700 Subject: [PATCH 7/7] chore(skill): make PR creation + drive-CI-to-green an autonomous closing loop Step 10 now creates the PR (gh pr create) once local checks pass, without waiting to be asked. Step 11 is an explicit loop: gh run watch --exit-status -> read each failure's --log-failed -> triage (my code / engine-OS-specific / behavior change) -> fix -> re-push, repeat until gh pr checks shows 0 fail. Reconcile 'When to ask': work autonomously through PR + CI, but never merge. --- .claude/skills/roll-playwright/SKILL.md | 37 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/.claude/skills/roll-playwright/SKILL.md b/.claude/skills/roll-playwright/SKILL.md index 894c0190..e817f051 100644 --- a/.claude/skills/roll-playwright/SKILL.md +++ b/.claude/skills/roll-playwright/SKILL.md @@ -185,22 +185,38 @@ git diff --submodule=log -- playwright # confirm the pointer moves OLD -> NEW git checkout -b roll/v git add -A && git commit -m "chore: roll to Playwright v" +git push -u origin roll/v +gh pr create --repo playwright-community/playwright-go --base main \ + --title "chore: roll to Playwright v" --body "" ``` -Open a PR only if the user asks. Summarize new APIs added in the PR body. +Create the PR when the local checks (Step 7/9) pass — don't wait to be asked. Summarize the new APIs and any behavior changes in the body. Then drive it to green (Step 11). -## Step 11 — Watch CI and keep the branch current +## Step 11 — Drive CI to green (loop until all checks pass) -A roll PR runs a large matrix (3 OS × 3 browsers × 2 Go versions) plus Lint, verify (type generation), and test-examples. Local chromium-green is **not** sufficient — most roll failures show up only on firefox/webkit or on Windows. +The roll is **not done when the PR opens — it's done when CI is green.** A roll PR runs a large matrix (3 OS × 3 browsers × 2 Go versions) plus Lint, verify (type generation), and test-examples. Local chromium-green is **not** sufficient — most roll failures surface only on firefox/webkit or on Windows. Loop autonomously until every check passes: -```bash -gh pr checks --repo playwright-community/playwright-go # all statuses -# For a failing job, read the failed step (not the whole log): -gh run view --repo playwright-community/playwright-go --job --log-failed | grep -iE 'FAIL|Error:|\.go:[0-9]+' -``` +1. **Wait for the run to finish** (don't poll in a tight loop — block on it): + ```bash + # Find the run for the branch's head commit, then watch it to completion: + RUN=$(gh run list --repo playwright-community/playwright-go --branch roll/v \ + --limit 1 --json databaseId --jq '.[0].databaseId') + gh run watch "$RUN" --repo playwright-community/playwright-go --exit-status + # (exits non-zero if the run failed; `gh pr checks --watch` is an alternative.) + ``` +2. **List results and read every failure's log** (the failed step, not the whole log): + ```bash + gh pr checks --repo playwright-community/playwright-go + gh run view --repo playwright-community/playwright-go --job --log-failed \ + | grep -iE 'FAIL|Error:|\.go:[0-9]+|panic' + ``` +3. **Triage each failure** the same way as Step 6 / "behavior changes": is it my code, an engine/OS-specific string (match any known variant), a lint issue, or an intended upstream change? Note the OS+browser — a failure on only webkit-windows is still a failure. +4. **Fix, re-verify locally on the affected browser(s), and re-push** (amend the relevant commit to keep history clean; `--force-with-lease`). Re-run from step 1. +5. Repeat until `gh pr checks` shows **0 fail** and no relevant pending. - **Flakiness.io is a reporter, not a merge gate** — it shows "fail" only because a test job it reports on failed; it goes green when the tests do. Don't chase it directly. -- Triage a failing test the same way as Step 6/“behavior changes”: is it my code, an engine-specific string, or an intended upstream change? Fix and re-push (amend the relevant commit to keep history clean; `--force-with-lease`). +- A genuinely flaky (not roll-caused) test that fails once may pass on re-run; confirm it's unrelated to your changes before re-running rather than fixing. The repo uses `flakiness-go` (no auto-retry). +- Each push triggers a fresh run; `gh run list --branch` returns the newest first. Watch the run for the *current* head commit, not a stale one. ### Rebasing when `main` moves @@ -230,7 +246,8 @@ A roll can change the behavior of an *existing* API, breaking a test that assert - Target version is ambiguous or unreleased upstream. - A new upstream API is non-trivial (new class, breaking signature change) and you're unsure whether Go should expose it or how to name it. - A pre-existing test fails and after investigation you're genuinely unsure whether it's an intended upstream behavior change (update the test) or a port bug (fix the code). If the evidence is conclusive (e.g. an upstream source rewrite + the Go client doesn't implement that logic), proceed and flag it; otherwise ask. -- Before pushing or opening a PR. + +Otherwise work the whole flow autonomously, **including creating the PR (Step 10) and iterating on pushes until CI is green (Step 11)** — those don't need approval. Do NOT merge the PR; leave that to a maintainer. (If the user explicitly said not to open a PR, stop after Step 9 and report instead.) ## Files & references