Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Go
on:
push:
branches: [ main ]
branches: [ main, roll/* ]
pull_request:
branches: [ main ]
branches: [ main, roll/* ]
jobs:
lint:
name: Lint
Expand Down
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-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 -->
[![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-148.0.7778.96-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-150.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.4-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 -->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: |
| Chromium <!-- GEN:chromium-version -->148.0.7778.96<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->26.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->150.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
34 changes: 34 additions & 0 deletions browser.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,20 @@ func (b *browserImpl) Contexts() []BrowserContext {
return b.contexts
}

func (b *browserImpl) Bind(title string, options ...BrowserBindOptions) (*Bind, error) {
overrides := map[string]any{"title": title}
result, err := b.channel.SendReturnAsDict("startServer", options, overrides)
if err != nil {
return nil, err
}
return &Bind{Endpoint: result["endpoint"].(string)}, nil
}

func (b *browserImpl) Unbind() error {
_, err := b.channel.Send("stopServer")
return err
}

func (b *browserImpl) Close(options ...BrowserCloseOptions) (err error) {
if len(options) == 1 {
b.closeReason = options[0].Reason
Expand Down Expand Up @@ -218,6 +232,10 @@ func (b *browserImpl) OnDisconnected(fn func(Browser)) {
b.On("disconnected", fn)
}

func (b *browserImpl) OnContext(fn func(BrowserContext)) {
b.On("context", fn)
}

func newBrowser(parent *channelOwner, objectType string, guid string, initializer map[string]any) *browserImpl {
b := &browserImpl{
isConnected: true,
Expand All @@ -227,6 +245,22 @@ func newBrowser(parent *channelOwner, objectType string, guid string, initialize
// convert parent to *browserTypeImpl
b.browserType = newBrowserType(parent.parent, parent.objectType, parent.guid, parent.initializer)
b.channel.On("close", b.onClose)
b.channel.On("context", func(params map[string]any) {
context := fromChannel(params["context"]).(*browserContextImpl)
b.Lock()
found := false
for _, c := range b.contexts {
if c == context {
found = true
break
}
}
if !found {
b.contexts = append(b.contexts, context)
}
b.Unlock()
b.Emit("context", context)
})
return b
}

Expand Down
117 changes: 99 additions & 18 deletions browser_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type browserContextImpl struct {
backgroundPages []Page
bindings *safe.SyncMap[string, BindingCallFunction]
tracing *tracingImpl
debugger *debuggerImpl
isClosedFlag bool
request *apiRequestContextImpl
harRecorders map[string]harRecordingMetadata
closed chan struct{}
Expand Down Expand Up @@ -72,6 +74,10 @@ func (b *browserContextImpl) Browser() Browser {
return b.browser
}

func (b *browserContextImpl) Debugger() (Debugger, error) {
return b.debugger, nil
}

func (b *browserContextImpl) Tracing() Tracing {
return b.tracing
}
Expand Down Expand Up @@ -237,11 +243,7 @@ func (b *browserContextImpl) AddInitScript(script Script) error {
return err
}

func (b *browserContextImpl) ExposeBinding(name string, binding BindingCallFunction, handle ...bool) error {
needsHandle := false
if len(handle) == 1 {
needsHandle = handle[0]
}
func (b *browserContextImpl) ExposeBinding(name string, binding BindingCallFunction) error {
for _, page := range b.Pages() {
if _, ok := page.(*pageImpl).bindings.Load(name); ok {
return fmt.Errorf("Function '%s' has been already registered in one of the pages", name)
Expand All @@ -251,8 +253,7 @@ func (b *browserContextImpl) ExposeBinding(name string, binding BindingCallFunct
return fmt.Errorf("Function '%s' has been already registered", name)
}
_, err := b.channel.Send("exposeBinding", map[string]any{
"name": name,
"needsHandle": needsHandle,
"name": name,
})
if err != nil {
return err
Expand Down Expand Up @@ -435,13 +436,36 @@ func (b *browserContextImpl) Close(options ...BrowserContextCloseOptions) error
if harId != "" {
overrides["harId"] = harId
}
response, err := b.channel.Send("harExport", overrides)
needCompressed := strings.HasSuffix(strings.ToLower(harMetaData.Path), ".zip")
if !b.connection.isRemote {
overrides["mode"] = "entries"
response, err := b.tracing.channel.SendReturnAsDict("harExport", overrides)
if err != nil {
return nil, err
}
if !needCompressed {
continue
}
entries, ok := response["entries"].([]any)
if !ok {
return nil, fmt.Errorf("could not convert HAR entries: %v", response)
}
_, err = b.connection.LocalUtils().Zip(localUtilsZipOptions{
ZipFile: harMetaData.Path,
Entries: entries,
Mode: "write",
})
if err != nil {
return nil, err
}
continue
}
overrides["mode"] = "archive"
response, err := b.tracing.channel.SendReturnAsDict("harExport", overrides)
if err != nil {
return nil, err
}
artifact := fromChannel(response).(*artifactImpl)
// Server side will compress artifact if content is attach or if file is .zip.
needCompressed := strings.HasSuffix(strings.ToLower(harMetaData.Path), ".zip")
artifact := fromChannel(response["artifact"]).(*artifactImpl)
if !needCompressed && harMetaData.Content == HarContentPolicyAttach {
tmpPath := harMetaData.Path + ".tmp"
if err := artifact.SaveAs(tmpPath); err != nil {
Expand Down Expand Up @@ -505,7 +529,7 @@ func (b *browserContextImpl) recordIntoHar(har string, options ...browserContext
overrides["page"] = options[0].Page.(*pageImpl).channel
}
}
harId, err := b.channel.Send("harStart", overrides)
harId, err := b.tracing.channel.Send("harStart", overrides)
if err != nil {
return err
}
Expand Down Expand Up @@ -547,6 +571,7 @@ func (b *browserContextImpl) onBinding(binding *bindingCallImpl) {
}

func (b *browserContextImpl) onClose() {
b.isClosedFlag = true
if b.browser != nil {
contexts := make([]BrowserContext, 0)
b.browser.Lock()
Expand Down Expand Up @@ -726,10 +751,38 @@ func (b *browserContextImpl) OnDialog(fn func(Dialog)) {
b.On("dialog", fn)
}

func (b *browserContextImpl) OnDownload(fn func(Download)) {
b.On("download", fn)
}

func (b *browserContextImpl) OnFrameAttached(fn func(Frame)) {
b.On("frameattached", fn)
}

func (b *browserContextImpl) OnFrameDetached(fn func(Frame)) {
b.On("framedetached", fn)
}

func (b *browserContextImpl) OnFrameNavigated(fn func(Frame)) {
b.On("framenavigated", fn)
}

func (b *browserContextImpl) OnPage(fn func(Page)) {
b.On("page", fn)
}

func (b *browserContextImpl) OnPageClose(fn func(Page)) {
b.On("pageclose", fn)
}

func (b *browserContextImpl) OnPageLoad(fn func(Page)) {
b.On("pageload", fn)
}

func (b *browserContextImpl) OnWebError(fn func(WebError)) {
b.On("weberror", fn)
}

func (b *browserContextImpl) OnRequest(fn func(Request)) {
b.On("request", fn)
}
Expand All @@ -746,10 +799,6 @@ func (b *browserContextImpl) OnResponse(fn func(Response)) {
b.On("response", fn)
}

func (b *browserContextImpl) OnWebError(fn func(WebError)) {
b.On("weberror", fn)
}

func (b *browserContextImpl) RouteWebSocket(url any, handler func(WebSocketRoute)) error {
b.Lock()
defer b.Unlock()
Expand Down Expand Up @@ -813,6 +862,11 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
bt.browser.contexts = append(bt.browser.contexts, bt)
}
bt.tracing = fromChannel(initializer["tracing"]).(*tracingImpl)
if dbg := fromNullableChannel(initializer["debugger"]); dbg != nil {
if d, ok := dbg.(*debuggerImpl); ok {
bt.debugger = d
}
}
bt.request = fromChannel(initializer["requestContext"]).(*apiRequestContextImpl)
bt.clock = newClock(bt)

Expand All @@ -839,6 +893,11 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
bt.channel.On("page", func(payload map[string]any) {
bt.onPage(fromChannel(payload["page"]).(*pageImpl))
})
// Note: the BrowserContext channel does not emit pageclose/frameattached/
// framedetached/framenavigated/pageload/weberror/download events. Upstream
// derives these context-level events from the owning Page (see page.go and
// frame.go, which forward to the context). The "pageError" handler below is
// the channel source for the weberror event.
bt.channel.On("route", func(params map[string]any) {
bt.channel.CreateTask(func() {
bt.onRoute(fromChannel(params["route"]).(*routeImpl))
Expand Down Expand Up @@ -888,12 +947,17 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
pwErr := &Error{}
remapMapToStruct(ev["error"].(map[string]any)["error"], pwErr)
err := parseError(*pwErr)
var location *WebErrorLocation
if locationValue, ok := ev["location"].(map[string]any); ok {
location = &WebErrorLocation{}
remapMapToStruct(locationValue, location)
}
page := fromNullableChannel(ev["page"])
if page != nil {
bt.Emit("weberror", newWebError(page.(*pageImpl), err))
bt.Emit("weberror", newWebError(page.(*pageImpl), err, location))
page.(*pageImpl).Emit("pageerror", err)
} else {
bt.Emit("weberror", newWebError(nil, err))
bt.Emit("weberror", newWebError(nil, err, location))
}
},
)
Expand Down Expand Up @@ -953,3 +1017,20 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
})
return bt
}

func (b *browserContextImpl) IsClosed() bool {
return b.isClosedFlag || b.closeReason != nil
}

func (b *browserContextImpl) SetStorageState(storageStatePath string) error {
storageString, err := os.ReadFile(storageStatePath)
if err != nil {
return err
}
var storageState map[string]any
if err := json.Unmarshal(storageString, &storageState); err != nil {
return err
}
_, err = b.channel.Send("setStorageState", map[string]any{"storageState": storageState})
return err
}
2 changes: 1 addition & 1 deletion browser_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func (b *browserTypeImpl) LaunchPersistentContext(userDataDir string, options ..

func (b *browserTypeImpl) Connect(wsEndpoint string, options ...BrowserTypeConnectOptions) (Browser, error) {
overrides := map[string]any{
"wsEndpoint": wsEndpoint,
"endpoint": wsEndpoint,
"headers": map[string]string{
"x-playwright-browser": b.Name(),
"x-playwright-launch-options": "{}",
Expand Down
4 changes: 4 additions & 0 deletions cdp_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ func (c *cdpSessionImpl) Detach() error {
return err
}

func (c *cdpSessionImpl) OnClose(fn func(CDPSession)) {
c.On("close", fn)
}

func (c *cdpSessionImpl) Send(method string, params map[string]any) (any, error) {
result, err := c.channel.Send("send", map[string]any{
"method": method,
Expand Down
8 changes: 8 additions & 0 deletions console_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,11 @@ func newConsoleMessage(event map[string]any) *consoleMessageImpl {
}
return bt
}

func (c *consoleMessageImpl) Timestamp() (float64, error) {
v, ok := c.event["timestamp"]
if !ok {
return 0, nil
}
return v.(float64), nil
}
Loading
Loading