Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./...`.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
[![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)

Playwright is a Go library to automate [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) with a single API. Playwright is built to enable cross-browser web automation that is **ever-green**, **capable**, **reliable** and **fast**.

| | 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.

Expand Down
3 changes: 3 additions & 0 deletions browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
88 changes: 64 additions & 24 deletions browser_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,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
}
Expand All @@ -104,7 +104,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) {
Expand Down Expand Up @@ -438,7 +438,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 {
Expand Down Expand Up @@ -644,7 +644,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()
Expand All @@ -662,17 +662,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()
Expand Down Expand Up @@ -784,33 +808,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)
Expand All @@ -820,7 +860,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
Expand Down Expand Up @@ -857,15 +897,15 @@ 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 {
page.(*pageImpl).Emit("request", request)
}
})
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)
Expand All @@ -879,7 +919,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))
Expand All @@ -892,7 +932,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 {
Expand Down
26 changes: 23 additions & 3 deletions browser_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -87,22 +95,30 @@ 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
}

func (b *browserTypeImpl) Connect(wsEndpoint string, options ...BrowserTypeConnectOptions) (Browser, error) {
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 {
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 19 additions & 3 deletions channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion channel_owner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading