chore: roll to Playwright v1.57.0 (#578)
Some checks failed
Go / Lint (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (chromium, oldstable, macos-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (chromium, oldstable, ubuntu-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (chromium, oldstable, windows-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (chromium, stable, macos-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (chromium, stable, ubuntu-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (chromium, stable, windows-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (firefox, oldstable, macos-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (firefox, oldstable, ubuntu-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (firefox, oldstable, windows-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (firefox, stable, macos-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (firefox, stable, ubuntu-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (firefox, stable, windows-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (webkit, oldstable, macos-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (webkit, oldstable, ubuntu-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (webkit, oldstable, windows-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (webkit, stable, macos-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (webkit, stable, ubuntu-latest) (push) Has been cancelled
Go / ${{ matrix.browser }} on ${{ matrix.os }}, go ${{ matrix.go }} (webkit, stable, windows-latest) (push) Has been cancelled
Go / test-examples (push) Has been cancelled
Docs / Deploy docs (push) Has been cancelled
Verify Types / verify (push) Has been cancelled
Go / finish (push) Has been cancelled

* chore: roll to Playwright v1.57.0

Fix playwright submodule commit

Rerun gofumpt

Change ConsoleMessages to use newConsoleMessage() to properly deserialize event objects

Add test coverage

Update page_test.go

Fix failing end-to-end test

Update README.md

Fix README

Update README.md

* Fix unit tests
This commit is contained in:
Remington Arneson 2026-01-16 17:06:27 -05:00 committed by GitHub
parent 56e30d60f8
commit fcd06e1fa6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1252 additions and 371 deletions

View File

@ -20,20 +20,20 @@ BROWSER=chromium HEADLESS=1 go test -v --race ./...
### Roll
1. Find out to which upstream version you want to roll, and change the value of `playwrightCliVersion` in the **run.go** to the new version.
1. Download current version of Playwright driver `go run scripts/install-browsers/main.go`
1. Apply patch `bash scripts/apply-patch.sh`
1. Fix merge conflicts if any, otherwise ignore this step. Once you are happy you can commit the changes `cd playwright; git commit -am "apply patch" && cd ..`
1. Regenerate a new patch `bash scripts/update-patch.sh`
1. Generate go code `go generate ./...`
2. Download current version of Playwright driver `go run scripts/install-browsers/main.go`
3. Apply patch `bash scripts/apply-patch.sh`
4. Fix merge conflicts if any, otherwise ignore this step. Once you are happy you can commit the changes `cd playwright; git commit -am "apply patch" && cd ..`
5. Regenerate a new patch `bash scripts/update-patch.sh`
6. Generate go code `go generate ./...`
To adapt to the new version of Playwright's protocol and feature updates, you may need to modify the patch. Refer to the following steps:
1. Apply patch `bash scripts/apply-patch.sh`
1. `cd playwright`
1. Revert the patch`git reset HEAD~1`
1. Modify the files under `docs/src/api`, etc. as needed. Available references:
2. `cd playwright`
3. Revert the patch`git reset HEAD~1`
4. Modify the files under `docs/src/api`, etc. as needed. Available references:
- Protocol `packages/protocol/src/protocol.yml`
- [Playwright python](https://github.com/microsoft/playwright-python)
1. Commit the changes `git commit -am "apply patch"`
1. Regenerate a new patch `bash scripts/update-patch.sh`
1. Generate go code `go generate ./...`.
5. Commit the changes `git commit -am "apply patch"`
6. Regenerate a new patch `bash scripts/update-patch.sh`
7. Generate go code `go generate ./...`.

View File

@ -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)
[![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://aka.ms/playwright-slack) [![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) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-136.0.7103.25-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-137.0-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.4-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop -->
[![Join Slack](https://img.shields.io/badge/join-slack-infomational)](https://aka.ms/playwright-slack) [![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) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-143.0.7499.4-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-144.0.2-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-26.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop -->
[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 <!-- GEN:chromium-version -->136.0.7103.25<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->18.4<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->137.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Chromium <!-- GEN:chromium-version -->143.0.7499.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->144.0.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all the browsers on all platforms.

View File

@ -93,6 +93,9 @@ func (b *browserImpl) NewContext(options ...BrowserNewContextOptions) (BrowserCo
context := fromChannel(channel).(*browserContextImpl)
context.browser = b
b.browserType.(*browserTypeImpl).didCreateContext(context, &option, nil)
if err := context.initializeHarFromOptions(); err != nil {
return nil, err
}
return context, nil
}

View File

@ -9,6 +9,7 @@ import (
"slices"
"strings"
"sync"
"sync/atomic"
"github.com/playwright-community/playwright-go/internal/safe"
)
@ -16,7 +17,7 @@ import (
type browserContextImpl struct {
channelOwner
timeoutSettings *timeoutSettings
closeWasCalled bool
closeWasCalled atomic.Bool
options *BrowserNewContextOptions
pages []Page
routes []*routeHandlerEntry
@ -91,7 +92,7 @@ func (b *browserContextImpl) NewCDPSession(page interface{}) (CDPSession, error)
return nil, err
}
cdpSession := fromChannel(channel).(*cdpSessionImpl)
cdpSession := fromChannelWithConnection(channel, b.connection).(*cdpSessionImpl)
return cdpSession, nil
}
@ -104,7 +105,7 @@ func (b *browserContextImpl) NewPage() (Page, error) {
if err != nil {
return nil, err
}
return fromChannel(channel).(*pageImpl), nil
return fromChannelWithConnection(channel, b.connection).(*pageImpl), nil
}
func (b *browserContextImpl) Cookies(urls ...string) ([]Cookie, error) {
@ -411,13 +412,13 @@ func (b *browserContextImpl) ExpectPage(cb func() error, options ...BrowserConte
}
func (b *browserContextImpl) Close(options ...BrowserContextCloseOptions) error {
if b.closeWasCalled {
if b.closeWasCalled.Load() {
return nil
}
if len(options) == 1 {
b.closeReason = options[0].Reason
}
b.closeWasCalled = true
b.closeWasCalled.Store(true)
_, err := b.channel.connection.WrapAPICall(func() (interface{}, error) {
return nil, b.request.Dispose(APIRequestContextDisposeOptions{
@ -438,7 +439,7 @@ func (b *browserContextImpl) Close(options ...BrowserContextCloseOptions) error
if err != nil {
return nil, err
}
artifact := fromChannel(response).(*artifactImpl)
artifact := fromChannelWithConnection(response, b.connection).(*artifactImpl)
// Server side will compress artifact if content is attach or if file is .zip.
needCompressed := strings.HasSuffix(strings.ToLower(harMetaData.Path), ".zip")
if !needCompressed && harMetaData.Content == HarContentPolicyAttach {
@ -597,7 +598,7 @@ func (b *browserContextImpl) onRoute(route *routeImpl) {
url := route.Request().URL()
for _, handlerEntry := range routes {
// If the page or the context was closed we stall all requests right away.
if (page != nil && page.closeWasCalled) || b.closeWasCalled {
if (page != nil && page.closeWasCalled.Load()) || b.closeWasCalled.Load() {
return
}
if !handlerEntry.Matches(url) {
@ -644,7 +645,7 @@ func (b *browserContextImpl) pause() <-chan error {
func (b *browserContextImpl) onBackgroundPage(ev map[string]interface{}) {
b.Lock()
p := fromChannel(ev["page"]).(*pageImpl)
p := fromChannelWithConnection(ev["page"], b.connection).(*pageImpl)
p.browserContext = b
b.backgroundPages = append(b.backgroundPages, p)
b.Unlock()
@ -662,17 +663,41 @@ func (b *browserContextImpl) setOptions(options *BrowserNewContextOptions, trace
options = &BrowserNewContextOptions{}
}
b.options = options
if b.options != nil && b.options.RecordHarPath != nil {
b.harRecorders[""] = harRecordingMetadata{
Path: *b.options.RecordHarPath,
Content: b.options.RecordHarContent,
}
}
if tracesDir != nil {
b.tracing.tracesDir = *tracesDir
}
}
// initializeHarFromOptions starts HAR recording if RecordHarPath is set in options.
// This must be called after context creation to properly register the HAR recorder on the server.
func (b *browserContextImpl) initializeHarFromOptions() error {
if b.options == nil || b.options.RecordHarPath == nil {
return nil
}
path := *b.options.RecordHarPath
// Determine default content policy based on file extension
var content *HarContentPolicy
if strings.HasSuffix(strings.ToLower(path), ".zip") {
content = HarContentPolicyAttach
} else {
content = HarContentPolicyEmbed
}
if b.options.RecordHarContent != nil {
content = b.options.RecordHarContent
} else if b.options.RecordHarOmitContent != nil && *b.options.RecordHarOmitContent {
content = HarContentPolicyOmit
}
mode := HarModeFull
if b.options.RecordHarMode != nil {
mode = b.options.RecordHarMode
}
return b.recordIntoHar(path, browserContextRecordIntoHarOptions{
URL: b.options.RecordHarURLFilter,
UpdateContent: content,
UpdateMode: mode,
})
}
func (b *browserContextImpl) BackgroundPages() []Page {
b.Lock()
defer b.Unlock()
@ -784,33 +809,49 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
}
bt.createChannelOwner(bt, parent, objectType, guid, initializer)
if parent.objectType == "Browser" {
bt.browser = fromChannel(parent.channel).(*browserImpl)
bt.browser = fromChannelWithConnection(parent.channel, bt.connection).(*browserImpl)
bt.browser.contexts = append(bt.browser.contexts, bt)
}
bt.tracing = fromChannel(initializer["tracing"]).(*tracingImpl)
bt.request = fromChannel(initializer["requestContext"]).(*apiRequestContextImpl)
bt.tracing = fromChannelWithConnection(initializer["tracing"], bt.connection).(*tracingImpl)
bt.request = fromChannelWithConnection(initializer["requestContext"], bt.connection).(*apiRequestContextImpl)
bt.clock = newClock(bt)
// Register this context with the selectors manager for custom selector engines
if bt.browser != nil && bt.browser.browserType != nil {
if browserType, ok := bt.browser.browserType.(*browserTypeImpl); ok && browserType.playwright != nil {
browserType.playwright.Selectors.(*selectorsImpl).addContext(bt)
}
}
bt.channel.On("bindingCall", func(params map[string]interface{}) {
bt.onBinding(fromChannel(params["binding"]).(*bindingCallImpl))
bt.onBinding(fromChannelWithConnection(params["binding"], bt.connection).(*bindingCallImpl))
})
bt.channel.On("close", bt.onClose)
bt.channel.On("close", func() {
// Unregister this context from the selectors manager
if bt.browser != nil && bt.browser.browserType != nil {
if browserType, ok := bt.browser.browserType.(*browserTypeImpl); ok && browserType.playwright != nil {
browserType.playwright.Selectors.(*selectorsImpl).removeContext(bt)
}
}
bt.onClose()
})
bt.channel.On("page", func(payload map[string]interface{}) {
bt.onPage(fromChannel(payload["page"]).(*pageImpl))
bt.onPage(fromChannelWithConnection(payload["page"], bt.connection).(*pageImpl))
})
bt.channel.On("route", func(params map[string]interface{}) {
bt.channel.CreateTask(func() {
bt.onRoute(fromChannel(params["route"]).(*routeImpl))
bt.onRoute(fromChannelWithConnection(params["route"], bt.connection).(*routeImpl))
})
})
bt.channel.On("webSocketRoute", func(params map[string]interface{}) {
bt.channel.CreateTask(func() {
bt.onWebSocketRoute(fromChannel(params["webSocketRoute"]).(*webSocketRouteImpl))
bt.onWebSocketRoute(fromChannelWithConnection(params["webSocketRoute"], bt.connection).(*webSocketRouteImpl))
})
})
bt.channel.On("backgroundPage", bt.onBackgroundPage)
bt.channel.On("serviceWorker", func(params map[string]interface{}) {
bt.onServiceWorker(fromChannel(params["worker"]).(*workerImpl))
bt.onServiceWorker(fromChannelWithConnection(params["worker"], bt.connection).(*workerImpl))
})
bt.channel.On("console", func(ev map[string]interface{}) {
message := newConsoleMessage(ev)
@ -820,7 +861,7 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
}
})
bt.channel.On("dialog", func(params map[string]interface{}) {
dialog := fromChannel(params["dialog"]).(*dialogImpl)
dialog := fromChannelWithConnection(params["dialog"], bt.connection).(*dialogImpl)
go func() {
hasListeners := bt.Emit("dialog", dialog)
page := dialog.page
@ -857,7 +898,7 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
},
)
bt.channel.On("request", func(ev map[string]interface{}) {
request := fromChannel(ev["request"]).(*requestImpl)
request := fromChannelWithConnection(ev["request"], bt.connection).(*requestImpl)
page := fromNullableChannel(ev["page"])
bt.Emit("request", request)
if page != nil {
@ -865,7 +906,7 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
}
})
bt.channel.On("requestFailed", func(ev map[string]interface{}) {
request := fromChannel(ev["request"]).(*requestImpl)
request := fromChannelWithConnection(ev["request"], bt.connection).(*requestImpl)
failureText := ev["failureText"]
if failureText != nil {
request.failureText = failureText.(string)
@ -879,7 +920,7 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
})
bt.channel.On("requestFinished", func(ev map[string]interface{}) {
request := fromChannel(ev["request"]).(*requestImpl)
request := fromChannelWithConnection(ev["request"], bt.connection).(*requestImpl)
response := fromNullableChannel(ev["response"])
page := fromNullableChannel(ev["page"])
request.setResponseEndTiming(ev["responseEndTiming"].(float64))
@ -892,7 +933,7 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
}
})
bt.channel.On("response", func(ev map[string]interface{}) {
response := fromChannel(ev["response"]).(*responseImpl)
response := fromChannelWithConnection(ev["response"], bt.connection).(*responseImpl)
page := fromNullableChannel(ev["page"])
bt.Emit("response", response)
if page != nil {

View File

@ -19,6 +19,10 @@ func (b *browserTypeImpl) ExecutablePath() string {
func (b *browserTypeImpl) Launch(options ...BrowserTypeLaunchOptions) (Browser, error) {
overrides := map[string]interface{}{}
// timeout is required in Playwright v1.57+ protocol
if len(options) == 0 || options[0].Timeout == nil {
overrides["timeout"] = float64(30000) // default 30s
}
if len(options) == 1 && options[0].Env != nil {
overrides["env"] = serializeMapToNameAndValue(options[0].Env)
options[0].Env = nil
@ -36,6 +40,10 @@ func (b *browserTypeImpl) LaunchPersistentContext(userDataDir string, options ..
overrides := map[string]interface{}{
"userDataDir": userDataDir,
}
// timeout is required in Playwright v1.57+ protocol
if len(options) == 0 || options[0].Timeout == nil {
overrides["timeout"] = float64(30000) // default 30s
}
option := &BrowserNewContextOptions{}
var tracesDir *string = nil
if len(options) == 1 {
@ -87,12 +95,15 @@ func (b *browserTypeImpl) LaunchPersistentContext(userDataDir string, options ..
options[0].RecordHarOmitContent = nil
}
}
channel, err := b.channel.Send("launchPersistentContext", options, overrides)
response, err := b.channel.SendReturnAsDict("launchPersistentContext", options, overrides)
if err != nil {
return nil, err
}
context := fromChannel(channel).(*browserContextImpl)
context := fromChannel(response["context"]).(*browserContextImpl)
b.didCreateContext(context, option, tracesDir)
if err := context.initializeHarFromOptions(); err != nil {
return nil, err
}
return context, nil
}
@ -100,9 +111,14 @@ func (b *browserTypeImpl) Connect(wsEndpoint string, options ...BrowserTypeConne
overrides := map[string]interface{}{
"wsEndpoint": wsEndpoint,
"headers": map[string]string{
"x-playwright-browser": b.Name(),
"x-playwright-browser": b.Name(),
"x-playwright-launch-options": "{}",
},
}
// timeout is required in Playwright v1.57+ protocol
if len(options) == 0 || options[0].Timeout == nil {
overrides["timeout"] = float64(0) // default no timeout
}
if len(options) == 1 {
if options[0].Headers != nil {
for k, v := range options[0].Headers {
@ -147,6 +163,10 @@ func (b *browserTypeImpl) ConnectOverCDP(endpointURL string, options ...BrowserT
overrides := map[string]interface{}{
"endpointURL": endpointURL,
}
// timeout is required in Playwright v1.57+ protocol
if len(options) == 0 || options[0].Timeout == nil {
overrides["timeout"] = float64(30000) // default 30s
}
if len(options) == 1 {
if options[0].Headers != nil {
overrides["headers"] = serializeMapToNameAndValue(options[0].Headers)

View File

@ -38,15 +38,31 @@ func (c *channel) CreateTask(fn func()) {
func (c *channel) Send(method string, options ...interface{}) (interface{}, error) {
return c.connection.WrapAPICall(func() (interface{}, error) {
return c.innerSend(method, options...).GetResultValue()
result, err := c.innerSend(method, options...).GetResultValue()
if err != nil {
return nil, err
}
// GUIDs are now always eagerly resolved in connection.Dispatch
return result, nil
}, c.owner.isInternalType)
}
func (c *channel) SendReturnAsDict(method string, options ...interface{}) (map[string]interface{}, error) {
ret, err := c.connection.WrapAPICall(func() (interface{}, error) {
return c.innerSend(method, options...).GetResult()
result, err := c.innerSend(method, options...).GetResult()
if err != nil {
return nil, err
}
// GUIDs are now always eagerly resolved in connection.Dispatch
return result, nil
}, c.owner.isInternalType)
return ret.(map[string]interface{}), err
if err != nil {
return nil, err
}
if ret == nil {
return make(map[string]interface{}), nil
}
return ret.(map[string]interface{}), nil
}
func (c *channel) innerSend(method string, options ...interface{}) *protocolCallback {

View File

@ -111,7 +111,9 @@ func (r *rootChannelOwner) initialize() (*Playwright, error) {
if err != nil {
return nil, err
}
return fromChannel(ret["playwright"]).(*Playwright), nil
// GUIDs are now always eagerly resolved in connection.Dispatch
playwrightValue := ret["playwright"]
return fromChannel(playwrightValue).(*Playwright), nil
}
func newRootChannelOwner(connection *connection) *rootChannelOwner {

View File

@ -100,15 +100,27 @@ func (c *connection) Dispatch(msg *message) {
if msg.Error != nil {
cb.SetError(parseError(msg.Error.Error))
} else {
cb.SetResult(c.replaceGuidsWithChannels(msg.Result).(map[string]interface{}))
// Always resolve GUIDs in responses, regardless of connection type
// The protocol guarantees that __create__ events arrive before responses that reference those objects
result, err := c.replaceGuidsWithChannels(msg.Result)
if err != nil {
cb.SetError(fmt.Errorf("failed to resolve response objects: %w", err))
} else {
cb.SetResult(result.(map[string]interface{}))
}
}
return
}
object, _ := c.objects.Load(msg.GUID)
if method == "__create__" {
c.createRemoteObject(
_, err := c.createRemoteObject(
object, msg.Params["type"].(string), msg.Params["guid"].(string), msg.Params["initializer"],
)
if err != nil {
// Critical: object creation failure indicates corrupted protocol state
// Close connection to prevent cascade failures
c.cleanup(err)
}
return
}
if object == nil {
@ -134,7 +146,15 @@ func (c *connection) Dispatch(msg *message) {
if object.objectType == "JsonPipe" {
object.channel.Emit(method, msg.Params)
} else {
object.channel.Emit(method, c.replaceGuidsWithChannels(msg.Params))
// Always resolve GUIDs in events, regardless of connection type
// The protocol guarantees that __create__ events arrive before events that reference those objects
params, err := c.replaceGuidsWithChannels(msg.Params)
if err != nil {
// Event parameters contain invalid references - connection is corrupted
c.cleanup(fmt.Errorf("failed to resolve event parameters for %s: %w", method, err))
return
}
object.channel.Emit(method, params)
}
}
@ -142,10 +162,13 @@ func (c *connection) LocalUtils() *localUtilsImpl {
return c.localUtils
}
func (c *connection) createRemoteObject(parent *channelOwner, objectType string, guid string, initializer interface{}) interface{} {
initializer = c.replaceGuidsWithChannels(initializer)
result := createObjectFactory(parent, objectType, guid, initializer.(map[string]interface{}))
return result
func (c *connection) createRemoteObject(parent *channelOwner, objectType string, guid string, initializer interface{}) (interface{}, error) {
resolved, err := c.replaceGuidsWithChannels(initializer)
if err != nil {
return nil, fmt.Errorf("failed to resolve initializer for %s (guid=%s): %w", objectType, guid, err)
}
result := createObjectFactory(parent, objectType, guid, resolved.(map[string]interface{}))
return result, nil
}
func (c *connection) WrapAPICall(cb func() (interface{}, error), isInternal bool) (interface{}, error) {
@ -156,31 +179,48 @@ func (c *connection) WrapAPICall(cb func() (interface{}, error), isInternal bool
return cb()
}
func (c *connection) replaceGuidsWithChannels(payload interface{}) interface{} {
func (c *connection) replaceGuidsWithChannels(payload interface{}) (interface{}, error) {
if payload == nil {
return nil
return nil, nil
}
v := reflect.ValueOf(payload)
if v.Kind() == reflect.Slice {
listV := payload.([]interface{})
for i := 0; i < len(listV); i++ {
listV[i] = c.replaceGuidsWithChannels(listV[i])
resolved, err := c.replaceGuidsWithChannels(listV[i])
if err != nil {
return nil, fmt.Errorf("failed to resolve slice element at index %d: %w", i, err)
}
listV[i] = resolved
}
return listV
return listV, nil
}
if v.Kind() == reflect.Map {
mapV := payload.(map[string]interface{})
// Check if this map represents an object reference (has "guid" field)
if guid, hasGUID := mapV["guid"]; hasGUID {
if channelOwner, ok := c.objects.Load(guid.(string)); ok {
return channelOwner.channel
guidStr, ok := guid.(string)
if !ok {
return nil, fmt.Errorf("guid field is not a string: %T", guid)
}
// Try to load the object from connection's objects map
if channelOwner, ok := c.objects.Load(guidStr); ok {
return channelOwner.channel, nil
}
// Object not found - this indicates a protocol error or message ordering issue
return nil, fmt.Errorf("object with guid %s was not bound in the connection", guidStr)
}
// Recursively process all values in the map
for key := range mapV {
mapV[key] = c.replaceGuidsWithChannels(mapV[key])
resolved, err := c.replaceGuidsWithChannels(mapV[key])
if err != nil {
return nil, fmt.Errorf("failed to resolve map key '%s': %w", key, err)
}
mapV[key] = resolved
}
return mapV
return mapV, nil
}
return payload
return payload, nil
}
func (c *connection) sendMessageToServer(object *channelOwner, method string, params interface{}, noReply bool) (cb *protocolCallback) {
@ -314,7 +354,20 @@ func newConnection(transport transport, localUtils ...*localUtilsImpl) *connecti
}
func fromChannel(v interface{}) interface{} {
return v.(*channel).object
if ch, ok := v.(*channel); ok {
return ch.object
}
panic(fmt.Sprintf("fromChannel: expected *channel, got %T", v))
}
// fromChannelWithConnection resolves a value to a channel object
// With the protocol fix, GUIDs are always eagerly resolved, so this behaves identically to fromChannel
// The conn parameter is kept for API compatibility but is no longer used
func fromChannelWithConnection(v interface{}, conn *connection) interface{} {
if ch, ok := v.(*channel); ok {
return ch.object
}
panic(fmt.Sprintf("fromChannelWithConnection: expected *channel, got %T: %+v", v, v))
}
func fromNullableChannel(v interface{}) interface{} {
@ -337,7 +390,9 @@ func (pc *protocolCallback) setResultOnce(result map[string]interface{}, err err
pc.once.Do(func() {
pc.value = result
pc.err = err
close(pc.done)
if pc.done != nil {
close(pc.done)
}
})
}

View File

@ -1,8 +1,9 @@
package playwright
type consoleMessageImpl struct {
event map[string]interface{}
page Page
event map[string]interface{}
page Page
worker Worker
}
func (c *consoleMessageImpl) Type() string {
@ -36,6 +37,10 @@ func (c *consoleMessageImpl) Page() Page {
return c.page
}
func (c *consoleMessageImpl) Worker() (Worker, error) {
return c.worker, nil
}
func newConsoleMessage(event map[string]interface{}) *consoleMessageImpl {
bt := &consoleMessageImpl{}
bt.event = event
@ -43,5 +48,9 @@ func newConsoleMessage(event map[string]interface{}) *consoleMessageImpl {
if page != nil {
bt.page = page.(*pageImpl)
}
worker := fromNullableChannel(event["worker"])
if worker != nil {
bt.worker = worker.(*workerImpl)
}
return bt
}

View File

@ -72,12 +72,20 @@ func main() {
// Filter for active entries. There should be 0, because we have completed the entry already
assertErrorToNilf("could not click: %v", page.Locator("text=Active").Click())
// Wait is necessary because headless is too fast
assertErrorToNilf("could not wait for selector state: %v", page.Locator("ul.todo-list > li").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateDetached,
}))
assertCountOfTodos(0)
// If we filter now for completed entries, there should be 1
assertErrorToNilf("could not click: %v", page.GetByRole("link", playwright.PageGetByRoleOptions{
Name: "Completed",
}).Click())
// Wait is necessary because headless is too fast
assertErrorToNilf("could not wait for selector state: %v", page.Locator("ul.todo-list > li").WaitFor(playwright.LocatorWaitForOptions{
State: playwright.WaitForSelectorStateVisible,
}))
assertCountOfTodos(1)
// Clear the list of completed entries, then it should be again 0

View File

@ -41,13 +41,20 @@ func (r *apiRequestImpl) NewContext(options ...APIRequestNewContextOptions) (API
options[0].StorageState = storageState
options[0].StorageStatePath = nil
}
if options[0].Timeout != nil {
overrides["timeout"] = options[0].Timeout
}
}
channel, err := r.channel.Send("newRequest", options, overrides)
if err != nil {
return nil, err
}
return fromChannel(channel).(*apiRequestContextImpl), nil
ctx := fromChannel(channel).(*apiRequestContextImpl)
if len(options) == 1 && options[0].Timeout != nil {
ctx.defaultTimeout = options[0].Timeout
}
return ctx, nil
}
func newApiRequestImpl(pw *Playwright) *apiRequestImpl {
@ -56,8 +63,9 @@ func newApiRequestImpl(pw *Playwright) *apiRequestImpl {
type apiRequestContextImpl struct {
channelOwner
tracing *tracingImpl
closeReason *string
tracing *tracingImpl
closeReason *string
defaultTimeout *float64
}
func (r *apiRequestContextImpl) Dispose(options ...APIRequestContextDisposeOptions) error {
@ -207,6 +215,10 @@ func (r *apiRequestContextImpl) innerFetch(url string, request Request, options
overrides["params"] = serializeMapToNameValue(options[0].Params)
options[0].Params = nil
}
// Use context-level timeout as default if no per-request timeout specified
if options[0].Timeout == nil && r.defaultTimeout != nil {
overrides["timeout"] = *r.defaultTimeout
}
}
response, err := r.channel.Send("fetch", options, overrides)
@ -312,7 +324,9 @@ func (r *apiRequestContextImpl) StorageState(path ...string) (*StorageState, err
func newAPIRequestContext(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *apiRequestContextImpl {
rc := &apiRequestContextImpl{}
rc.createChannelOwner(rc, parent, objectType, guid, initializer)
rc.tracing = fromChannel(initializer["tracing"]).(*tracingImpl)
if tracingValue := initializer["tracing"]; tracingValue != nil {
rc.tracing = fromNullableChannel(tracingValue).(*tracingImpl)
}
return rc
}

View File

@ -60,9 +60,14 @@ func (f *frameImpl) Name() string {
}
func (f *frameImpl) SetContent(content string, options ...FrameSetContentOptions) error {
_, err := f.channel.Send("setContent", map[string]interface{}{
overrides := map[string]interface{}{
"html": content,
}, options)
}
// timeout is required in Playwright v1.57+ protocol
if len(options) == 0 || options[0].Timeout == nil {
overrides["timeout"] = f.page.timeoutSettings.NavigationTimeout()
}
_, err := f.channel.Send("setContent", overrides, options)
return err
}
@ -75,9 +80,14 @@ func (f *frameImpl) Content() (string, error) {
}
func (f *frameImpl) Goto(url string, options ...FrameGotoOptions) (Response, error) {
channel, err := f.channel.Send("goto", map[string]interface{}{
overrides := map[string]interface{}{
"url": url,
}, options)
}
// timeout is required in Playwright v1.57+ protocol
if len(options) == 0 || options[0].Timeout == nil {
overrides["timeout"] = f.page.timeoutSettings.NavigationTimeout()
}
channel, err := f.channel.Send("goto", overrides, options)
if err != nil {
return nil, fmt.Errorf("Frame.Goto %s: %w", url, err)
}
@ -414,7 +424,7 @@ func (f *frameImpl) DispatchEvent(selector, typ string, eventInit interface{}, o
"selector": selector,
"type": typ,
"eventInit": serializeArgument(eventInit),
})
}, options)
return err
}
@ -505,12 +515,18 @@ func (f *frameImpl) WaitForFunction(expression string, arg interface{}, options
if len(options) == 1 {
option = options[0]
}
result, err := f.channel.Send("waitForFunction", map[string]interface{}{
overrides := map[string]interface{}{
"expression": expression,
"arg": serializeArgument(arg),
"timeout": option.Timeout,
"polling": option.Polling,
})
}
// timeout is required in Playwright v1.57+ protocol
if option.Timeout == nil {
overrides["timeout"] = f.page.timeoutSettings.Timeout()
} else {
overrides["timeout"] = option.Timeout
}
result, err := f.channel.Send("waitForFunction", overrides)
if err != nil {
return nil, err
}

View File

@ -217,8 +217,9 @@ type Browser interface {
// Non-persistent browser contexts don't write any browsing data to disk.
type BrowserContext interface {
EventEmitter
// **NOTE** Only works with Chromium browser's persistent context.
// Emitted when new background page is created in the context.
// This event is not emitted.
//
// Deprecated: Background pages have been removed from Chromium together with Manifest V2 extensions.
OnBackgroundPage(fn func(Page))
// Playwright has ability to mock clock and passage of time.
@ -292,11 +293,13 @@ type BrowserContext interface {
// script: Script to be evaluated in all pages in the browser context.
AddInitScript(script Script) error
// **NOTE** Background pages are only supported on Chromium-based browsers.
// All existing background pages in the context.
// Returns an empty list.
//
// Deprecated: Background pages have been removed from Chromium together with Manifest V2 extensions.
BackgroundPages() []Page
// Returns the browser instance of the context. If it was launched as a persistent context null gets returned.
// Gets the browser instance that owns the context. Returns `null` if the context is created outside of normal
// browser, e.g. Android or Electron.
Browser() Browser
// Removes cookies from context. Accepts optional filter.
@ -351,6 +354,8 @@ type BrowserContext interface {
// - `'clipboard-write'`
// - `'geolocation'`
// - `'gyroscope'`
// - `'local-fonts'`
// - `'local-network-access'`
// - `'magnetometer'`
// - `'microphone'`
// - `'midi-sysex'` (system-exclusive midi)
@ -538,6 +543,12 @@ type BrowserType interface {
// **parent** directory of the "Profile Path" seen at `chrome://version`.
//
// Note that browsers do not allow launching multiple instances with the same User Data Directory.
//
// **NOTE** Chromium/Chrome: Due to recent Chrome policy changes, automating the default Chrome user profile is not
// supported. Pointing `userDataDir` to Chrome's main "User Data" directory (the profile used for your regular
// browsing) may result in pages not loading or the browser exiting. Create and use a separate directory (for example,
// an empty folder) as your automation profile instead. See https://developer.chrome.com/blog/remote-debugging-port
// for details.
LaunchPersistentContext(userDataDir string, options ...BrowserTypeLaunchPersistentContextOptions) (BrowserContext, error)
// Returns browser name. For example: `chromium`, `webkit` or `firefox`.
@ -651,6 +662,10 @@ type ConsoleMessage interface {
// `trace`, `clear`, `startGroup`, `startGroupCollapsed`, `endGroup`, `assert`, `profile`,
// `profileEnd`, `count`, `timeEnd`.
Type() string
// The web worker or service worker that produced this console message, if any. Note that console messages from web
// workers also have non-null [ConsoleMessage.Page].
Worker() (Worker, error)
}
// [Dialog] objects are dispatched by page via the [Page.OnDialog] event.
@ -2323,6 +2338,17 @@ type Locator interface {
// [actionability]: https://playwright.dev/docs/actionability
Dblclick(options ...LocatorDblclickOptions) error
// Describes the locator, description is used in the trace viewer and reports. Returns the locator pointing to the
// same element.
//
// description: Locator description.
Describe(description string) Locator
// Returns locator description previously set with [Locator.Describe]. Returns `null` if no custom description has
// been set. Prefer `Locator.toString()` for a human-readable representation, as it uses the description when
// available.
Description() (string, error)
// Programmatically dispatch an event on the matching element.
//
// # Details
@ -3029,7 +3055,13 @@ type LocatorAssertions interface {
}
// The Mouse class operates in main-frame CSS pixels relative to the top-left corner of the viewport.
// **NOTE** If you want to debug where the mouse moved, you can use the [Trace viewer] or
// [Playwright Inspector]. A red dot showing the location of the mouse will be shown for every
// mouse action.
// Every `page` object has its own Mouse, accessible with [Page.Mouse].
//
// [Trace viewer]: https://playwright.dev/docs/trace-viewer-intro
// [Playwright Inspector]: https://playwright.dev/docs/running-tests
type Mouse interface {
// Shortcut for [Mouse.Move], [Mouse.Down], [Mouse.Up].
//
@ -3669,6 +3701,9 @@ type Page interface {
Keyboard() Keyboard
// Returns up to (currently) 200 last console messages from this page. See [Page.OnConsole] for more details.
ConsoleMessages() ([]ConsoleMessage, error)
// The method returns an element locator that can be used to perform actions on this page / frame. Locator is resolved
// to the element immediately before performing an action, so a series of actions on the same locator can in fact be
// performed on different DOM elements. That would happen if the DOM structure between those actions has changed.
@ -3687,8 +3722,8 @@ type Page interface {
// Returns the opener for popup pages and `null` for others. If the opener has been closed already the returns `null`.
Opener() (Page, error)
// Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume'
// button in the page overlay or to call `playwright.resume()` in the DevTools console.
// Pauses script execution. Playwright will stop executing the script and wait for the user to either press the
// 'Resume' button in the page overlay or to call `playwright.resume()` in the DevTools console.
// User can inspect selectors or perform manual steps while paused. Resume will continue running the original script
// from the place it was paused.
// **NOTE** This method requires Playwright to be started in a headed mode, with a falsy “[object Object]” option.
@ -3751,6 +3786,14 @@ type Page interface {
// [locators]: https://playwright.dev/docs/locators
QuerySelectorAll(selector string) ([]ElementHandle, error)
// Returns up to (currently) 100 last network request from this page. See [Page.OnRequest] for more details.
// Returned requests should be accessed immediately, otherwise they might be collected to prevent unbounded memory
// growth as new requests come in. Once collected, retrieving most information about the request is impossible.
// Note that requests reported through the [Page.OnRequest] request are not collected, so there is a trade off between
// efficient memory usage with [Page.Requests] and the amount of available information reported through
// [Page.OnRequest].
Requests() ([]Request, error)
// When testing a web page, sometimes unexpected overlays like a "Sign up" dialog appear and block actions you want to
// automate, e.g. clicking a button. These overlays don't always show up in the same way or at the same time, making
// them tricky to handle in automated tests.
@ -4452,11 +4495,30 @@ type Touchscreen interface {
// API for collecting and saving Playwright traces. Playwright traces can be opened in
// [Trace Viewer] after Playwright script runs.
// **NOTE** You probably want to
// [enable tracing in your config file] instead
// of using `context.tracing`.
// The `context.tracing` API captures browser operations and network activity, but it doesn't record test assertions
// (like `expect` calls). We recommend
// [enabling tracing through Playwright Test configuration],
// which includes those assertions and provides a more complete trace for debugging test failures.
// Start recording a trace before performing actions. At the end, stop tracing and save it to a file.
//
// [Trace Viewer]: https://playwright.dev/docs/trace-viewer
// [enable tracing in your config file]: https://playwright.dev/docs/api/class-testoptions#test-options-trace
// [enabling tracing through Playwright Test configuration]: https://playwright.dev/docs/api/class-testoptions#test-options-trace
type Tracing interface {
// Start tracing.
// **NOTE** You probably want to
// [enable tracing in your config file] instead
// of using `Tracing.start`.
// The `context.tracing` API captures browser operations and network activity, but it doesn't record test assertions
// (like `expect` calls). We recommend
// [enabling tracing through Playwright Test configuration],
// which includes those assertions and provides a more complete trace for debugging test failures.
//
// [enable tracing in your config file]: https://playwright.dev/docs/api/class-testoptions#test-options-trace
// [enabling tracing through Playwright Test configuration]: https://playwright.dev/docs/api/class-testoptions#test-options-trace
Start(options ...TracingStartOptions) error
// Start a new trace chunk. If you'd like to record multiple traces on the same [BrowserContext], use [Tracing.Start]
@ -4547,7 +4609,7 @@ type WebSocket interface {
// [Page.RouteWebSocket] or [BrowserContext.RouteWebSocket], the `WebSocketRoute` object allows to handle the
// WebSocket, like an actual server would do.
// **Mocking**
// By default, the routed WebSocket will not connect to the server. This way, you can mock entire communcation over
// By default, the routed WebSocket will not connect to the server. This way, you can mock entire communication over
// the WebSocket. Here is an example that responds to a `"request"` with a `"response"`.
// Since we do not call [WebSocketRoute.ConnectToServer] inside the WebSocket route handler, Playwright assumes that
// WebSocket will be mocked, and opens the WebSocket inside the page automatically.
@ -4631,6 +4693,9 @@ type Worker interface {
// [WebWorker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
OnClose(fn func(Worker))
// Emitted when JavaScript within the worker calls one of console API methods, e.g. `console.log` or `console.dir`.
OnConsole(fn func(ConsoleMessage))
// Returns the return value of “[object Object]”.
// If the function passed to the [Worker.Evaluate] returns a [Promise], then [Worker.Evaluate] would wait for the
// promise to resolve and return its value.

View File

@ -20,6 +20,9 @@ type APIRequestNewContextOptions struct {
// a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
// `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
// with an exact match to the request origin that the certificate is valid for.
// Client certificate authentication is only active when at least one client certificate is provided. If you want to
// reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that
// does not match any of the domains you plan to visit.
// **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it
// work by replacing `localhost` with `local.playwright`.
ClientCertificates []ClientCertificate `json:"clientCertificates"`
@ -327,8 +330,8 @@ type APIRequestContextPutOptions struct {
}
type StorageState struct {
Cookies []Cookie `json:"cookies"`
Origins []Origin `json:"origins"`
Cookies []StorageStateCookie `json:"cookies"`
Origins []Origin `json:"origins"`
}
type NameValue struct {
@ -367,6 +370,9 @@ type BrowserNewContextOptions struct {
// a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
// `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
// with an exact match to the request origin that the certificate is valid for.
// Client certificate authentication is only active when at least one client certificate is provided. If you want to
// reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that
// does not match any of the domains you plan to visit.
// **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it
// work by replacing `localhost` with `local.playwright`.
ClientCertificates []ClientCertificate `json:"clientCertificates"`
@ -514,6 +520,9 @@ type BrowserNewPageOptions struct {
// a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
// `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
// with an exact match to the request origin that the certificate is valid for.
// Client certificate authentication is only active when at least one client certificate is provided. If you want to
// reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that
// does not match any of the domains you plan to visit.
// **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it
// work by replacing `localhost` with `local.playwright`.
ClientCertificates []ClientCertificate `json:"clientCertificates"`
@ -651,12 +660,12 @@ type BrowserStartTracingOptions struct {
type OptionalCookie struct {
Name string `json:"name"`
Value string `json:"value"`
// Either url or domain / path are required. Optional.
// Either `url` or both `domain` and `path` are required. Optional.
URL *string `json:"url"`
// For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url
// or domain / path are required. Optional.
// For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either
// `url` or both `domain` and `path` are required. Optional.
Domain *string `json:"domain"`
// Either url or domain / path are required Optional.
// Either `url` or both `domain` and `path` are required. Optional.
Path *string `json:"path"`
// Unix time in seconds. Optional.
Expires *float64 `json:"expires"`
@ -666,6 +675,12 @@ type OptionalCookie struct {
Secure *bool `json:"secure"`
// Optional.
SameSite *SameSiteAttribute `json:"sameSite"`
// For partitioned third-party cookies (aka
// [CHIPS], the
// partition key. Optional.
//
// [CHIPS]: https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies)
PartitionKey *string `json:"partitionKey"`
}
type Script struct {
@ -696,10 +711,11 @@ type Cookie struct {
Domain string `json:"domain"`
Path string `json:"path"`
// Unix time in seconds.
Expires float64 `json:"expires"`
HttpOnly bool `json:"httpOnly"`
Secure bool `json:"secure"`
SameSite *SameSiteAttribute `json:"sameSite"`
Expires float64 `json:"expires"`
HttpOnly bool `json:"httpOnly"`
Secure bool `json:"secure"`
SameSite *SameSiteAttribute `json:"sameSite"`
PartitionKey *string `json:"partitionKey"`
}
type BrowserContextGrantPermissionsOptions struct {
@ -847,8 +863,11 @@ type BrowserTypeLaunchOptions struct {
ExecutablePath *string `json:"executablePath"`
// Firefox user preferences. Learn more about the Firefox user preferences at
// [`about:config`].
// You can also provide a path to a custom [`policies.json` file] via
// `PLAYWRIGHT_FIREFOX_POLICIES_JSON` environment variable.
//
// [`about:config`]: https://support.mozilla.org/en-US/kb/about-config-editor-firefox
// [`policies.json` file]: https://mozilla.github.io/policy-templates/
FirefoxUserPrefs map[string]interface{} `json:"firefoxUserPrefs"`
// Close the browser process on SIGHUP. Defaults to `true`.
HandleSIGHUP *bool `json:"handleSIGHUP"`
@ -922,6 +941,9 @@ type BrowserTypeLaunchPersistentContextOptions struct {
// a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally,
// `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided
// with an exact match to the request origin that the certificate is valid for.
// Client certificate authentication is only active when at least one client certificate is provided. If you want to
// reject all client certificates sent by the server, you need to provide a client certificate with an `origin` that
// does not match any of the domains you plan to visit.
// **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it
// work by replacing `localhost` with `local.playwright`.
ClientCertificates []ClientCertificate `json:"clientCertificates"`
@ -957,8 +979,11 @@ type BrowserTypeLaunchPersistentContextOptions struct {
ExtraHttpHeaders map[string]string `json:"extraHTTPHeaders"`
// Firefox user preferences. Learn more about the Firefox user preferences at
// [`about:config`].
// You can also provide a path to a custom [`policies.json` file] via
// `PLAYWRIGHT_FIREFOX_POLICIES_JSON` environment variable.
//
// [`about:config`]: https://support.mozilla.org/en-US/kb/about-config-editor-firefox
// [`policies.json` file]: https://mozilla.github.io/policy-templates/
FirefoxUserPrefs map[string]interface{} `json:"firefoxUserPrefs"`
// Emulates `forced-colors` media feature, supported values are `active`, `none`. See [Page.EmulateMedia] for
// more details. Passing `no-override` resets emulation to system defaults. Defaults to `none`.
@ -1157,6 +1182,9 @@ type ElementHandleClickOptions struct {
// A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of
// the element.
Position *Position `json:"position"`
// Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor
// position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location.
Steps *int `json:"steps"`
// Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can
// be changed by using the [BrowserContext.SetDefaultTimeout] or [Page.SetDefaultTimeout] methods.
Timeout *float64 `json:"timeout"`
@ -1187,6 +1215,9 @@ type ElementHandleDblclickOptions struct {
// A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of
// the element.
Position *Position `json:"position"`
// Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor
// position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location.
Steps *int `json:"steps"`
// Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can
// be changed by using the [BrowserContext.SetDefaultTimeout] or [Page.SetDefaultTimeout] methods.
Timeout *float64 `json:"timeout"`
@ -1533,6 +1564,9 @@ type FrameClickOptions struct {
// A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of
// the element.
Position *Position `json:"position"`
// Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor
// position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location.
Steps *int `json:"steps"`
// When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
// element, the call throws an exception.
Strict *bool `json:"strict"`
@ -1604,6 +1638,9 @@ type FrameDragAndDropOptions struct {
// Clicks on the source element at this point relative to the top-left corner of the element's padding box. If not
// specified, some visible point of the element is used.
SourcePosition *Position `json:"sourcePosition"`
// Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup`
// of the drag. When set to 1, emits a single `mousemove` event at the destination location.
Steps *int `json:"steps"`
// When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
// element, the call throws an exception.
Strict *bool `json:"strict"`
@ -2258,9 +2295,6 @@ type KeyboardTypeOptions struct {
}
type LocatorAriaSnapshotOptions struct {
// Generate symbolic reference for each element. One can use `aria-ref=<ref>` locator immediately after capturing the
// snapshot to perform actions on the element.
Ref *bool `json:"ref"`
// Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can
// be changed by using the [BrowserContext.SetDefaultTimeout] or [Page.SetDefaultTimeout] methods.
Timeout *float64 `json:"timeout"`
@ -2338,6 +2372,9 @@ type LocatorClickOptions struct {
// A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of
// the element.
Position *Position `json:"position"`
// Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor
// position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location.
Steps *int `json:"steps"`
// Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can
// be changed by using the [BrowserContext.SetDefaultTimeout] or [Page.SetDefaultTimeout] methods.
Timeout *float64 `json:"timeout"`
@ -2370,6 +2407,9 @@ type LocatorDblclickOptions struct {
// A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of
// the element.
Position *Position `json:"position"`
// Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor
// position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location.
Steps *int `json:"steps"`
// Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can
// be changed by using the [BrowserContext.SetDefaultTimeout] or [Page.SetDefaultTimeout] methods.
Timeout *float64 `json:"timeout"`
@ -2400,6 +2440,9 @@ type LocatorDragToOptions struct {
// Clicks on the source element at this point relative to the top-left corner of the element's padding box. If not
// specified, some visible point of the element is used.
SourcePosition *Position `json:"sourcePosition"`
// Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup`
// of the drag. When set to 1, emits a single `mousemove` event at the destination location.
Steps *int `json:"steps"`
// Drops on the target element at this point relative to the top-left corner of the element's padding box. If not
// specified, some visible point of the element is used.
TargetPosition *Position `json:"targetPosition"`
@ -3073,7 +3116,8 @@ type MouseDownOptions struct {
}
type MouseMoveOptions struct {
// Defaults to 1. Sends intermediate `mousemove` events.
// Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor
// position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location.
Steps *int `json:"steps"`
}
@ -3158,6 +3202,9 @@ type PageClickOptions struct {
// A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of
// the element.
Position *Position `json:"position"`
// Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between Playwright's current cursor
// position and the provided destination. When set to 1, emits a single `mousemove` event at the destination location.
Steps *int `json:"steps"`
// When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
// element, the call throws an exception.
Strict *bool `json:"strict"`
@ -3239,6 +3286,9 @@ type PageDragAndDropOptions struct {
// Clicks on the source element at this point relative to the top-left corner of the element's padding box. If not
// specified, some visible point of the element is used.
SourcePosition *Position `json:"sourcePosition"`
// Defaults to 1. Sends `n` interpolated `mousemove` events to represent travel between the `mousedown` and `mouseup`
// of the drag. When set to 1, emits a single `mousemove` event at the destination location.
Steps *int `json:"steps"`
// When true, the call requires selector to resolve to a single element. If given selector resolves to more than one
// element, the call throws an exception.
Strict *bool `json:"strict"`
@ -4320,6 +4370,18 @@ type Proxy struct {
Password *string `json:"password"`
}
type StorageStateCookie struct {
Name string `json:"name"`
Value string `json:"value"`
Domain string `json:"domain"`
Path string `json:"path"`
// Unix time in seconds.
Expires float64 `json:"expires"`
HttpOnly bool `json:"httpOnly"`
Secure bool `json:"secure"`
SameSite *SameSiteAttribute `json:"sameSite"`
}
type Origin struct {
Origin string `json:"origin"`
LocalStorage []NameValue `json:"localStorage"`
@ -4336,7 +4398,7 @@ type RecordVideo struct {
type OptionalStorageState struct {
// Cookies to set for context
Cookies []OptionalCookie `json:"cookies"`
Cookies []OptionalStorageStateOptionalCookie `json:"cookies"`
// localStorage to set for context
Origins []Origin `json:"origins"`
}
@ -4362,3 +4424,23 @@ type TracingGroupOptionsLocation struct {
Line *int `json:"line"`
Column *int `json:"column"`
}
type OptionalStorageStateOptionalCookie struct {
Name string `json:"name"`
Value string `json:"value"`
// Either url or domain / path are required. Optional.
URL *string `json:"url"`
// For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url
// or domain / path are required. Optional.
Domain *string `json:"domain"`
// Either url or domain / path are required Optional.
Path *string `json:"path"`
// Unix time in seconds. Optional.
Expires *float64 `json:"expires"`
// Optional.
HttpOnly *bool `json:"httpOnly"`
// Optional.
Secure *bool `json:"secure"`
// Optional.
SameSite *SameSiteAttribute `json:"sameSite"`
}

View File

@ -61,15 +61,21 @@ func transformStructIntoMapIfNeeded(inStruct interface{}) map[string]interface{}
// Merge into the base map by the JSON struct tag
for i := 0; i < v.NumField(); i++ {
fi := typ.Field(i)
tagv := fi.Tag.Get("json")
key := strings.Split(tagv, ",")[0]
if key == "" {
key = fi.Name
}
// Special handling for timeout field: provide default value when nil
// This is required in Playwright v1.57+ protocol where timeout is no longer optional
if key == "timeout" && skipFieldSerialization(v.Field(i)) {
out[key] = float64(30000) // default 30s
continue
}
// Skip the values when the field is a pointer (like *string) and nil.
if fi.IsExported() && !skipFieldSerialization(v.Field(i)) {
// We use the JSON struct fields for getting the original names
// out of the field.
tagv := fi.Tag.Get("json")
key := strings.Split(tagv, ",")[0]
if key == "" {
key = fi.Name
}
out[key] = transformStructValues(v.Field(i).Interface())
}
}
@ -110,6 +116,16 @@ func transformOptions(options ...interface{}) map[string]interface{} {
v := reflect.ValueOf(option)
if v.Kind() == reflect.Slice {
if v.Len() == 0 {
// Check if the slice element type has a Timeout field and add default if so
// This is required in Playwright v1.57+ protocol where timeout is no longer optional
elemType := v.Type().Elem()
if elemType.Kind() == reflect.Struct {
if _, hasTimeout := elemType.FieldByName("Timeout"); hasTimeout {
if base["timeout"] == nil {
base["timeout"] = float64(30000) // default 30s
}
}
}
return base
}
option = v.Index(0).Interface()

View File

@ -115,7 +115,11 @@ func convertInputFiles(files interface{}, context *browserContextImpl) (*inputFi
}
if result["rootDir"] != nil {
converted.DirectoryStream = result["rootDir"].(*channel)
if ch, ok := result["rootDir"].(*channel); ok {
converted.DirectoryStream = ch
} else {
converted.DirectoryStream = fromChannel(result["rootDir"]).(*writableStream).channel
}
} else {
converted.Streams = streams
}

View File

@ -33,7 +33,7 @@ func (j *jsonPipe) Poll() (*message, error) {
func newJsonPipe(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *jsonPipe {
j := &jsonPipe{
msgChan: make(chan *message, 2),
msgChan: make(chan *message, 10),
}
j.createChannelOwner(j, parent, objectType, guid, initializer)
j.channel.On("message", func(ev map[string]interface{}) {
@ -54,6 +54,12 @@ func newJsonPipe(parent *channelOwner, objectType string, guid string, initializ
},
}
}
// Send directly to maintain message ordering - the channel buffer prevents blocking
// Previously used a goroutine which could cause out-of-order delivery
defer func() {
// Recover from panic if channel is closed
_ = recover()
}()
j.msgChan <- &msg
})
j.channel.Once("closed", func() {

View File

@ -12,10 +12,11 @@ var (
)
type locatorImpl struct {
frame *frameImpl
selector string
options *LocatorOptions
err error
frame *frameImpl
selector string
options *LocatorOptions
err error
description *string
}
type LocatorOptions LocatorFilterOptions
@ -65,6 +66,23 @@ func (l *locatorImpl) Err() error {
return l.err
}
func (l *locatorImpl) Describe(description string) Locator {
return &locatorImpl{
frame: l.frame,
selector: l.selector,
options: l.options,
err: l.err,
description: &description,
}
}
func (l *locatorImpl) Description() (string, error) {
if l.description == nil {
return "", nil
}
return *l.description, nil
}
func (l *locatorImpl) All() ([]Locator, error) {
result := make([]Locator, 0)
count, err := l.Count()
@ -125,10 +143,10 @@ func (l *locatorImpl) Blur(options ...LocatorBlurOptions) error {
"selector": l.selector,
"strict": true,
}
if len(options) == 1 {
if options[0].Timeout != nil {
params["timeout"] = options[0].Timeout
}
if len(options) == 1 && options[0].Timeout != nil {
params["timeout"] = options[0].Timeout
} else {
params["timeout"] = float64(30000) // default 30s, required in Playwright v1.57+
}
_, err := l.frame.channel.Send("blur", params)
return err

View File

@ -1,13 +1,24 @@
package playwright
// dummyObject is a placeholder for unimplemented protocol objects (Android, Electron, etc.)
type dummyObject struct {
channelOwner
}
func newDummyObject(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *dummyObject {
d := &dummyObject{}
d.createChannelOwner(d, parent, objectType, guid, initializer)
return d
}
func createObjectFactory(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) interface{} {
switch objectType {
case "Android":
return nil
return newDummyObject(parent, objectType, guid, initializer)
case "AndroidSocket":
return nil
return newDummyObject(parent, objectType, guid, initializer)
case "AndroidDevice":
return nil
return newDummyObject(parent, objectType, guid, initializer)
case "APIRequestContext":
return newAPIRequestContext(parent, objectType, guid, initializer)
case "Artifact":
@ -25,9 +36,9 @@ func createObjectFactory(parent *channelOwner, objectType string, guid string, i
case "Dialog":
return newDialog(parent, objectType, guid, initializer)
case "Electron":
return nil
return newDummyObject(parent, objectType, guid, initializer)
case "ElectronApplication":
return nil
return newDummyObject(parent, objectType, guid, initializer)
case "ElementHandle":
return newElementHandle(parent, objectType, guid, initializer)
case "Frame":

33
page.go
View File

@ -7,6 +7,7 @@ import (
"os"
"slices"
"sync"
"sync/atomic"
"github.com/playwright-community/playwright-go/internal/safe"
)
@ -30,7 +31,7 @@ type pageImpl struct {
ownedContext BrowserContext
bindings *safe.SyncMap[string, BindingCallFunction]
closeReason *string
closeWasCalled bool
closeWasCalled atomic.Bool
harRouters []*harRouter
locatorHandlers map[float64]*locatorHandlerEntry
}
@ -125,7 +126,7 @@ func (p *pageImpl) Close(options ...PageCloseOptions) error {
if len(options) == 1 {
p.closeReason = options[0].Reason
}
p.closeWasCalled = true
p.closeWasCalled.Store(true)
_, err := p.channel.Send("close", options)
if err == nil && p.ownedContext != nil {
err = p.ownedContext.Close()
@ -746,6 +747,32 @@ func (p *pageImpl) Keyboard() Keyboard {
return p.keyboard
}
func (p *pageImpl) ConsoleMessages() ([]ConsoleMessage, error) {
result, err := p.channel.Send("consoleMessages", nil)
if err != nil {
return nil, err
}
messages := result.([]interface{})
consoleMessages := make([]ConsoleMessage, len(messages))
for i, m := range messages {
consoleMessages[i] = newConsoleMessage(m.(map[string]interface{}))
}
return consoleMessages, nil
}
func (p *pageImpl) Requests() ([]Request, error) {
result, err := p.channel.Send("requests", nil)
if err != nil {
return nil, err
}
requests := result.([]interface{})
reqs := make([]Request, len(requests))
for i, r := range requests {
reqs[i] = fromChannel(r).(*requestImpl)
}
return reqs, nil
}
func (p *pageImpl) Mouse() Mouse {
return p.mouse
}
@ -942,7 +969,7 @@ func (p *pageImpl) onRoute(route *routeImpl) {
url := route.Request().URL()
for _, handlerEntry := range routes {
// If the page was closed we stall all requests right away.
if p.closeWasCalled || p.browserContext.closeWasCalled {
if p.closeWasCalled.Load() || p.browserContext.closeWasCalled.Load() {
return
}
if !handlerEntry.Matches(url) {

View File

@ -1,8 +1,10 @@
package playwright
import (
"fmt"
"net/url"
"path"
"strings"
)
type pageAssertionsImpl struct {
@ -21,6 +23,65 @@ func newPageAssertions(page Page, isNot bool, defaultTimeout *float64) *pageAsse
}
}
// expectOnFrame calls the frame's expect method directly without a selector.
// This is needed for page-level assertions like ToHaveTitle and ToHaveURL
// which should not be bound to a specific element.
func (pa *pageAssertionsImpl) expectOnFrame(
expression string,
options frameExpectOptions,
expected interface{},
message string,
) error {
options.IsNot = pa.isNot
if options.Timeout == nil {
options.Timeout = pa.defaultTimeout
}
if options.IsNot {
message = strings.ReplaceAll(message, "expected to", "expected not to")
}
frame := pa.actualPage.MainFrame().(*frameImpl)
overrides := map[string]interface{}{
"expression": expression,
}
result, err := frame.channel.SendReturnAsDict("expect", options, overrides)
if err != nil {
return err
}
var (
received interface{}
matches bool
log []string
)
if v, ok := result["received"]; ok {
received = parseResult(v)
}
if v, ok := result["matches"]; ok {
matches = v.(bool)
}
if v, ok := result["log"]; ok {
for _, l := range v.([]interface{}) {
log = append(log, l.(string))
}
}
if matches == pa.isNot {
actual := received
logStr := strings.Join(log, "\n")
if logStr != "" {
logStr = "\nCall log:\n" + logStr
}
if expected != nil {
return fmt.Errorf("%s '%v'\nActual value: %v %s", message, expected, actual, logStr)
}
return fmt.Errorf("%s\nActual value: %v %s", message, actual, logStr)
}
return nil
}
func (pa *pageAssertionsImpl) ToHaveTitle(titleOrRegExp interface{}, options ...PageAssertionsToHaveTitleOptions) error {
var timeout *float64
if len(options) == 1 {
@ -30,7 +91,7 @@ func (pa *pageAssertionsImpl) ToHaveTitle(titleOrRegExp interface{}, options ...
if err != nil {
return err
}
return pa.expect(
return pa.expectOnFrame(
"to.have.title",
frameExpectOptions{ExpectedText: expectedValues, Timeout: timeout},
titleOrRegExp,
@ -57,7 +118,7 @@ func (pa *pageAssertionsImpl) ToHaveURL(urlOrRegExp interface{}, options ...Page
if err != nil {
return err
}
return pa.expect(
return pa.expectOnFrame(
"to.have.url",
frameExpectOptions{ExpectedText: expectedValues, Timeout: timeout},
urlOrRegExp,

View File

@ -330,10 +330,10 @@ index 7867ce5c8..6a27ede39 100644
:::note
diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md
index afd73a414..bf3f5ee0a 100644
index 3e7529c48..aef4351b8 100644
--- a/docs/src/api/class-browsercontext.md
+++ b/docs/src/api/class-browsercontext.md
@@ -417,7 +417,7 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
@@ -389,7 +389,7 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
### param: BrowserContext.addInitScript.script
* since: v1.8
@ -342,7 +342,7 @@ index afd73a414..bf3f5ee0a 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.
@@ -1214,7 +1214,7 @@ handler function to route the request.
@@ -1186,7 +1186,7 @@ handler function to route the request.
### param: BrowserContext.route.handler
* since: v1.8
@ -351,7 +351,7 @@ index afd73a414..bf3f5ee0a 100644
- `handler` <[function]\([Route]\)>
handler function to route the request.
@@ -1357,7 +1357,7 @@ Handler function to route the WebSocket.
@@ -1329,7 +1329,7 @@ Handler function to route the WebSocket.
### param: BrowserContext.routeWebSocket.handler
* since: v1.48
@ -360,7 +360,7 @@ index afd73a414..bf3f5ee0a 100644
- `handler` <[function]\([WebSocketRoute]\)>
Handler function to route the WebSocket.
@@ -1365,7 +1365,7 @@ Handler function to route the WebSocket.
@@ -1337,7 +1337,7 @@ Handler function to route the WebSocket.
## method: BrowserContext.serviceWorkers
* since: v1.11
@ -369,7 +369,7 @@ index afd73a414..bf3f5ee0a 100644
- returns: <[Array]<[Worker]>>
:::note
@@ -1522,6 +1522,7 @@ Returns storage state for this browser context, contains current cookies, local
@@ -1495,6 +1495,7 @@ Returns storage state for this browser context, contains current cookies, local
### option: BrowserContext.storageState.indexedDB
* since: v1.51
@ -377,7 +377,7 @@ index afd73a414..bf3f5ee0a 100644
- `indexedDB` ?<boolean>
Set to `true` to include [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) in the storage state snapshot.
@@ -1559,6 +1560,13 @@ A glob pattern, regex pattern or predicate receiving [URL] used to register a ro
@@ -1532,6 +1533,13 @@ A glob pattern, regex pattern or predicate receiving [URL] used to register a ro
Optional handler function used to register a routing with [`method: BrowserContext.route`].
@ -391,7 +391,7 @@ index afd73a414..bf3f5ee0a 100644
### param: BrowserContext.unroute.handler
* since: v1.8
* langs: csharp, java
@@ -1600,7 +1608,8 @@ Condition to wait for.
@@ -1573,7 +1581,8 @@ Condition to wait for.
## async method: BrowserContext.waitForConsoleMessage
* since: v1.34
@ -401,7 +401,7 @@ index afd73a414..bf3f5ee0a 100644
- alias-python: expect_console_message
- alias-csharp: RunAndWaitForConsoleMessage
- returns: <[ConsoleMessage]>
@@ -1631,7 +1640,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti
@@ -1604,7 +1613,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti
## async method: BrowserContext.waitForEvent
* since: v1.8
@ -411,7 +411,7 @@ index afd73a414..bf3f5ee0a 100644
- alias-python: expect_event
- returns: <[any]>
@@ -1697,7 +1707,8 @@ Either a predicate that receives an event or an options object. Optional.
@@ -1670,7 +1680,8 @@ Either a predicate that receives an event or an options object. Optional.
## async method: BrowserContext.waitForPage
* since: v1.9
@ -421,7 +421,7 @@ index afd73a414..bf3f5ee0a 100644
- alias-python: expect_page
- alias-csharp: RunAndWaitForPage
- returns: <[Page]>
@@ -1716,7 +1727,7 @@ Will throw an error if the context closes before new [Page] is created.
@@ -1689,7 +1700,7 @@ Will throw an error if the context closes before new [Page] is created.
### option: BrowserContext.waitForPage.predicate
* since: v1.9
@ -430,7 +430,7 @@ index afd73a414..bf3f5ee0a 100644
- `predicate` <[function]\([Page]\):[boolean]>
Receives the [Page] object and resolves to truthy value when the waiting should resolve.
@@ -1729,7 +1740,8 @@ Receives the [Page] object and resolves to truthy value when the waiting should
@@ -1702,7 +1713,8 @@ Receives the [Page] object and resolves to truthy value when the waiting should
## async method: BrowserContext.waitForEvent2
* since: v1.8
@ -494,7 +494,7 @@ index e5610b424..748b93b68 100644
- `time` <[float]|[string]|[Date]>
diff --git a/docs/src/api/class-consolemessage.md b/docs/src/api/class-consolemessage.md
index 347838ae4..e461d6f9f 100644
index b712bca26..4a164b95e 100644
--- a/docs/src/api/class-consolemessage.md
+++ b/docs/src/api/class-consolemessage.md
@@ -112,7 +112,7 @@ List of arguments passed to a `console` function call. See also [`event: Page.co
@ -519,12 +519,31 @@ index 347838ae4..e461d6f9f 100644
+
## method: ConsoleMessage.type
* since: v1.8
* langs: js, python
@@ -144,7 +151,7 @@ The text of the console message.
## method: ConsoleMessage.type
* since: v1.8
-* langs: csharp, java
+* langs: csharp, java, go
- returns: <[string]>
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 e286c63bf..7dbd5f4fd 100644
index f9c366e42..678bf3e65 100644
--- a/docs/src/api/class-frame.md
+++ b/docs/src/api/class-frame.md
@@ -1946,7 +1946,7 @@ await page.MainFrame.WaitForFunctionAsync("selector => !!document.querySelector(
@@ -283,6 +283,9 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Frame.click.trial = %%-input-trial-with-modifiers-%%
* since: v1.11
+### option: Frame.click.steps = %%-input-mousemove-steps-%%
+* since: v1.57
+
## async method: Frame.content
* since: v1.8
- returns: <[string]>
@@ -1949,7 +1952,7 @@ await page.MainFrame.WaitForFunctionAsync("selector => !!document.querySelector(
Optional argument to pass to [`param: expression`].
@ -533,7 +552,7 @@ index e286c63bf..7dbd5f4fd 100644
* since: v1.8
### option: Frame.waitForFunction.polling = %%-csharp-java-wait-for-function-polling-%%
@@ -1998,6 +1998,11 @@ await frame.WaitForLoadStateAsync(); // Defaults to LoadState.Load
@@ -2001,6 +2004,11 @@ await frame.WaitForLoadStateAsync(); // Defaults to LoadState.Load
```
### param: Frame.waitForLoadState.state = %%-wait-for-load-state-state-%%
@ -545,7 +564,7 @@ index e286c63bf..7dbd5f4fd 100644
* since: v1.8
### option: Frame.waitForLoadState.timeout = %%-navigation-timeout-%%
@@ -2010,6 +2015,7 @@ await frame.WaitForLoadStateAsync(); // Defaults to LoadState.Load
@@ -2013,6 +2021,7 @@ await frame.WaitForLoadStateAsync(); // Defaults to LoadState.Load
* since: v1.8
* deprecated: This method is inherently racy, please use [`method: Frame.waitForURL`] instead.
* langs:
@ -554,10 +573,10 @@ index e286c63bf..7dbd5f4fd 100644
* alias-csharp: RunAndWaitForNavigation
- returns: <[null]|[Response]>
diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md
index 25462a0ed..f85e4aff6 100644
index 042c08bb9..a1925c73d 100644
--- a/docs/src/api/class-locator.md
+++ b/docs/src/api/class-locator.md
@@ -885,7 +885,7 @@ Optional argument to pass to [`param: expression`].
@@ -1006,7 +1006,7 @@ Optional argument to pass to [`param: expression`].
### option: Locator.evaluate.timeout
* since: v1.14
@ -566,7 +585,7 @@ index 25462a0ed..f85e4aff6 100644
- `timeout` <[float]>
Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
@@ -982,7 +982,7 @@ Optional argument to pass to [`param: expression`].
@@ -1103,7 +1103,7 @@ Optional argument to pass to [`param: expression`].
### option: Locator.evaluateHandle.timeout
* since: v1.14
@ -576,7 +595,7 @@ index 25462a0ed..f85e4aff6 100644
Maximum time in milliseconds to wait for the locator before evaluating. Note that after locator is resolved, evaluation itself is not limited by the timeout. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
diff --git a/docs/src/api/class-locatorassertions.md b/docs/src/api/class-locatorassertions.md
index 78ed036c0..e78149d3d 100644
index c2edbf753..2f0a348a0 100644
--- a/docs/src/api/class-locatorassertions.md
+++ b/docs/src/api/class-locatorassertions.md
@@ -67,7 +67,7 @@ public class ExampleTests : PageTest
@ -625,10 +644,10 @@ index 78ed036c0..e78149d3d 100644
Expected options currently selected.
diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md
index a69ac2446..a026da311 100644
index 717cbf06c..a54492b22 100644
--- a/docs/src/api/class-page.md
+++ b/docs/src/api/class-page.md
@@ -621,7 +621,7 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
@@ -614,7 +614,7 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
### param: Page.addInitScript.script
* since: v1.8
@ -637,7 +656,17 @@ index a69ac2446..a026da311 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.
@@ -1267,6 +1267,14 @@ Passing `null` disables CSS media emulation.
@@ -808,6 +808,9 @@ When all steps combined have not finished during the specified [`option: timeout
### option: Page.click.trial = %%-input-trial-with-modifiers-%%
* since: v1.11
+### option: Page.click.steps = %%-input-mousemove-steps-%%
+* since: v1.57
+
## async method: Page.close
* since: v1.8
@@ -1263,6 +1266,14 @@ Passing `null` disables CSS media emulation.
Changes the CSS media type of the page. The only allowed values are `'Screen'`, `'Print'` and `'Null'`.
Passing `'Null'` disables CSS media emulation.
@ -652,7 +681,7 @@ index a69ac2446..a026da311 100644
### option: Page.emulateMedia.colorScheme
* since: v1.9
* langs: js, java
@@ -1283,6 +1291,14 @@ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CS
@@ -1279,6 +1290,14 @@ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CS
Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media feature, supported values are `'light'` and `'dark'`. Passing
`'Null'` disables color scheme emulation. `'no-preference'` is deprecated.
@ -667,7 +696,7 @@ index a69ac2446..a026da311 100644
### option: Page.emulateMedia.reducedMotion
* since: v1.12
* langs: js, java
@@ -1297,6 +1313,13 @@ Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce
@@ -1293,6 +1312,13 @@ Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce
Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. Passing `null` disables reduced motion emulation.
@ -681,7 +710,7 @@ index a69ac2446..a026da311 100644
### option: Page.emulateMedia.forcedColors
* since: v1.15
* langs: js, java
@@ -1309,6 +1332,13 @@ Emulates `'forced-colors'` media feature, supported values are `'active'` and `'
@@ -1305,6 +1331,13 @@ Emulates `'forced-colors'` media feature, supported values are `'active'` and `'
* langs: csharp, python
- `forcedColors` <[ForcedColors]<"active"|"none"|"null">>
@ -695,7 +724,7 @@ index a69ac2446..a026da311 100644
### option: Page.emulateMedia.contrast
* since: v1.51
* langs: js, java
@@ -1321,6 +1351,13 @@ Emulates `'prefers-contrast'` media feature, supported values are `'no-preferenc
@@ -1317,6 +1350,13 @@ Emulates `'prefers-contrast'` media feature, supported values are `'no-preferenc
* langs: csharp, python
- `contrast` <[Contrast]<"no-preference"|"more"|"null">>
@ -709,7 +738,7 @@ index a69ac2446..a026da311 100644
## async method: Page.evalOnSelector
* since: v1.9
* discouraged: This method does not wait for the element to pass actionability
@@ -2140,14 +2177,14 @@ Frame name specified in the `iframe`'s `name` attribute.
@@ -2136,14 +2176,14 @@ Frame name specified in the `iframe`'s `name` attribute.
### option: Page.frame.name
* since: v1.8
@ -726,7 +755,7 @@ index a69ac2446..a026da311 100644
- `url` ?<[string]|[RegExp]|[function]\([URL]\):[boolean]>
A glob pattern, regex pattern or predicate receiving frame's `url` as a [URL] object. Optional.
@@ -2917,7 +2954,7 @@ Paper width, accepts values labeled with units.
@@ -2936,7 +2976,7 @@ Paper width, accepts values labeled with units.
### option: Page.pdf.width
* since: v1.8
@ -735,7 +764,7 @@ index a69ac2446..a026da311 100644
- `width` <[string]>
Paper width, accepts values labeled with units.
@@ -2931,7 +2968,7 @@ Paper height, accepts values labeled with units.
@@ -2950,7 +2990,7 @@ Paper height, accepts values labeled with units.
### option: Page.pdf.height
* since: v1.8
@ -744,7 +773,7 @@ index a69ac2446..a026da311 100644
- `height` <[string]>
Paper height, accepts values labeled with units.
@@ -2949,7 +2986,7 @@ Paper margins, defaults to none.
@@ -2968,7 +3008,7 @@ Paper margins, defaults to none.
### option: Page.pdf.margin
* since: v1.8
@ -753,7 +782,7 @@ index a69ac2446..a026da311 100644
- `margin` <[Object]>
- `top` ?<[string]> Top margin, accepts values labeled with units. Defaults to `0`.
- `right` ?<[string]> Right margin, accepts values labeled with units. Defaults to `0`.
@@ -3370,7 +3407,7 @@ Function that should be run once [`param: locator`] appears. This function shoul
@@ -3400,7 +3440,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
@ -762,7 +791,7 @@ index a69ac2446..a026da311 100644
* since: v1.42
- `handler` <[function]\([Locator]\)>
@@ -3616,6 +3653,13 @@ A glob pattern, regex pattern, or predicate that receives a [URL] to match durin
@@ -3646,6 +3686,13 @@ A glob pattern, regex pattern, or predicate that receives a [URL] to match durin
handler function to route the request.
@ -776,7 +805,7 @@ index a69ac2446..a026da311 100644
### param: Page.route.handler
* since: v1.8
* langs: csharp, java
@@ -3750,7 +3794,7 @@ Handler function to route the WebSocket.
@@ -3780,7 +3827,7 @@ Handler function to route the WebSocket.
### param: Page.routeWebSocket.handler
* since: v1.48
@ -785,7 +814,7 @@ index a69ac2446..a026da311 100644
- `handler` <[function]\([WebSocketRoute]\)>
Handler function to route the WebSocket.
@@ -4079,14 +4123,14 @@ await page.GotoAsync("https://www.microsoft.com");
@@ -4109,14 +4156,14 @@ await page.GotoAsync("https://www.microsoft.com");
### param: Page.setViewportSize.width
* since: v1.10
@ -802,7 +831,7 @@ index a69ac2446..a026da311 100644
- `height` <[int]>
Page height in pixels.
@@ -4273,6 +4317,13 @@ A glob pattern, regex pattern or predicate receiving [URL] to match while routin
@@ -4303,6 +4350,13 @@ A glob pattern, regex pattern or predicate receiving [URL] to match while routin
Optional handler function to route the request.
@ -816,7 +845,7 @@ index a69ac2446..a026da311 100644
### param: Page.unroute.handler
* since: v1.8
* langs: csharp, java
@@ -4311,7 +4362,8 @@ Performs action and waits for the Page to close.
@@ -4341,7 +4395,8 @@ Performs action and waits for the Page to close.
## async method: Page.waitForConsoleMessage
* since: v1.9
@ -826,7 +855,7 @@ index a69ac2446..a026da311 100644
- alias-python: expect_console_message
- alias-csharp: RunAndWaitForConsoleMessage
- returns: <[ConsoleMessage]>
@@ -4342,7 +4394,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti
@@ -4372,7 +4427,8 @@ Receives the [ConsoleMessage] object and resolves to truthy value when the waiti
## async method: Page.waitForDownload
* since: v1.9
@ -836,7 +865,7 @@ index a69ac2446..a026da311 100644
- alias-python: expect_download
- alias-csharp: RunAndWaitForDownload
- returns: <[Download]>
@@ -4373,7 +4426,8 @@ Receives the [Download] object and resolves to truthy value when the waiting sho
@@ -4403,7 +4459,8 @@ Receives the [Download] object and resolves to truthy value when the waiting sho
## async method: Page.waitForEvent
* since: v1.8
@ -846,7 +875,7 @@ index a69ac2446..a026da311 100644
- alias-python: expect_event
- returns: <[any]>
@@ -4426,7 +4480,8 @@ Either a predicate that receives an event or an options object. Optional.
@@ -4456,7 +4513,8 @@ Either a predicate that receives an event or an options object. Optional.
## async method: Page.waitForFileChooser
* since: v1.9
@ -856,7 +885,7 @@ index a69ac2446..a026da311 100644
- alias-python: expect_file_chooser
- alias-csharp: RunAndWaitForFileChooser
- returns: <[FileChooser]>
@@ -4584,7 +4639,7 @@ await page.WaitForFunctionAsync("selector => !!document.querySelector(selector)"
@@ -4614,7 +4672,7 @@ await page.WaitForFunctionAsync("selector => !!document.querySelector(selector)"
Optional argument to pass to [`param: expression`].
@ -865,7 +894,7 @@ index a69ac2446..a026da311 100644
* since: v1.8
### option: Page.waitForFunction.polling = %%-csharp-java-wait-for-function-polling-%%
@@ -4681,6 +4736,11 @@ Console.WriteLine(await popup.TitleAsync()); // popup is ready to use.
@@ -4711,6 +4769,11 @@ Console.WriteLine(await popup.TitleAsync()); // popup is ready to use.
```
### param: Page.waitForLoadState.state = %%-wait-for-load-state-state-%%
@ -877,7 +906,7 @@ index a69ac2446..a026da311 100644
* since: v1.8
### option: Page.waitForLoadState.timeout = %%-navigation-timeout-%%
@@ -4693,6 +4753,7 @@ Console.WriteLine(await popup.TitleAsync()); // popup is ready to use.
@@ -4723,6 +4786,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:
@ -885,7 +914,7 @@ index a69ac2446..a026da311 100644
* alias-python: expect_navigation
* alias-csharp: RunAndWaitForNavigation
- returns: <[null]|[Response]>
@@ -4777,7 +4838,8 @@ a navigation.
@@ -4807,7 +4871,8 @@ a navigation.
## async method: Page.waitForPopup
* since: v1.9
@ -895,7 +924,7 @@ index a69ac2446..a026da311 100644
- alias-python: expect_popup
- alias-csharp: RunAndWaitForPopup
- returns: <[Page]>
@@ -4809,6 +4871,7 @@ Receives the [Page] object and resolves to truthy value when the waiting should
@@ -4839,6 +4904,7 @@ Receives the [Page] object and resolves to truthy value when the waiting should
## async method: Page.waitForRequest
* since: v1.8
* langs:
@ -903,7 +932,7 @@ index a69ac2446..a026da311 100644
* alias-python: expect_request
* alias-csharp: RunAndWaitForRequest
- returns: <[Request]>
@@ -4916,7 +4979,8 @@ changed by using the [`method: Page.setDefaultTimeout`] method.
@@ -4946,7 +5012,8 @@ changed by using the [`method: Page.setDefaultTimeout`] method.
## async method: Page.waitForRequestFinished
* since: v1.12
@ -913,7 +942,7 @@ index a69ac2446..a026da311 100644
- alias-python: expect_request_finished
- alias-csharp: RunAndWaitForRequestFinished
- returns: <[Request]>
@@ -4948,6 +5012,7 @@ Receives the [Request] object and resolves to truthy value when the waiting shou
@@ -4978,6 +5045,7 @@ Receives the [Request] object and resolves to truthy value when the waiting shou
## async method: Page.waitForResponse
* since: v1.8
* langs:
@ -921,7 +950,7 @@ index a69ac2446..a026da311 100644
* alias-python: expect_response
* alias-csharp: RunAndWaitForResponse
- returns: <[Response]>
@@ -5311,7 +5376,8 @@ await page.WaitForURLAsync("**/target.html");
@@ -5341,7 +5409,8 @@ await page.WaitForURLAsync("**/target.html");
## async method: Page.waitForWebSocket
* since: v1.9
@ -931,7 +960,7 @@ index a69ac2446..a026da311 100644
- alias-python: expect_websocket
- alias-csharp: RunAndWaitForWebSocket
- returns: <[WebSocket]>
@@ -5342,7 +5408,8 @@ Receives the [WebSocket] object and resolves to truthy value when the waiting sh
@@ -5372,7 +5441,8 @@ Receives the [WebSocket] object and resolves to truthy value when the waiting sh
## async method: Page.waitForWorker
* since: v1.9
@ -941,7 +970,7 @@ index a69ac2446..a026da311 100644
- alias-python: expect_worker
- alias-csharp: RunAndWaitForWorker
- returns: <[Worker]>
@@ -5384,7 +5451,8 @@ This does not contain ServiceWorkers
@@ -5414,7 +5484,8 @@ This does not contain ServiceWorkers
## async method: Page.waitForEvent2
* since: v1.8
@ -1126,10 +1155,10 @@ 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 3b0011c7b..48a28fa24 100644
index 0528891fd..1323d2393 100644
--- a/docs/src/api/class-tracing.md
+++ b/docs/src/api/class-tracing.md
@@ -142,7 +142,7 @@ If this option is true tracing will
@@ -156,7 +156,7 @@ If this option is true tracing will
### option: Tracing.start.sources
* since: v1.17
@ -1191,7 +1220,7 @@ index e33740eac..d89648d91 100644
- returns: <[any]>
diff --git a/docs/src/api/class-websocketroute.md b/docs/src/api/class-websocketroute.md
index e23316ebc..ae3d20b9f 100644
index fcbe1b21f..dc6503e4f 100644
--- a/docs/src/api/class-websocketroute.md
+++ b/docs/src/api/class-websocketroute.md
@@ -325,7 +325,7 @@ Function that will handle WebSocket closure. Received an optional [close code](h
@ -1218,7 +1247,7 @@ index e23316ebc..ae3d20b9f 100644
* since: v1.48
* langs: csharp, java
diff --git a/docs/src/api/params.md b/docs/src/api/params.md
index 4c4937004..ea6212dc9 100644
index 37f6665a9..dbe37d8a1 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:
@ -1248,7 +1277,7 @@ index 4c4937004..ea6212dc9 100644
- `timeout` <[float]>
Maximum time in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by
@@ -188,8 +188,8 @@ Defaults to `'visible'`. Can be either:
@@ -198,8 +198,8 @@ Defaults to `'visible'`. Can be either:
* `'hidden'` - wait for element to be either detached from DOM, or have an empty bounding box or `visibility:hidden`.
This is opposite to the `'visible'` option.
@ -1259,7 +1288,7 @@ index 4c4937004..ea6212dc9 100644
- `polling` <[float]|"raf">
If [`option: polling`] is `'raf'`, then [`param: expression`] is constantly executed in `requestAnimationFrame`
@@ -210,14 +210,14 @@ If `true`, Playwright does not pass its own configurations args and only uses th
@@ -220,14 +220,14 @@ If `true`, Playwright does not pass its own configurations args and only uses th
array is given, then filters out the given default arguments. Dangerous option; use with care. Defaults to `false`.
## csharp-java-browser-option-ignoredefaultargs
@ -1276,8 +1305,8 @@ index 4c4937004..ea6212dc9 100644
- `ignoreAllDefaultArgs` <[boolean]>
If `true`, Playwright does not pass its own configurations args and only uses the ones from [`option: args`].
@@ -236,7 +236,7 @@ Dangerous option; use with care. Defaults to `false`.
Network proxy settings.
@@ -250,7 +250,7 @@ Network proxy settings.
- `env` <[Object]<[string], [string]|[undefined]>>
## csharp-java-browser-option-env
-* langs: csharp, java
@ -1285,7 +1314,7 @@ index 4c4937004..ea6212dc9 100644
- `env` <[Object]<[string], [string]>>
Specify environment variables that will be visible to the browser. Defaults to `process.env`.
@@ -269,6 +269,29 @@ Learn more about [storage state and auth](../auth.md).
@@ -283,6 +283,29 @@ Learn more about [storage state and auth](../auth.md).
Populates context with given storage state. This option can be used to initialize context with logged-in information obtained via [`method: BrowserContext.storageState`].
@ -1315,7 +1344,7 @@ index 4c4937004..ea6212dc9 100644
## csharp-java-context-option-storage-state
* langs: csharp, java
- `storageState` <[string]>
@@ -277,7 +300,7 @@ Populates context with given storage state. This option can be used to initializ
@@ -291,7 +314,7 @@ Populates context with given storage state. This option can be used to initializ
obtained via [`method: BrowserContext.storageState`].
## csharp-java-context-option-storage-state-path
@ -1324,7 +1353,7 @@ index 4c4937004..ea6212dc9 100644
- `storageStatePath` <[path]>
Populates context with given storage state. This option can be used to initialize context with logged-in information
@@ -374,7 +397,7 @@ Query parameters to be sent with the URL.
@@ -388,7 +411,7 @@ Query parameters to be sent with the URL.
Query parameters to be sent with the URL.
## csharp-fetch-option-params
@ -1333,7 +1362,7 @@ index 4c4937004..ea6212dc9 100644
- `params` <[Object]<[string], [Serializable]>>
Query parameters to be sent with the URL.
@@ -392,19 +415,19 @@ Query parameters to be sent with the URL.
@@ -406,19 +429,19 @@ Query parameters to be sent with the URL.
Optional request parameters.
## js-python-csharp-fetch-option-headers
@ -1356,7 +1385,7 @@ index 4c4937004..ea6212dc9 100644
- `failOnStatusCode` <[boolean]>
Whether to throw on response codes other than 2xx and 3xx. By default response object is returned
@@ -436,6 +459,14 @@ unless explicitly provided.
@@ -450,6 +473,14 @@ unless explicitly provided.
An instance of [FormData] can be created via [`method: APIRequestContext.createFormData`].
@ -1371,7 +1400,7 @@ index 4c4937004..ea6212dc9 100644
## js-fetch-option-multipart
* langs: js
- `multipart` <[FormData]|[Object]<[string], [string]|[float]|[boolean]|[ReadStream]|[Object]>>
@@ -469,6 +500,15 @@ unless explicitly provided. File values can be passed as file-like object contai
@@ -483,6 +514,15 @@ unless explicitly provided. File values can be passed as file-like object contai
An instance of [FormData] can be created via [`method: APIRequestContext.createFormData`].
@ -1387,7 +1416,7 @@ index 4c4937004..ea6212dc9 100644
## js-python-csharp-fetch-option-data
* langs: js, python, csharp
- `data` <[string]|[Buffer]|[Serializable]>
@@ -477,21 +517,29 @@ Allows to set post data of the request. If the data parameter is an object, it w
@@ -491,21 +531,29 @@ Allows to set post data of the request. If the data parameter is an object, it w
and `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` header will be
set to `application/octet-stream` if not explicitly set.
@ -1420,7 +1449,7 @@ index 4c4937004..ea6212dc9 100644
- `maxRetries` <[int]>
Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
@@ -533,7 +581,7 @@ Function to be evaluated in the worker context.
@@ -547,7 +595,7 @@ Function to be evaluated in the worker context.
Function to be evaluated in the main Electron process.
## python-context-option-viewport
@ -1429,7 +1458,7 @@ index 4c4937004..ea6212dc9 100644
- `viewport` <[null]|[Object]>
- `width` <[int]> page width in pixels.
- `height` <[int]> page height in pixels.
@@ -541,7 +589,7 @@ Function to be evaluated in the main Electron process.
@@ -555,7 +603,7 @@ Function to be evaluated in the main Electron process.
Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `no_viewport` disables the fixed viewport. Learn more about [viewport emulation](../emulation.md#viewport).
## python-context-option-no-viewport
@ -1438,7 +1467,7 @@ index 4c4937004..ea6212dc9 100644
- `noViewport` <[boolean]>
Does not enforce fixed viewport, allows resizing window in the headed mode.
@@ -649,6 +697,13 @@ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CS
@@ -665,6 +713,13 @@ Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CS
Emulates [prefers-colors-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) media feature, supported values are `'light'` and `'dark'`. See
[`method: Page.emulateMedia`] for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'light'`.
@ -1452,7 +1481,7 @@ index 4c4937004..ea6212dc9 100644
## context-option-reducedMotion
* langs: js, java
- `reducedMotion` <null|[ReducedMotion]<"reduce"|"no-preference">>
@@ -661,6 +716,12 @@ Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce
@@ -677,6 +732,12 @@ Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce
Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce'`, `'no-preference'`. See [`method: Page.emulateMedia`] for more details. Passing `'null'` resets emulation to system defaults. Defaults to `'no-preference'`.
@ -1465,7 +1494,7 @@ index 4c4937004..ea6212dc9 100644
## context-option-forcedColors
* langs: js, java
- `forcedColors` <null|[ForcedColors]<"active"|"none">>
@@ -668,10 +729,10 @@ Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce
@@ -684,10 +745,10 @@ Emulates `'prefers-reduced-motion'` media feature, supported values are `'reduce
Emulates `'forced-colors'` media feature, supported values are `'active'`, `'none'`. See [`method: Page.emulateMedia`] for more details. Passing `null` resets emulation to system defaults. Defaults to `'none'`.
## context-option-forcedColors-csharp-python
@ -1479,7 +1508,7 @@ index 4c4937004..ea6212dc9 100644
## context-option-contrast
* langs: js, java
@@ -718,7 +779,7 @@ specified, the HAR is not recorded. Make sure to await [`method: BrowserContext.
@@ -735,7 +796,7 @@ specified, the HAR is not recorded. Make sure to await [`method: BrowserContext.
saved.
## context-option-recordhar-path
@ -1488,7 +1517,7 @@ index 4c4937004..ea6212dc9 100644
- alias-python: record_har_path
- `recordHarPath` <[path]>
@@ -727,33 +788,33 @@ specified HAR file on the filesystem. If not specified, the HAR is not recorded.
@@ -744,33 +805,33 @@ specified HAR file on the filesystem. If not specified, the HAR is not recorded.
call [`method: BrowserContext.close`] for the HAR to be saved.
## context-option-recordhar-omit-content
@ -1527,7 +1556,7 @@ index 4c4937004..ea6212dc9 100644
- `recordVideo` <[Object]>
- `dir` <[path]> Path to the directory to put videos into.
- `size` ?<[Object]> Optional dimensions of the recorded videos. If not specified the size will be equal to `viewport`
@@ -820,7 +881,7 @@ Specifies whether to wait for already running listeners and what to do if they t
@@ -837,7 +898,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
@ -1536,7 +1565,7 @@ index 4c4937004..ea6212dc9 100644
* since: v1.41
- `behavior` <[UnrouteBehavior]<"wait"|"ignoreErrors"|"default">>
@@ -831,7 +892,7 @@ Specifies whether to wait for already running handlers and what to do if they th
@@ -848,7 +909,7 @@ Specifies whether to wait for already running handlers and what to do if they th
## select-options-values
@ -1545,7 +1574,7 @@ index 4c4937004..ea6212dc9 100644
- `values` <[null]|[string]|[ElementHandle]|[Array]<[string]>|[Object]|[Array]<[ElementHandle]>|[Array]<[Object]>>
- `value` ?<[string]> Matches by `option.value`. Optional.
- `label` ?<[string]> Matches by `option.label`. Optional.
@@ -849,7 +910,7 @@ the parameter is a string without wildcard characters, the method will wait for
@@ -866,7 +927,7 @@ the parameter is a string without wildcard characters, the method will wait for
equal to the string.
## wait-for-event-event
@ -1554,7 +1583,7 @@ index 4c4937004..ea6212dc9 100644
- `event` <[string]>
Event name, same one typically passed into `*.on(event)`.
@@ -907,7 +968,7 @@ only the first option matching one of the passed options is selected. Optional.
@@ -924,7 +985,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
@ -1563,7 +1592,7 @@ index 4c4937004..ea6212dc9 100644
- `timeout` <[float]>
Maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
@@ -927,7 +988,7 @@ using the [`method: AndroidDevice.setDefaultTimeout`] method.
@@ -944,7 +1005,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
@ -1572,7 +1601,7 @@ index 4c4937004..ea6212dc9 100644
- `timeout` <[float]>
Time to retry the assertion for in milliseconds. Defaults to `5000`.
@@ -981,8 +1042,10 @@ between the same pixel in compared images, between zero (strict) and one (lax),
@@ -998,8 +1059,10 @@ between the same pixel in compared images, between zero (strict) and one (lax),
- %%-context-option-httpcredentials-%%
- %%-context-option-colorscheme-%%
- %%-context-option-colorscheme-csharp-python-%%
@ -1583,8 +1612,8 @@ index 4c4937004..ea6212dc9 100644
- %%-context-option-forcedColors-%%
- %%-context-option-forcedColors-csharp-python-%%
- %%-context-option-contrast-%%
@@ -1072,7 +1135,7 @@ Firefox user preferences. Learn more about the Firefox user preferences at
[`about:config`](https://support.mozilla.org/en-US/kb/about-config-editor-firefox).
@@ -1091,7 +1154,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
-* langs: csharp, java

@ -1 +1 @@
Subproject commit 471930b1ceae03c9e66e0eb80c1364a1a788e7db
Subproject commit 80581972582c9565e141c5fedd3c5fa10cc0e38b

View File

@ -30,11 +30,26 @@ func (p *Playwright) Stop() error {
return p.connection.Stop()
}
// Pid returns the process ID of the Playwright driver process, or 0 if not available
func (p *Playwright) Pid() int {
if pt, ok := p.connection.transport.(*pipeTransport); ok {
if pt.process != nil {
return pt.process.Pid
}
}
return 0
}
func (p *Playwright) setSelectors(selectors Selectors) {
selectorsOwner := fromChannel(p.initializer["selectors"]).(*selectorsOwnerImpl)
p.Selectors.(*selectorsImpl).removeChannel(selectorsOwner)
p.Selectors = selectors
p.Selectors.(*selectorsImpl).addChannel(selectorsOwner)
// Selectors has been moved to client-side only in Playwright v1.57+
if p.initializer["selectors"] != nil {
selectorsOwner := fromChannel(p.initializer["selectors"]).(*selectorsOwnerImpl)
p.Selectors.(*selectorsImpl).removeChannel(selectorsOwner)
p.Selectors = selectors
p.Selectors.(*selectorsImpl).addChannel(selectorsOwner)
} else {
p.Selectors = selectors
}
}
func newPlaywright(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *Playwright {
@ -50,10 +65,14 @@ func newPlaywright(parent *channelOwner, objectType string, guid string, initial
pw.Chromium.(*browserTypeImpl).playwright = pw
pw.Firefox.(*browserTypeImpl).playwright = pw
pw.WebKit.(*browserTypeImpl).playwright = pw
selectorsOwner := fromChannel(initializer["selectors"]).(*selectorsOwnerImpl)
pw.Selectors.(*selectorsImpl).addChannel(selectorsOwner)
pw.connection.afterClose = func() {
pw.Selectors.(*selectorsImpl).removeChannel(selectorsOwner)
// Selectors has been moved to client-side only in Playwright v1.57+
// Only set up channel if selectors is in the initializer (older protocol)
if initializer["selectors"] != nil {
selectorsOwner := fromChannel(initializer["selectors"]).(*selectorsOwnerImpl)
pw.Selectors.(*selectorsImpl).addChannel(selectorsOwner)
pw.connection.afterClose = func() {
pw.Selectors.(*selectorsImpl).removeChannel(selectorsOwner)
}
}
if pw.connection.localUtils != nil {
pw.Devices = pw.connection.localUtils.Devices

2
run.go
View File

@ -16,7 +16,7 @@ import (
"strings"
)
const playwrightCliVersion = "1.52.0"
const playwrightCliVersion = "1.57.0"
var (
logger = slog.Default()

View File

@ -11,7 +11,6 @@ import (
"sync"
"testing"
"github.com/mitchellh/go-ps"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -150,7 +149,12 @@ func TestShouldNotHangWhenPlaywrightUnexpectedExit(t *testing.T) {
context, err := browser.NewContext()
require.NoError(t, err)
err = killPlaywrightProcess()
// Get the process ID directly from Playwright
pid := pw.Pid()
require.NotZero(t, pid, "Playwright process PID should not be zero")
// Kill the process
err = killProcessByPid(pid)
require.NoError(t, err)
_, err = context.NewPage()
@ -172,25 +176,6 @@ func TestGetNodeExecutable(t *testing.T) {
assert.Contains(t, executable, "testDirectory")
}
// find and kill playwright process
func killPlaywrightProcess() error {
all, err := ps.Processes()
if err != nil {
return err
}
for _, process := range all {
if process.Executable() == "node" || process.Executable() == "node.exe" {
if process.PPid() == os.Getpid() {
if err := killProcessByPid(process.Pid()); err != nil {
return err
}
return nil
}
}
}
return fmt.Errorf("playwright process not found")
}
func killProcessByPid(pid int) error {
process, err := os.FindProcess(pid)
if err != nil {

View File

@ -23,7 +23,8 @@ func newSelectorsOwner(parent *channelOwner, objectType string, guid string, ini
}
type selectorsImpl struct {
channels sync.Map
mu sync.RWMutex // protects registrations slice
contexts sync.Map // map of BrowserContext channels
registrations []map[string]interface{}
}
@ -41,48 +42,82 @@ func (s *selectorsImpl) Register(name string, script Script, options ...Selector
} else {
source = *script.Content
}
params := map[string]interface{}{
selectorEngine := map[string]interface{}{
"name": name,
"source": source,
}
if len(options) == 1 && options[0].ContentScript != nil {
params["contentScript"] = *options[0].ContentScript
selectorEngine["contentScript"] = *options[0].ContentScript
}
var err error
s.channels.Range(func(key, value any) bool {
_, err = value.(*selectorsOwnerImpl).channel.Send("register", params)
return err == nil
params := map[string]interface{}{
"selectorEngine": selectorEngine,
}
// Register with all active contexts, ignoring contexts that have been closed
s.contexts.Range(func(key, value any) bool {
_, _ = value.(*browserContextImpl).channel.Send("registerSelectorEngine", params)
// Continue to next context even if this one failed (e.g., context closed)
return true
})
if err != nil {
return err
}
s.registrations = append(s.registrations, params)
s.mu.Lock()
s.registrations = append(s.registrations, selectorEngine)
s.mu.Unlock()
return nil
}
func (s *selectorsImpl) SetTestIdAttribute(name string) {
setTestIdAttributeName(name)
s.channels.Range(func(key, value any) bool {
value.(*selectorsOwnerImpl).setTestIdAttributeName(name)
s.contexts.Range(func(key, value any) bool {
value.(*browserContextImpl).channel.SendNoReply("setTestIdAttributeName", map[string]interface{}{
"testIdAttributeName": name,
})
return true
})
}
func (s *selectorsImpl) addChannel(channel *selectorsOwnerImpl) {
s.channels.Store(channel.guid, channel)
for _, params := range s.registrations {
channel.channel.SendNoReply("register", params)
// Legacy support for older Playwright versions with server-side selectors
s.contexts.Store(channel.guid, channel)
s.mu.RLock()
for _, selectorEngine := range s.registrations {
params := map[string]interface{}{
"selectorEngine": selectorEngine,
}
channel.channel.SendNoReply("registerSelectorEngine", params)
channel.setTestIdAttributeName(getTestIdAttributeName())
}
s.mu.RUnlock()
}
func (s *selectorsImpl) removeChannel(channel *selectorsOwnerImpl) {
s.channels.Delete(channel.guid)
// Legacy support for older Playwright versions with server-side selectors
s.contexts.Delete(channel.guid)
}
func (s *selectorsImpl) addContext(context *browserContextImpl) {
s.contexts.Store(context.guid, context)
s.mu.RLock()
for _, selectorEngine := range s.registrations {
params := map[string]interface{}{
"selectorEngine": selectorEngine,
}
context.channel.SendNoReply("registerSelectorEngine", params)
}
s.mu.RUnlock()
testIdAttr := getTestIdAttributeName()
if testIdAttr != "" {
context.channel.SendNoReply("setTestIdAttributeName", map[string]interface{}{
"testIdAttributeName": testIdAttr,
})
}
}
func (s *selectorsImpl) removeContext(context *browserContextImpl) {
s.contexts.Delete(context.guid)
}
func newSelectorsImpl() *selectorsImpl {
return &selectorsImpl{
channels: sync.Map{},
contexts: sync.Map{},
registrations: make([]map[string]interface{}, 0),
}
}

View File

@ -179,15 +179,15 @@ func TestBrowserContextAddCookies(t *testing.T) {
require.Equal(t, []playwright.Cookie{
{
Name: "password",
Value: "123456",
Domain: "127.0.0.1",
Path: "/",
Expires: -1,
HttpOnly: false,
Secure: false,
SameSite: sameSite,
Name: "password",
Value: "123456",
Domain: "127.0.0.1",
Path: "/",
Expires: -1,
HttpOnly: false,
Secure: false,
SameSite: sameSite,
PartitionKey: playwright.String(""),
},
}, cookies)
@ -281,58 +281,8 @@ func TestBrowserContextUnrouteShouldWork(t *testing.T) {
}
func TestBrowserContextShouldReturnBackgroundPage(t *testing.T) {
BeforeEach(t)
if !isChromium {
t.Skip()
}
if runtime.GOOS == "windows" {
t.Skip("flaky on windows")
}
extensionPath := Asset("simple-extension")
context, err := browserType.LaunchPersistentContext(
t.TempDir(),
playwright.BrowserTypeLaunchPersistentContextOptions{
Headless: playwright.Bool(false),
Args: []string{
fmt.Sprintf("--disable-extensions-except=%s", extensionPath),
fmt.Sprintf("--load-extension=%s", extensionPath),
},
},
)
require.NoError(t, err)
var page playwright.Page
if len(context.BackgroundPages()) == 1 {
page = context.BackgroundPages()[0]
} else {
ret, err := context.WaitForEvent("backgroundPage", playwright.BrowserContextWaitForEventOptions{
Timeout: playwright.Float(1000),
})
if err != nil {
// probably missing event
if len(context.BackgroundPages()) == 1 {
page = context.BackgroundPages()[0]
} else {
t.Fatal(err)
}
} else {
page = ret.(playwright.Page)
}
}
require.NotNil(t, page)
contains := func(pages []playwright.Page, page playwright.Page) bool {
for _, p := range pages {
if p == page {
return true
}
}
return false
}
require.False(t, contains(context.Pages(), page))
require.True(t, contains(context.BackgroundPages(), page))
context.Close()
require.Len(t, context.BackgroundPages(), 0)
require.Len(t, context.Pages(), 0)
// Background pages have been removed from Chromium together with Manifest V2 extensions
t.Skip("Background pages are deprecated - Manifest V2 extensions no longer supported in Chromium")
}
func TestPageEventShouldHaveURL(t *testing.T) {

View File

@ -139,7 +139,13 @@ func TestConsoleShouldTriggerCorrectLog(t *testing.T) {
_, err = page.Evaluate("url => fetch(url).catch(e => {})", server.EMPTY_PAGE)
require.NoError(t, err)
message := <-messages
require.Contains(t, message.Text(), "Access-Control-Allow-Origin")
headerString := "Access-Control-Allow-Origin"
corsString := "CORS"
require.Condition(t, func() bool {
return strings.Contains(message.Text(), headerString) || strings.Contains(message.Text(), corsString)
}, "The text should contain either '%s' or '%s'", headerString, corsString)
require.Equal(t, "error", message.Type())
}

View File

@ -128,7 +128,7 @@ func TestShoulSupportGlobalTimeoutOption(t *testing.T) {
time.Sleep(200 * time.Millisecond)
})
_, err = request.Get(server.PREFIX + "/empty.html")
require.Contains(t, err.Error(), `Request timed out after`)
require.ErrorContains(t, err, `Timeout 100ms exceeded`)
}
func TestShouldPropagateExtraHttpHeadersWithRedirects(t *testing.T) {
@ -261,7 +261,7 @@ func TestStorageStateShouldRoundTripThroughFile(t *testing.T) {
BeforeEach(t)
storageState := &playwright.StorageState{
Cookies: []playwright.Cookie{
Cookies: []playwright.StorageStateCookie{
{
Name: "a",
Value: "b",

View File

@ -721,3 +721,82 @@ func TestLocatorShouldSupportFilterVisible(t *testing.T) {
Visible: playwright.Bool(false),
}).GetByText("data1")).ToHaveText("Hidden data1"))
}
// TestLocatorDescribe verifies that Locator.Describe() sets a description
// Based on upstream test: playwright/tests/page/locator-convenience.spec.ts
func TestLocatorDescribe(t *testing.T) {
BeforeEach(t)
require.NoError(t, page.SetContent(`<button>Submit</button>`))
locator := page.Locator("button")
// Locator without description should return empty string
desc, err := locator.Description()
require.NoError(t, err)
require.Empty(t, desc, "description should be empty for locator without description")
// Set description
describedLocator := locator.Describe("Submit button")
desc, err = describedLocator.Description()
require.NoError(t, err)
require.Equal(t, "Submit button", desc)
// Original locator should still have no description
desc, err = locator.Description()
require.NoError(t, err)
require.Empty(t, desc, "original locator should remain unchanged")
}
// TestLocatorDescribeSpecialCharacters verifies descriptions with special characters
func TestLocatorDescribeSpecialCharacters(t *testing.T) {
BeforeEach(t)
require.NoError(t, page.SetContent(`<div>Test</div>`))
locator := page.Locator("div").Describe(`Button with "quotes" and 'apostrophes'`)
desc, err := locator.Description()
require.NoError(t, err)
require.Equal(t, `Button with "quotes" and 'apostrophes'`, desc)
}
// TestLocatorDescribeChained verifies descriptions work with chained locators
func TestLocatorDescribeChained(t *testing.T) {
BeforeEach(t)
require.NoError(t, page.SetContent(`
<form>
<input type="text" />
</form>
`))
locator := page.Locator("form").Locator("input").Describe("Form input field")
desc, err := locator.Description()
require.NoError(t, err)
require.Equal(t, "Form input field", desc)
}
// TestLocatorDescribeMultipleCalls verifies multiple describe calls override each other
func TestLocatorDescribeMultipleCalls(t *testing.T) {
BeforeEach(t)
require.NoError(t, page.SetContent(`<button><span>Click me</span></button>`))
// First description
locator1 := page.Locator("button").Describe("First description")
desc, err := locator1.Description()
require.NoError(t, err)
require.Equal(t, "First description", desc)
// Second description on chained locator
locator2 := locator1.Locator("span").Describe("Second description")
desc, err = locator2.Description()
require.NoError(t, err)
require.Equal(t, "Second description", desc)
// Chained locator without describe should have no description
locator3 := locator2.Locator("span")
desc, err = locator3.Description()
require.NoError(t, err)
require.Empty(t, desc, "chained locator without describe should have empty description")
}

View File

@ -100,20 +100,7 @@ func TestShouldSnapshotComplex(t *testing.T) {
}
func TestShouldSnapshotWithRef(t *testing.T) {
BeforeEach(t)
require.NoError(t, page.SetContent(`<ul><li><a href="about:blank">link</a></li></ul>`))
expected := Unshift(`
- list [ref=s1e3]:
- listitem [ref=s1e4]:
- link "link" [ref=s1e5]:
- /url: about:blank
`)
ariaSnapshot, err := page.Locator("body").AriaSnapshot(playwright.LocatorAriaSnapshotOptions{
Ref: playwright.Bool(true),
})
require.NoError(t, err)
require.Equal(t, expected, ariaSnapshot)
t.Skip("the Ref option was removed in Playwright v1.53")
}
func TestShouldSnapshotWithUnexpectedChildrenEqual(t *testing.T) {

View File

@ -332,11 +332,16 @@ func TestPageClockStubTimers(t *testing.T) {
beforePageClock(t, 0, 1000)
// Set up a signal to ensure the async function has started
_, err := page.Evaluate(`window.timeoutStarted = false`)
require.NoError(t, err)
chanRet := make(chan interface{}, 1)
go func() {
ret, err := page.Evaluate(`
async () => {
const prev = performance.now();
window.timeoutStarted = true;
await new Promise(f => setTimeout(f, 1000));
const next = performance.now();
return { prev, next };
@ -347,6 +352,13 @@ func TestPageClockStubTimers(t *testing.T) {
close(chanRet)
}
}()
// Wait for the async function to start and set up the setTimeout
require.Eventually(t, func() bool {
started, _ := page.Evaluate(`window.timeoutStarted`)
return started == true
}, 5*time.Second, 50*time.Millisecond)
require.NoError(t, page.Clock().RunFor(1000))
ret := <-chanRet
require.Equal(t, map[string]interface{}{
@ -372,11 +384,16 @@ func TestPageClockStubTimersPerformance(t *testing.T) {
beforePageClock(t, 1000, 2000)
// Set up a signal to ensure the async function has started
_, err := page.Evaluate(`window.timeoutStarted = false`)
require.NoError(t, err)
chanRet := make(chan interface{}, 1)
go func() {
ret, err := page.Evaluate(`
async () => {
const prev = performance.now();
window.timeoutStarted = true;
await new Promise(f => setTimeout(f, 1000));
const next = performance.now();
return { prev, next };
@ -387,6 +404,13 @@ func TestPageClockStubTimersPerformance(t *testing.T) {
close(chanRet)
}
}()
// Wait for the async function to start and set up the setTimeout
require.Eventually(t, func() bool {
started, _ := page.Evaluate(`window.timeoutStarted`)
return started == true
}, 5*time.Second, 50*time.Millisecond)
require.NoError(t, page.Clock().RunFor(1000))
origin, err := page.Evaluate(`performance.timeOrigin`)
require.NoError(t, err)

View File

@ -44,14 +44,14 @@ func TestPageSetContent(t *testing.T) {
func TestPageSetContentShouldRespectDefaultNavigationTimeout(t *testing.T) {
BeforeEach(t)
page.SetDefaultNavigationTimeout(5)
imgPath := "/img/png"
page.SetDefaultNavigationTimeout(1)
imgPath := "/img.png"
// stall for image
require.NoError(t, page.Route(imgPath, func(r playwright.Route) {}))
server.SetRoute(imgPath, func(w http.ResponseWriter, r *http.Request) {})
err := page.SetContent(fmt.Sprintf(`<img src="%s"></img>`, server.PREFIX+imgPath))
require.ErrorIs(t, err, playwright.ErrTimeout)
require.ErrorContains(t, err, "Timeout 5ms exceeded.")
require.ErrorContains(t, err, "Timeout 1ms exceeded.")
}
func TestPageScreenshot(t *testing.T) {
@ -1219,11 +1219,10 @@ func TestPageGotoShouldFailWhenExceedingBrowserContextNavigationTimeout(t *testi
// Hang for request to the empty.html
server.SetRoute("/empty.html", func(w http.ResponseWriter, r *http.Request) {})
context.SetDefaultNavigationTimeout(5)
defer context.SetDefaultNavigationTimeout(30 * 1000) // reset
context.SetDefaultNavigationTimeout(2)
_, err := page.Goto(server.EMPTY_PAGE)
require.ErrorIs(t, err, playwright.ErrTimeout)
require.ErrorContains(t, err, "Timeout 5ms exceeded.")
require.ErrorContains(t, err, "Timeout 2ms exceeded.")
require.ErrorContains(t, err, "/empty.html")
}
@ -1264,3 +1263,129 @@ func TestShouldEmulateContrast(t *testing.T) {
require.NoError(t, err)
require.True(t, ret.(bool))
}
// TestPageConsoleMessages verifies that Page.ConsoleMessages() returns accumulated console messages
// Based on upstream test: playwright/tests/page/page-event-console.spec.ts
func TestPageConsoleMessages(t *testing.T) {
BeforeEach(t)
// Generate 301 console messages (should keep last 100)
_, err := page.Evaluate(`() => {
for (let i = 0; i < 301; i++) {
console.log('message' + i);
}
}`)
require.NoError(t, err)
messages, err := page.ConsoleMessages()
require.NoError(t, err)
// Should return at least 100 messages (the buffer limit)
require.GreaterOrEqual(t, len(messages), 100, "should be at least 100 messages")
// Verify the last 100 messages are correct (message201 to message300)
expectedStart := 301 - len(messages)
for i, msg := range messages {
expectedText := fmt.Sprintf("message%d", expectedStart+i)
require.Equal(t, expectedText, msg.Text())
require.Equal(t, "log", msg.Type())
// Note: Page() may be nil for console messages retrieved from the buffer
// as they don't include full channel references like live events do
}
}
// TestPageConsoleMessagesEmpty verifies that ConsoleMessages() returns empty array for new page
func TestPageConsoleMessagesEmpty(t *testing.T) {
BeforeEach(t)
messages, err := page.ConsoleMessages()
require.NoError(t, err)
require.Empty(t, messages)
}
// TestPageConsoleMessagesTypes verifies different console message types are captured
func TestPageConsoleMessagesTypes(t *testing.T) {
BeforeEach(t)
_, err := page.Evaluate(`() => {
console.log('log message');
console.warn('warn message');
console.error('error message');
console.info('info message');
}`)
require.NoError(t, err)
messages, err := page.ConsoleMessages()
require.NoError(t, err)
require.Len(t, messages, 4)
expectedTypes := []string{"log", "warning", "error", "info"}
for i, msg := range messages {
require.Equal(t, expectedTypes[i], msg.Type())
}
}
// TestPageRequests verifies that Page.Requests() returns accumulated requests
// Based on upstream test: playwright/tests/page/page-event-request.spec.ts
func TestPageRequests(t *testing.T) {
BeforeEach(t)
// Navigate to a page (creates initial request)
_, err := page.Goto(server.EMPTY_PAGE)
require.NoError(t, err)
// Create multiple fetch requests
for i := 0; i < 99; i++ {
path := fmt.Sprintf("/fetch%d", i)
server.SetRoute(path, func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("response"))
require.NoError(t, err)
})
}
// Make 99 fetch requests
for i := 0; i < 99; i++ {
url := fmt.Sprintf("%s/fetch%d", server.PREFIX, i)
_, err := page.Evaluate(`url => fetch(url)`, url)
require.NoError(t, err)
}
requests, err := page.Requests()
require.NoError(t, err)
// Should have at least 100 requests (navigation + 99 fetches, buffer limit is 100)
require.GreaterOrEqual(t, len(requests), 99, "should capture fetch requests")
// Verify requests are functional
for _, req := range requests {
require.NotEmpty(t, req.URL())
require.NotEmpty(t, req.Method())
}
}
// TestPageRequestsEmpty verifies that Requests() returns empty array for new page
func TestPageRequestsEmpty(t *testing.T) {
BeforeEach(t)
requests, err := page.Requests()
require.NoError(t, err)
require.Empty(t, requests)
}
// TestPageRequestsWithNavigation verifies navigation requests are included
func TestPageRequestsWithNavigation(t *testing.T) {
BeforeEach(t)
_, err := page.Goto(server.EMPTY_PAGE)
require.NoError(t, err)
requests, err := page.Requests()
require.NoError(t, err)
require.Len(t, requests, 1, "should capture navigation request")
req := requests[0]
require.Equal(t, server.EMPTY_PAGE, req.URL())
require.Equal(t, "GET", req.Method())
require.Equal(t, "document", req.ResourceType())
}

View File

@ -20,7 +20,7 @@ func newRemoteServer() (*remoteServer, error) {
if err != nil {
return nil, fmt.Errorf("could not start Playwright: %v", err)
}
cmd := driver.Command("launch-server", "--browser", browserName)
cmd := driver.Command("run-server")
cmd.Stderr = os.Stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
@ -31,11 +31,17 @@ func newRemoteServer() (*remoteServer, error) {
return nil, fmt.Errorf("could not start server: %v", err)
}
scanner := bufio.NewReader(stdout)
url, err := scanner.ReadString('\n')
url = strings.TrimRight(url, "\n")
line, err := scanner.ReadString('\n')
if err != nil {
return nil, fmt.Errorf("could not read url: %v", err)
}
line = strings.TrimSpace(line)
// Remove "Listening on " prefix
const prefix = "Listening on "
if !strings.HasPrefix(line, prefix) {
return nil, fmt.Errorf("unexpected output format: %s", line)
}
url := strings.TrimPrefix(line, prefix)
return &remoteServer{
url: url,
cmd: cmd,

View File

@ -2,39 +2,36 @@
package playwright_test
import (
"fmt"
"testing"
"time"
"github.com/playwright-community/playwright-go"
"github.com/stretchr/testify/require"
)
func TestSelectorsRegisterShouldWork(t *testing.T) {
BeforeEach(t)
tagSelector := `
{
create(root, target) {
return target.nodeName;
},
tagSelector := `(() => ({
query(root, selector) {
return root.querySelector(selector);
},
queryAll(root, selector) {
return Array.from(root.querySelectorAll(selector));
}
}
`
selectorName := "tag_" + browserName
selector2Name := "tag2_" + browserName
}))()`
// Use unique names to avoid conflicts when running tests multiple times
uniqueSuffix := fmt.Sprintf("%s_%d", t.Name(), time.Now().UnixNano())
selectorName := "tag_" + browserName + "_" + uniqueSuffix
selector2Name := "tag2_" + browserName + "_" + uniqueSuffix
err := pw.Selectors.Register(selectorName, playwright.Script{})
require.ErrorContains(t, err, `Either source or path should be specified`)
// Register one engine before creating context.
err = pw.Selectors.Register(selectorName, playwright.Script{
err := pw.Selectors.Register(selectorName, playwright.Script{
Content: &tagSelector,
})
require.NoError(t, err)
BeforeEach(t)
// Register another engine after creating context.
err = pw.Selectors.Register(selector2Name, playwright.Script{
Content: &tagSelector,
@ -49,7 +46,7 @@ func TestSelectorsRegisterShouldWork(t *testing.T) {
ret, err = page.EvalOnSelector(selectorName+"=SPAN", `e => e.nodeName`, nil)
require.NoError(t, err)
require.Equal(t, "SPAN", ret)
ret, err = page.EvalOnSelectorAll(selectorName+"=DIV", `es => es.length`, nil)
ret, err = page.EvalOnSelectorAll(selectorName+"=DIV", `es => es.length`)
require.NoError(t, err)
require.Equal(t, 2, ret)
@ -64,7 +61,7 @@ func TestSelectorsRegisterShouldWork(t *testing.T) {
require.Equal(t, 2, ret)
// Selector names are case-sensitive.
_, err = page.QuerySelector("tAG=DIV")
_, err = page.Locator("tAG=DIV").All()
require.ErrorContains(t, err, `Unknown engine "tAG" while parsing selector tAG=DIV`)
require.NoError(t, context.Close())
@ -101,12 +98,14 @@ func TestSelectorsShouldUseDataTestIdInStrictErrors(t *testing.T) {
func TestSelectorsShouldWorkWithPath(t *testing.T) {
BeforeEach(t)
require.NoError(t, pw.Selectors.Register("foo", playwright.Script{
// Use unique name to avoid conflicts when running tests multiple times
selectorName := fmt.Sprintf("foo_%s_%d", t.Name(), time.Now().UnixNano())
require.NoError(t, pw.Selectors.Register(selectorName, playwright.Script{
Path: playwright.String(Asset("sectionselectorengine.js")),
}))
require.NoError(t, page.SetContent(`<section></section>`))
ret, err := page.EvalOnSelector("foo=whatever", `e => e.nodeName`, nil)
ret, err := page.EvalOnSelector(selectorName+"=whatever", `e => e.nodeName`, nil)
require.NoError(t, err)
require.Equal(t, "SECTION", ret)
}

View File

@ -7,6 +7,7 @@ import (
"io"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/playwright-community/playwright-go"
@ -163,6 +164,59 @@ func TestShouldShowTracingGroupInActionList(t *testing.T) {
}, actions)
}
// mapInternalAPIToPublic maps internal Playwright class.method names to public API names
func mapInternalAPIToPublic(class, method string) string {
// Map internal classes to public API classes
classMethodKey := class + "." + method
// Common Frame methods that should map to Page
frameToPageMethods := map[string]bool{
"goto": true, "reload": true, "goBack": true, "goForward": true,
"setContent": true, "waitForNavigation": true, "waitForURL": true,
"waitForLoadState": true, "screenshot": true, "pdf": true,
"close": true, "pause": true,
}
// Frame selector/locator methods that should map to Locator
frameLocatorMethods := map[string]bool{
"click": true, "dblclick": true, "fill": true, "press": true,
"type": true, "hover": true, "check": true, "uncheck": true,
"selectOption": true, "setInputFiles": true, "focus": true,
"blur": true, "tap": true, "dispatchEvent": true, "evaluate": true,
"isVisible": true, "isHidden": true, "isEnabled": true, "isDisabled": true,
"isChecked": true, "isEditable": true, "textContent": true,
"innerText": true, "innerHTML": true, "getAttribute": true,
}
if class == "Frame" {
if frameToPageMethods[method] {
class = "Page"
} else if frameLocatorMethods[method] {
class = "Locator"
}
}
// Special case mappings
specialMappings := map[string]string{
"BrowserContext.newPage": "BrowserContext.NewPage",
"Page.waitForTimeout": "Page.WaitForTimeout",
}
// Convert method to title case (first letter uppercase)
titleCaseMethod := strings.ToUpper(method[:1]) + method[1:]
apiName := class + "." + titleCaseMethod
// Check for special mappings
if mapped, ok := specialMappings[classMethodKey]; ok {
return mapped
}
if mapped, ok := specialMappings[apiName]; ok {
return mapped
}
return apiName
}
func parseTrace(t *testing.T, tracePath string) (files map[string][]byte, events []interface{}) {
t.Helper()
// read and unzip trace
@ -193,9 +247,23 @@ func parseTrace(t *testing.T, tracePath string) (files map[string][]byte, events
var event map[string]interface{}
err := json.Unmarshal(line, &event)
require.NoError(t, err)
switch event["type"].(string) {
eventType, _ := event["type"].(string)
switch eventType {
case "before":
event["type"] = "action"
// Compute apiName from class and method for regular actions
// For tracing groups, use the title field
class, _ := event["class"].(string)
method, _ := event["method"].(string)
title, hasTitle := event["title"].(string)
if method == "tracingGroup" && hasTitle {
event["apiName"] = title
} else if class != "" && method != "" {
event["apiName"] = mapInternalAPIToPublic(class, method)
}
actionMap[event["callId"].(string)] = event
events = append(events, event)
case "input":
@ -233,7 +301,9 @@ func getTraceActions(events []interface{}) []string {
})
for _, e := range actionEvents {
event := e.(map[string]interface{})
actions = append(actions, event["apiName"].(string))
if apiName, ok := event["apiName"].(string); ok {
actions = append(actions, apiName)
}
}
return actions
}

View File

@ -210,7 +210,7 @@ func TestPageUnrouteShouldNotWaitForPendingHandlersToComplete(t *testing.T) {
secondHandlerCalled := false
require.NoError(t, context.Route(regexp.MustCompile(".*"), func(route playwright.Route) {
require.NoError(t, page.Route(regexp.MustCompile(".*"), func(route playwright.Route) {
secondHandlerCalled = true
require.NoError(t, route.Continue())
}))

View File

@ -147,3 +147,73 @@ func TestWorkerShouldClearUponCrossProcessNavigation(t *testing.T) {
require.True(t, destroyed)
require.Equal(t, 0, len(page.Workers()))
}
// TestConsoleMessageWorker verifies that ConsoleMessage.Worker() returns the worker
// Based on upstream test: playwright/tests/page/workers.spec.ts
func TestConsoleMessageWorker(t *testing.T) {
BeforeEach(t)
// Create a worker and capture console message from it
workerChan := make(chan playwright.Worker, 1)
page.Once("worker", func(worker playwright.Worker) {
workerChan <- worker
})
consoleChan := make(chan playwright.ConsoleMessage, 1)
page.Once("console", func(message playwright.ConsoleMessage) {
consoleChan <- message
})
// Create a worker that logs a message
_, err := page.Evaluate(`() => {
const workerCode = 'console.log("hello from worker")';
const blob = new Blob([workerCode], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
}`)
require.NoError(t, err)
// Wait for worker creation
worker := <-workerChan
require.NotNil(t, worker)
// Wait for console message
message := <-consoleChan
require.NotNil(t, message)
// Verify the message is from the worker
require.Equal(t, "hello from worker", message.Text())
msgWorker, err := message.Worker()
require.NoError(t, err)
require.Equal(t, worker, msgWorker, "console message should reference the worker")
// Worker console messages also have a page reference (they're emitted to both)
msgPage := message.Page()
require.Equal(t, page, msgPage, "worker console messages are also associated with the page")
}
// TestConsoleMessageWorkerNil verifies that page console messages have nil worker
func TestConsoleMessageWorkerNil(t *testing.T) {
BeforeEach(t)
consoleChan := make(chan playwright.ConsoleMessage, 1)
page.Once("console", func(message playwright.ConsoleMessage) {
consoleChan <- message
})
_, err := page.Evaluate(`() => console.log('hello from page')`)
require.NoError(t, err)
message := <-consoleChan
require.NotNil(t, message)
require.Equal(t, "hello from page", message.Text())
// Page console messages should not have a worker
msgWorker, err := message.Worker()
require.NoError(t, err)
require.Nil(t, msgWorker, "page console messages should not have a worker")
// But should have a page
msgPage := message.Page()
require.Equal(t, page, msgPage)
}

View File

@ -21,6 +21,7 @@ type pipeTransport struct {
bufReader *bufio.Reader
closed chan struct{}
onClose func() error
process *os.Process
}
func (t *pipeTransport) Poll() (*message, error) {
@ -137,5 +138,7 @@ func newPipeTransport(driver *PlaywrightDriver, stderr io.Writer) (transport, er
return nil, fmt.Errorf("could not start driver: %w", err)
}
t.process = cmd.Process
return t, nil
}

View File

@ -48,9 +48,9 @@ func IntSlice(v ...int) *[]int {
// ToOptionalStorageState converts StorageState to OptionalStorageState for use directly in [Browser.NewContext]
func (s StorageState) ToOptionalStorageState() *OptionalStorageState {
cookies := make([]OptionalCookie, len(s.Cookies))
cookies := make([]OptionalStorageStateOptionalCookie, len(s.Cookies))
for i, c := range s.Cookies {
cookies[i] = c.ToOptionalCookie()
cookies[i] = c.ToOptionalStorageStateOptionalCookie()
}
return &OptionalStorageState{
Origins: s.Origins,
@ -58,6 +58,19 @@ func (s StorageState) ToOptionalStorageState() *OptionalStorageState {
}
}
func (c StorageStateCookie) ToOptionalStorageStateOptionalCookie() OptionalStorageStateOptionalCookie {
return OptionalStorageStateOptionalCookie{
Name: c.Name,
Value: c.Value,
Domain: String(c.Domain),
Path: String(c.Path),
Expires: Float(c.Expires),
HttpOnly: Bool(c.HttpOnly),
Secure: Bool(c.Secure),
SameSite: c.SameSite,
}
}
func (c Cookie) ToOptionalCookie() OptionalCookie {
return OptionalCookie{
Name: c.Name,

View File

@ -70,9 +70,16 @@ func (w *workerImpl) OnClose(fn func(Worker)) {
w.On("close", fn)
}
func (w *workerImpl) OnConsole(fn func(ConsoleMessage)) {
w.On("console", fn)
}
func newWorker(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *workerImpl {
bt := &workerImpl{}
bt.createChannelOwner(bt, parent, objectType, guid, initializer)
bt.channel.On("close", bt.onClose)
bt.channel.On("console", func(ev map[string]interface{}) {
bt.Emit("console", newConsoleMessage(ev))
})
return bt
}