diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e60e6829..7a97ae64 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,9 +9,14 @@ jobs: name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + - name: Set up Go 1.x + uses: actions/setup-go@v4 + with: + go-version: ^1.19.9 + id: go - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v3 with: version: latest test: @@ -23,33 +28,14 @@ jobs: runs-on: ${{ matrix.os }} name: ${{ matrix.browser }} on ${{ matrix.os }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: true - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: ^1.16.1 + go-version: ^1.19.9 id: go - - name: Cache modules and build - uses: actions/cache@v3 - with: - # In order: - # * Module download cache - # * Build cache (Linux) - # * Build cache (Mac) - # * Build cache (Windows) - path: | - ~/go/pkg/mod - ~/.cache/go-build - ~/Library/Caches/go-build - %LocalAppData%\go-build - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Download modules - if: steps.cache.outputs.cache-hit != 'true' - run: go mod download - run: | go install ./... playwright install --with-deps ${{ matrix.browser }} @@ -77,11 +63,11 @@ jobs: needs: test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: ^1.16.1 + go-version: ^1.19.9 id: go - name: Install goveralls env: @@ -94,11 +80,11 @@ jobs: test-examples: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Go 1.x - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: ^1.16.1 + go-version: ^1.19.9 id: go - run: | go install ./... diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0325bb66..53416e9a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,7 +7,7 @@ jobs: name: Deploy docs runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.7 diff --git a/.github/workflows/verify_type_generation.yml b/.github/workflows/verify_type_generation.yml index f952da7c..3f76089c 100644 --- a/.github/workflows/verify_type_generation.yml +++ b/.github/workflows/verify_type_generation.yml @@ -8,14 +8,13 @@ jobs: verify: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: submodules: true - - uses: microsoft/playwright-github-action@v1 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v4 with: - go-version: ^1.16.1 + go-version: ^1.19.9 - name: Install Browsers run: | go install ./... diff --git a/README.md b/README.md index 0f96a3da..1ca0d8d3 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![PkgGoDev](https://pkg.go.dev/badge/github.com/playwright-community/playwright-go)](https://pkg.go.dev/github.com/playwright-community/playwright-go) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](http://opensource.org/licenses/MIT) [![Go Report Card](https://goreportcard.com/badge/github.com/playwright-community/playwright-go)](https://goreportcard.com/report/github.com/playwright-community/playwright-go) ![Build Status](https://github.com/playwright-community/playwright-go/workflows/Go/badge.svg) -[![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) [![Chromium version](https://img.shields.io/badge/chromium-105.0.5195.19-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-103.0-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-16.0-blue.svg?logo=safari)](https://webkit.org/) +[![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) [![Chromium version](https://img.shields.io/badge/chromium-110.0.5481.38-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-108.0.2-blue.svg?logo=mozilla-firefox)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-16.4-blue.svg?logo=safari)](https://webkit.org/) [API reference](https://playwright.dev/docs/api/class-playwright) | [Example recipes](https://github.com/playwright-community/playwright-go/tree/main/examples) @@ -13,9 +13,9 @@ Playwright is a Go library to automate [Chromium](https://www.chromium.org/Home) | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 105.0.5195.19 | ✅ | ✅ | ✅ | -| WebKit 16.0 | ✅ | ✅ | ✅ | -| Firefox 103.0 | ✅ | ✅ | ✅ | +| Chromium 110.0.5481.38 | ✅ | ✅ | ✅ | +| WebKit 16.4 | ✅ | ✅ | ✅ | +| Firefox 108.0.2 | ✅ | ✅ | ✅ | Headless execution is supported for all the browsers on all platforms. diff --git a/artifact.go b/artifact.go index 953ae8ef..6b5ac738 100644 --- a/artifact.go +++ b/artifact.go @@ -29,7 +29,7 @@ func (a *artifactImpl) SaveAs(path string) error { if err != nil { return err } - stream := streamChannel.(*streamImpl) + stream := fromChannel(streamChannel).(*streamImpl) return stream.SaveAs(path) } diff --git a/browser.go b/browser.go index e219ba3f..ea34fe79 100644 --- a/browser.go +++ b/browser.go @@ -3,15 +3,20 @@ package playwright import ( "encoding/json" "fmt" - "io/ioutil" + "os" ) type browserImpl struct { channelOwner - isConnected bool - isClosedOrClosing bool - isConnectedOverWebSocket bool - contexts []BrowserContext + isConnected bool + isClosedOrClosing bool + shouldCloseConnectionOnClose bool + contexts []BrowserContext + browserType BrowserType +} + +func (b *browserImpl) BrowserType() BrowserType { + return b.browserType } func (b *browserImpl) IsConnected() bool { @@ -28,8 +33,8 @@ func (b *browserImpl) NewContext(options ...BrowserNewContextOptions) (BrowserCo options[0].ExtraHttpHeaders = nil } if options[0].StorageStatePath != nil { - var storageState *BrowserNewContextOptionsStorageState - storageString, err := ioutil.ReadFile(*options[0].StorageStatePath) + var storageState *OptionalStorageState + storageString, err := os.ReadFile(*options[0].StorageStatePath) if err != nil { return nil, fmt.Errorf("could not read storage state file: %w", err) } @@ -102,11 +107,17 @@ func (b *browserImpl) Contexts() []BrowserContext { } func (b *browserImpl) Close() error { + if b.isClosedOrClosing { + return nil + } + b.Lock() + b.isClosedOrClosing = true + b.Unlock() _, err := b.channel.Send("close") if err != nil { return fmt.Errorf("could not send message: %w", err) } - if b.isConnectedOverWebSocket { + if b.shouldCloseConnectionOnClose { return b.connection.Stop() } return nil @@ -118,20 +129,22 @@ func (b *browserImpl) Version() string { func (b *browserImpl) onClose() { b.Lock() - if !b.isClosedOrClosing { + b.isClosedOrClosing = true + if b.isConnected { b.isConnected = false - b.isClosedOrClosing = true b.Emit("disconnected") } b.Unlock() } func newBrowser(parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) *browserImpl { - bt := &browserImpl{ + b := &browserImpl{ isConnected: true, contexts: make([]BrowserContext, 0), } - bt.createChannelOwner(bt, parent, objectType, guid, initializer) - bt.channel.On("close", bt.onClose) - return bt + b.createChannelOwner(b, parent, objectType, guid, initializer) + // convert parent to *browserTypeImpl + b.browserType = newBrowserType(parent.parent, parent.objectType, parent.guid, parent.initializer) + b.channel.On("close", b.onClose) + return b } diff --git a/browser_context.go b/browser_context.go index da8f42bb..23a98b7a 100644 --- a/browser_context.go +++ b/browser_context.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "log" "os" ) @@ -75,22 +74,22 @@ func (b *browserContextImpl) NewPage(options ...BrowserNewPageOptions) (Page, er return fromChannel(channel).(*pageImpl), nil } -func (b *browserContextImpl) Cookies(urls ...string) ([]*BrowserContextCookiesResult, error) { +func (b *browserContextImpl) Cookies(urls ...string) ([]*Cookie, error) { result, err := b.channel.Send("cookies", map[string]interface{}{ "urls": urls, }) if err != nil { return nil, fmt.Errorf("could not send message: %w", err) } - cookies := make([]*BrowserContextCookiesResult, len(result.([]interface{}))) + cookies := make([]*Cookie, len(result.([]interface{}))) for i, cookie := range result.([]interface{}) { - cookies[i] = &BrowserContextCookiesResult{} + cookies[i] = &Cookie{} remapMapToStruct(cookie, cookies[i]) } return cookies, nil } -func (b *browserContextImpl) AddCookies(cookies ...BrowserContextAddCookiesOptionsCookies) error { +func (b *browserContextImpl) AddCookies(cookies ...OptionalCookie) error { _, err := b.channel.Send("addCookies", map[string]interface{}{ "cookies": cookies, }) @@ -114,14 +113,14 @@ func (b *browserContextImpl) ClearPermissions() error { return err } -// SetGeolocationOptions represents the options for BrowserContext.SetGeolocation() -type SetGeolocationOptions struct { - Longitude int `json:"longitude"` - Latitude int `json:"latitude"` - Accuracy *int `json:"accuracy"` +// Geolocation represents the options for BrowserContext.SetGeolocation() +type Geolocation struct { + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` + Accuracy *float64 `json:"accuracy"` } -func (b *browserContextImpl) SetGeolocation(gelocation *SetGeolocationOptions) error { +func (b *browserContextImpl) SetGeolocation(gelocation *Geolocation) error { _, err := b.channel.Send("setGeolocation", map[string]interface{}{ "geolocation": gelocation, }) @@ -153,7 +152,7 @@ func (b *browserContextImpl) AddInitScript(options BrowserContextAddInitScriptOp source = *options.Script } if options.Path != nil { - content, err := ioutil.ReadFile(*options.Path) + content, err := os.ReadFile(*options.Path) if err != nil { return err } @@ -192,8 +191,8 @@ func (b *browserContextImpl) ExposeFunction(name string, binding ExposedFunction }) } -func (b *browserContextImpl) Route(url interface{}, handler routeHandler) error { - b.routes = append(b.routes, newRouteHandlerEntry(newURLMatcher(url), handler)) +func (b *browserContextImpl) Route(url interface{}, handler routeHandler, times ...int) error { + b.routes = append(b.routes, newRouteHandlerEntry(newURLMatcher(url, b.options.BaseURL), handler, times...)) if len(b.routes) == 1 { _, err := b.channel.Send("setNetworkInterceptionEnabled", map[string]interface{}{ "enabled": true, @@ -218,12 +217,26 @@ func (b *browserContextImpl) Unroute(url interface{}, handlers ...routeHandler) return nil } -func (b *browserContextImpl) WaitForEvent(event string, predicate ...interface{}) interface{} { - return <-waitForEvent(b, event, predicate...) +func (b *browserContextImpl) WaitForEvent(event string, options ...BrowserContextWaitForEventOptions) (interface{}, error) { + return b.waiterForEvent(event, options...).Wait() } -func (b *browserContextImpl) ExpectEvent(event string, cb func() error) (interface{}, error) { - return newExpectWrapper(b.WaitForEvent, []interface{}{event}, cb) +func (b *browserContextImpl) waiterForEvent(event string, options ...BrowserContextWaitForEventOptions) *waiter { + timeout := b.timeoutSettings.Timeout() + var predicate interface{} = nil + if len(options) == 1 { + if options[0].Timeout != nil { + timeout = *options[0].Timeout + } + predicate = options[0].Predicate + } + waiter := newWaiter().WithTimeout(timeout) + waiter.RejectOnEvent(b, "close", errors.New("context closed")) + return waiter.WaitForEvent(b, event, predicate) +} + +func (b *browserContextImpl) ExpectEvent(event string, cb func() error, options ...BrowserContextWaitForEventOptions) (interface{}, error) { + return b.waiterForEvent(event, options...).Expect(cb) } func (b *browserContextImpl) Close() error { @@ -258,15 +271,15 @@ type StorageState struct { } type Cookie struct { - Name string `json:"name"` - Value string `json:"value"` - URL string `json:"url"` - Domain string `json:"domain"` - Path string `json:"path"` - Expires float64 `json:"expires"` - HttpOnly bool `json:"httpOnly"` - Secure bool `json:"secure"` - SameSite string `json:"sameSite"` + Name string `json:"name"` + Value string `json:"value"` + URL string `json:"url"` + Domain string `json:"domain"` + Path string `json:"path"` + Expires float64 `json:"expires"` + HttpOnly bool `json:"httpOnly"` + Secure bool `json:"secure"` + SameSite *SameSiteAttribute `json:"sameSite"` } type OriginsState struct { Origin string `json:"origin"` @@ -336,11 +349,12 @@ func (b *browserContextImpl) onPage(page *pageImpl) { } } -func (b *browserContextImpl) onRoute(route *routeImpl, request *requestImpl) { +func (b *browserContextImpl) onRoute(route *routeImpl) { go func() { + url := route.Request().URL() for _, handlerEntry := range b.routes { - if handlerEntry.matcher.Matches(request.URL()) { - handlerEntry.handler(route, request) + if handlerEntry.matcher.Matches(url) { + handlerEntry.handler(route) return } } @@ -350,8 +364,16 @@ func (b *browserContextImpl) onRoute(route *routeImpl, request *requestImpl) { }() } func (p *browserContextImpl) Pause() error { - _, err := p.channel.Send("pause") - return err + return <-p.pause() +} + +func (p *browserContextImpl) pause() <-chan error { + ret := make(chan error, 1) + go func() { + _, err := p.channel.Send("pause") + ret <- err + }() + return ret } func (b *browserContextImpl) OnBackgroundPage(ev map[string]interface{}) { @@ -396,9 +418,7 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini request.failureText = failureText.(string) } page := fromNullableChannel(ev["page"]) - if request.timing != nil { - request.timing.ResponseEnd = ev["responseEndTiming"].(float64) - } + request.setResponseEndTiming(ev["responseEndTiming"].(float64)) bt.Emit("requestfailed", request) if page != nil { page.(*pageImpl).Emit("requestfailed", request) @@ -408,17 +428,15 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini bt.channel.On("requestFinished", func(ev map[string]interface{}) { request := fromChannel(ev["request"]).(*requestImpl) response := fromNullableChannel(ev["response"]) - if response != nil { - response.(*responseImpl).finished <- true - } page := fromNullableChannel(ev["page"]) - if request.timing != nil { - request.timing.ResponseEnd = ev["responseEndTiming"].(float64) - } + request.setResponseEndTiming(ev["responseEndTiming"].(float64)) bt.Emit("requestfinished", request) if page != nil { page.(*pageImpl).Emit("requestfinished", request) } + if response != nil { + response.(*responseImpl).finished <- true + } }) bt.channel.On("response", func(ev map[string]interface{}) { response := fromChannel(ev["response"]).(*responseImpl) @@ -433,8 +451,14 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini bt.onPage(fromChannel(payload["page"]).(*pageImpl)) }) bt.channel.On("route", func(params map[string]interface{}) { - bt.onRoute(fromChannel(params["route"]).(*routeImpl), fromChannel(params["request"]).(*requestImpl)) + bt.onRoute(fromChannel(params["route"]).(*routeImpl)) }) bt.channel.On("backgroundPage", bt.OnBackgroundPage) + bt.setEventSubscriptionMapping(map[string]string{ + "request": "request", + "response": "response", + "requestfinished": "requestFinished", + "responsefailed": "responseFailed", + }) return bt } diff --git a/browser_type.go b/browser_type.go index 42e1978e..02cb17e7 100644 --- a/browser_type.go +++ b/browser_type.go @@ -66,12 +66,13 @@ func (b *browserTypeImpl) Connect(url string, options ...BrowserTypeConnectOptio overrides := map[string]interface{}{ "wsEndpoint": url, } - pipe, err := b.channel.Send("connect", overrides, options) + localUtils := b.connection.LocalUtils() + pipe, err := localUtils.channel.Send("connect", overrides, options) if err != nil { return nil, err } jsonPipe := fromChannel(pipe).(*jsonPipe) - connection := newConnection(jsonPipe.Close) + connection := newConnection(jsonPipe.Close, localUtils) connection.isRemote = true var browser *browserImpl pipeClosed := func() { @@ -83,6 +84,7 @@ func (b *browserTypeImpl) Connect(url string, options ...BrowserTypeConnectOptio context.(*browserContextImpl).onClose() } browser.onClose() + connection.cleanup() } jsonPipe.On("closed", pipeClosed) connection.onmessage = func(message map[string]interface{}) error { @@ -95,7 +97,7 @@ func (b *browserTypeImpl) Connect(url string, options ...BrowserTypeConnectOptio jsonPipe.On("message", connection.Dispatch) playwright := connection.Start() browser = fromChannel(playwright.initializer["preLaunchedBrowser"]).(*browserImpl) - browser.isConnectedOverWebSocket = true + browser.shouldCloseConnectionOnClose = true return browser, nil } func (b *browserTypeImpl) ConnectOverCDP(endpointURL string, options ...BrowserTypeConnectOverCDPOptions) (Browser, error) { diff --git a/channel_owner.go b/channel_owner.go index e11420b5..8b1373e0 100644 --- a/channel_owner.go +++ b/channel_owner.go @@ -7,13 +7,14 @@ import ( type channelOwner struct { sync.RWMutex eventEmitter - objectType string - guid string - channel *channel - objects map[string]*channelOwner - connection *connection - initializer map[string]interface{} - parent *channelOwner + objectType string + guid string + channel *channel + objects map[string]*channelOwner + eventToSubscriptionMapping map[string]string + connection *connection + initializer map[string]interface{} + parent *channelOwner } func (c *channelOwner) dispose() { @@ -30,6 +31,48 @@ func (c *channelOwner) dispose() { c.objects = make(map[string]*channelOwner) } +func (c *channelOwner) adopt(child *channelOwner) { + delete(child.parent.objects, child.guid) + c.objects[child.guid] = child + child.parent = c +} + +func (c *channelOwner) setEventSubscriptionMapping(mapping map[string]string) { + c.eventToSubscriptionMapping = mapping +} + +func (c *channelOwner) updateSubscription(event string, enabled bool) { + protocolEvent, ok := c.eventToSubscriptionMapping[event] + if ok { + c.channel.SendNoReply("updateSubscription", map[string]interface{}{ + "event": protocolEvent, + "enabled": enabled, + }) + } +} + +func (c *channelOwner) Once(name string, handler interface{}) { + c.addEvent(name, handler, true) +} + +func (c *channelOwner) On(name string, handler interface{}) { + c.addEvent(name, handler, false) +} + +func (c *channelOwner) addEvent(name string, handler interface{}, once bool) { + if c.ListenerCount(name) == 0 { + c.updateSubscription(name, true) + } + c.eventEmitter.addEvent(name, handler, once) +} + +func (c *channelOwner) RemoveListener(name string, handler interface{}) { + c.eventEmitter.RemoveListener(name, handler) + if c.ListenerCount(name) == 0 { + c.updateSubscription(name, false) + } +} + func (c *channelOwner) createChannelOwner(self interface{}, parent *channelOwner, objectType string, guid string, initializer map[string]interface{}) { c.objectType = objectType c.guid = guid @@ -45,6 +88,7 @@ func (c *channelOwner) createChannelOwner(self interface{}, parent *channelOwner } c.channel = newChannel(c.connection, guid) c.channel.object = self + c.eventToSubscriptionMapping = map[string]string{} c.initEventEmitter() } diff --git a/connection.go b/connection.go index b38e3b01..60c9eeea 100644 --- a/connection.go +++ b/connection.go @@ -25,6 +25,7 @@ type connection struct { onClose func() error onmessage func(map[string]interface{}) error isRemote bool + localUtils *localUtilsImpl } func (c *connection) Start() *Playwright { @@ -41,7 +42,26 @@ func (c *connection) Start() *Playwright { } func (c *connection) Stop() error { - return c.onClose() + err := c.onClose() + if err != nil { + return err + } + c.cleanup() + return nil +} + +func (c *connection) cleanup() { + c.callbacks.Range(func(key, value any) bool { + select { + case value.(chan callback) <- callback{ + Error: &Error{ + Name: "Error", + Message: "Connection closed", + }}: + default: + } + return true + }) } func (c *connection) Dispatch(msg *message) { @@ -69,6 +89,14 @@ func (c *connection) Dispatch(msg *message) { if object == nil { return } + if method == "__adopt__" { + child, ok := c.objects[msg.Params["guid"].(string)] + if !ok { + return + } + object.adopt(child) + return + } if method == "__dispose__" { object.dispose() return @@ -80,6 +108,10 @@ func (c *connection) Dispatch(msg *message) { } } +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{})) @@ -186,13 +218,16 @@ func serializeCallStack(stack stack.CallStack) []map[string]interface{} { return callStack } -func newConnection(onClose func() error) *connection { +func newConnection(onClose func() error, localUtils ...*localUtilsImpl) *connection { connection := &connection{ waitingForRemoteObjects: make(map[string]chan interface{}), objects: make(map[string]*channelOwner), onClose: onClose, isRemote: false, } + if len(localUtils) > 0 { + connection.localUtils = localUtils[0] + } connection.rootObject = newRootChannelOwner(connection) return connection } diff --git a/element_handle.go b/element_handle.go index 2018b318..e0ed7be9 100644 --- a/element_handle.go +++ b/element_handle.go @@ -3,7 +3,7 @@ package playwright import ( "encoding/base64" "fmt" - "io/ioutil" + "os" ) type elementHandleImpl struct { @@ -265,7 +265,7 @@ func (e *elementHandleImpl) Screenshot(options ...ElementHandleScreenshotOptions return nil, fmt.Errorf("could not decode base64 :%w", err) } if path != nil { - if err := ioutil.WriteFile(*path, image, 0644); err != nil { + if err := os.WriteFile(*path, image, 0644); err != nil { return nil, err } } diff --git a/errors.go b/errors.go index d1a69340..c235fb43 100644 --- a/errors.go +++ b/errors.go @@ -11,21 +11,26 @@ func (e *Error) Error() string { return e.Message } -// TimeoutError represents a Playwright TimeoutError -type TimeoutError Error +func (e *Error) Is(target error) bool { + err, ok := target.(*Error) + if !ok { + return false + } + if err.Name != e.Name { + return false + } + if e.Name != "Error" { + return true // same name and not normal error + } + return e.Message == err.Message +} -func (e *TimeoutError) Error() string { - return e.Message +// TimeoutError represents a Playwright TimeoutError +var TimeoutError = &Error{ + Name: "TimeoutError", } func parseError(err errorPayload) error { - if err.Name == "TimeoutError" { - return &TimeoutError{ - Name: "TimeoutError", - Message: err.Message, - Stack: err.Stack, - } - } return &Error{ Name: err.Name, Message: err.Message, diff --git a/event_emitter.go b/event_emitter.go index f1d8d097..4a2cefb0 100644 --- a/event_emitter.go +++ b/event_emitter.go @@ -12,10 +12,8 @@ type ( on []interface{} } eventEmitter struct { - eventsMutex sync.Mutex - events map[string]*eventRegister - addEventHandlers []func(name string, handler interface{}) - removeEventHandlers []func(name string, handler interface{}) + eventsMutex sync.Mutex + events map[string]*eventRegister } ) @@ -53,18 +51,7 @@ func (e *eventEmitter) On(name string, handler interface{}) { e.addEvent(name, handler, false) } -func (e *eventEmitter) addEventHandler(handler func(name string, handler interface{})) { - e.addEventHandlers = append(e.addEventHandlers, handler) -} - -func (e *eventEmitter) removeEventHandler(handler func(name string, handler interface{})) { - e.removeEventHandlers = append(e.removeEventHandlers, handler) -} - func (e *eventEmitter) RemoveListener(name string, handler interface{}) { - for _, mitm := range e.removeEventHandlers { - mitm(name, handler) - } e.eventsMutex.Lock() defer e.eventsMutex.Unlock() if _, ok := e.events[name]; !ok { @@ -92,20 +79,20 @@ func (e *eventEmitter) RemoveListener(name string, handler interface{}) { e.events[name].once = onceHandlers } +// ListenerCount count the listeners by name, count all if name is empty func (e *eventEmitter) ListenerCount(name string) int { count := 0 e.eventsMutex.Lock() for key := range e.events { - count += len(e.events[key].on) + len(e.events[key].once) + if name == "" || name == key { + count += len(e.events[key].on) + len(e.events[key].once) + } } e.eventsMutex.Unlock() return count } func (e *eventEmitter) addEvent(name string, handler interface{}, once bool) { - for _, mitm := range e.addEventHandlers { - mitm(name, handler) - } e.eventsMutex.Lock() if _, ok := e.events[name]; !ok { e.events[name] = &eventRegister{ diff --git a/event_emitter_test.go b/event_emitter_test.go index 1c6c9e07..e939d648 100644 --- a/event_emitter_test.go +++ b/event_emitter_test.go @@ -6,7 +6,29 @@ import ( "github.com/stretchr/testify/require" ) -const testEventName = "foobar" +const ( + testEventName = "foobar" + testEventNameFoo = "foo" + testEventNameBar = "bar" +) + +func TestEventEmitterListenerCount(t *testing.T) { + handler := &eventEmitter{} + handler.initEventEmitter() + wasCalled := make(chan interface{}, 1) + myHandler := func(payload ...interface{}) { + wasCalled <- payload[0] + } + require.Nil(t, handler.events[testEventNameFoo]) + handler.On(testEventNameFoo, myHandler) + require.Equal(t, 1, handler.ListenerCount(testEventNameFoo)) + handler.Once(testEventNameFoo, myHandler) + require.Equal(t, 2, handler.ListenerCount(testEventNameFoo)) + require.Nil(t, handler.events[testEventNameBar]) + handler.Once(testEventNameBar, myHandler) + require.Equal(t, 1, handler.ListenerCount(testEventNameBar)) + require.Equal(t, 3, handler.ListenerCount("")) +} func TestEventEmitterOn(t *testing.T) { handler := &eventEmitter{} diff --git a/examples/mobile-and-geolocation/main.go b/examples/mobile-and-geolocation/main.go index 104f8a91..67a65b79 100644 --- a/examples/mobile-and-geolocation/main.go +++ b/examples/mobile-and-geolocation/main.go @@ -20,9 +20,9 @@ func main() { } device := pw.Devices["Pixel 5"] context, err := browser.NewContext(playwright.BrowserNewContextOptions{ - Geolocation: &playwright.BrowserNewContextOptionsGeolocation{ - Longitude: playwright.Float(12.492507), - Latitude: playwright.Float(41.889938), + Geolocation: &playwright.Geolocation{ + Longitude: 12.492507, + Latitude: 41.889938, }, Permissions: []string{"geolocation"}, Viewport: device.Viewport, diff --git a/examples/parallel-scraping/main.go b/examples/parallel-scraping/main.go index bec0c0d6..4e82936f 100644 --- a/examples/parallel-scraping/main.go +++ b/examples/parallel-scraping/main.go @@ -10,7 +10,6 @@ import ( "encoding/csv" "fmt" "io" - "io/ioutil" "log" "math" "net/http" @@ -161,11 +160,11 @@ func main() { } func getAlexaTopDomains() ([]string, error) { - resp, err := http.Get("http://s3.amazonaws.com/alexa-static/top-1m.csv.zip") + resp, err := http.Get("http://s3-us-west-1.amazonaws.com/umbrella-static/top-1m.csv.zip") if err != nil { return nil, fmt.Errorf("could not get: %w", err) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("could not read body: %w", err) } diff --git a/examples/screenshot/main.go b/examples/screenshot/main.go index 3d484031..855e5be1 100644 --- a/examples/screenshot/main.go +++ b/examples/screenshot/main.go @@ -23,7 +23,8 @@ func main() { log.Fatalf("could not create page: %v", err) } if _, err = page.Goto("http://whatsmyuseragent.org/", playwright.PageGotoOptions{ - WaitUntil: playwright.WaitUntilStateNetworkidle, + // networkidle is DISCOURAGED + WaitUntil: playwright.WaitUntilStateDomcontentloaded, }); err != nil { log.Fatalf("could not goto: %v", err) } diff --git a/examples/video/main.go b/examples/video/main.go index 93bcf7b6..09a9583a 100644 --- a/examples/video/main.go +++ b/examples/video/main.go @@ -20,7 +20,7 @@ func main() { log.Fatalf("could not launch Chromium: %v", err) } page, err := browser.NewPage(playwright.BrowserNewContextOptions{ - RecordVideo: &playwright.BrowserNewContextOptionsRecordVideo{ + RecordVideo: &playwright.RecordVideo{ Dir: playwright.String("videos/"), }, }) diff --git a/expect_wrapper.go b/expect_wrapper.go deleted file mode 100644 index 152f52e1..00000000 --- a/expect_wrapper.go +++ /dev/null @@ -1,23 +0,0 @@ -package playwright - -import ( - "reflect" -) - -func newExpectWrapper(f interface{}, args []interface{}, cb func() error) (interface{}, error) { - val := make(chan interface{}, 1) - go func() { - reflectArgs := make([]reflect.Value, 0) - for i := 0; i < len(args); i++ { - reflectArgs = append(reflectArgs, reflect.ValueOf(args[i])) - } - result := reflect.ValueOf(f).Call(reflectArgs) - evVal := result[0].Interface() - val <- evVal - }() - - if err := cb(); err != nil { - return nil, err - } - return <-val, nil -} diff --git a/fetch.go b/fetch.go new file mode 100644 index 00000000..41559175 --- /dev/null +++ b/fetch.go @@ -0,0 +1,403 @@ +package playwright + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "os" +) + +type apiRequestImpl struct { + *Playwright +} + +func (r *apiRequestImpl) NewContext(options ...APIRequestNewContextOptions) (APIRequestContext, error) { + overrides := map[string]interface{}{} + if len(options) == 1 { + if options[0].ExtraHttpHeaders != nil { + overrides["extraHTTPHeaders"] = serializeMapToNameAndValue(options[0].ExtraHttpHeaders) + options[0].ExtraHttpHeaders = nil + } + if options[0].StorageStatePath != nil { + var storageState *StorageState + storageString, err := os.ReadFile(*options[0].StorageStatePath) + if err != nil { + return nil, fmt.Errorf("could not read storage state file: %w", err) + } + err = json.Unmarshal(storageString, &storageState) + if err != nil { + return nil, fmt.Errorf("could not parse storage state file: %w", err) + } + options[0].StorageState = storageState + options[0].StorageStatePath = nil + } + } + + channel, err := r.channel.Send("newRequest", overrides, options) + if err != nil { + return nil, fmt.Errorf("could not send message: %w", err) + } + return fromChannel(channel).(*apiRequestContextImpl), nil +} + +func newApiRequestImpl(pw *Playwright) *apiRequestImpl { + return &apiRequestImpl{pw} +} + +type apiRequestContextImpl struct { + channelOwner + tracing *tracingImpl +} + +func (r *apiRequestContextImpl) Dispose() error { + _, err := r.channel.Send("dispose") + return err +} + +func (r *apiRequestContextImpl) Delete(url string, options ...APIRequestContextDeleteOptions) (APIResponse, error) { + opts := APIRequestContextFetchOptions{ + Method: String("DELETE"), + } + if len(options) == 1 { + err := assignStructFields(&opts, options[0], false) + if err != nil { + return nil, err + } + } + + return r.Fetch(url, opts) +} + +func (r *apiRequestContextImpl) Fetch(urlOrRequest interface{}, options ...APIRequestContextFetchOptions) (APIResponse, error) { + switch v := urlOrRequest.(type) { + case string: + return r.innerFetch(v, nil, options...) + case Request: + return r.innerFetch("", v, options...) + default: + return nil, fmt.Errorf("urlOrRequest has unsupported type: %T", urlOrRequest) + } +} + +func (r *apiRequestContextImpl) innerFetch(url string, request Request, options ...APIRequestContextFetchOptions) (APIResponse, error) { + overrides := map[string]interface{}{} + if url != "" { + overrides["url"] = url + } else if request != nil { + overrides["url"] = request.URL() + } + + if len(options) == 1 { + if options[0].MaxRedirects != nil && *options[0].MaxRedirects < 0 { + return nil, errors.New("maxRedirects must be non-negative") + } + // only one of them can be specified + if countNonNil(options[0].Data, options[0].Form, options[0].Multipart) > 1 { + return nil, errors.New("only one of 'data', 'form' or 'multipart' can be specified") + } + if options[0].Method == nil { + if request != nil { + options[0].Method = String(request.Method()) + } else { + options[0].Method = String("GET") + } + } + if options[0].Headers == nil { + if request != nil { + overrides["headers"] = serializeMapToNameAndValue(request.Headers()) + } + } else { + overrides["headers"] = serializeMapToNameAndValue(options[0].Headers) + options[0].Headers = nil + } + if options[0].Data != nil { + switch v := options[0].Data.(type) { + case string: + headersArray, ok := overrides["headers"].([]map[string]string) + if ok && isJsonContentType(headersArray) { + overrides["jsonData"] = v + } else { + overrides["postData"] = base64.StdEncoding.EncodeToString([]byte(v)) + } + case []byte: + overrides["postData"] = base64.StdEncoding.EncodeToString(v) + case interface{}: + data, err := json.Marshal(v) + if err != nil { + return nil, fmt.Errorf("could not marshal data: %w", err) + } + overrides["jsonData"] = string(data) + default: + return nil, errors.New("data must be a string, []byte, or interface{} that can marshal to json") + } + options[0].Data = nil + } else if options[0].Form != nil { + form, ok := options[0].Form.(map[string]interface{}) + if !ok { + return nil, errors.New("form must be a map") + } + overrides["formData"] = serializeMapToNameValue(form) + options[0].Form = nil + } else if options[0].Multipart != nil { + _, ok := options[0].Multipart.(map[string]interface{}) + if !ok { + return nil, errors.New("multipart must be a map") + } + multipartData := []map[string]interface{}{} + for name, value := range options[0].Multipart.(map[string]interface{}) { + switch v := value.(type) { + case InputFile: + multipartData = append(multipartData, map[string]interface{}{ + "name": name, + "file": map[string]string{ + "name": v.Name, + "mimeType": v.MimeType, + "buffer": base64.StdEncoding.EncodeToString(v.Buffer), + }, + }) + default: + multipartData = append(multipartData, map[string]interface{}{ + "name": name, + "value": String(fmt.Sprintf("%v", v)), + }) + } + } + overrides["multipartData"] = multipartData + options[0].Multipart = nil + } else if request != nil { + postDataBuf, err := request.PostDataBuffer() + if err == nil { + overrides["postData"] = base64.StdEncoding.EncodeToString(postDataBuf) + } + } + if options[0].Params != nil { + overrides["params"] = serializeMapToNameValue(options[0].Params) + options[0].Params = nil + } + } + + response, err := r.channel.Send("fetch", overrides, options) + if err != nil { + return nil, err + } + + return newAPIResponse(r, response.(map[string]interface{})), nil +} + +func (r *apiRequestContextImpl) Get(url string, options ...APIRequestContextGetOptions) (APIResponse, error) { + opts := APIRequestContextFetchOptions{ + Method: String("GET"), + } + if len(options) == 1 { + err := assignStructFields(&opts, options[0], false) + if err != nil { + return nil, err + } + } + + return r.Fetch(url, opts) +} + +func (r *apiRequestContextImpl) Head(url string, options ...APIRequestContextHeadOptions) (APIResponse, error) { + opts := APIRequestContextFetchOptions{ + Method: String("HEAD"), + } + if len(options) == 1 { + err := assignStructFields(&opts, options[0], false) + if err != nil { + return nil, err + } + } + + return r.Fetch(url, opts) +} + +func (r *apiRequestContextImpl) Patch(url string, options ...APIRequestContextPatchOptions) (APIResponse, error) { + opts := APIRequestContextFetchOptions{ + Method: String("PATCH"), + } + if len(options) == 1 { + err := assignStructFields(&opts, options[0], false) + if err != nil { + return nil, err + } + } + + return r.Fetch(url, opts) +} + +func (r *apiRequestContextImpl) Put(url string, options ...APIRequestContextPutOptions) (APIResponse, error) { + opts := APIRequestContextFetchOptions{ + Method: String("GET"), + } + if len(options) == 1 { + err := assignStructFields(&opts, options[0], false) + if err != nil { + return nil, err + } + } + + return r.Fetch(url, opts) +} + +func (r *apiRequestContextImpl) Post(url string, options ...APIRequestContextPostOptions) (APIResponse, error) { + opts := APIRequestContextFetchOptions{ + Method: String("GET"), + } + if len(options) == 1 { + err := assignStructFields(&opts, options[0], false) + if err != nil { + return nil, err + } + } + + return r.Fetch(url, opts) +} + +func (r *apiRequestContextImpl) StorageState(paths ...string) (*StorageState, error) { + result, err := r.channel.SendReturnAsDict("storageState") + if err != nil { + return nil, err + } + if len(paths) == 1 { + file, err := os.Create(paths[0]) + if err != nil { + return nil, err + } + if err := json.NewEncoder(file).Encode(result); err != nil { + return nil, err + } + if err := file.Close(); err != nil { + return nil, err + } + } + var storageState StorageState + remapMapToStruct(result, &storageState) + return &storageState, nil +} + +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) + return rc +} + +type apiResponseImpl struct { + request *apiRequestContextImpl + initializer map[string]interface{} + headers *rawHeaders +} + +func (r *apiResponseImpl) Body() ([]byte, error) { + result, err := r.request.channel.SendReturnAsDict("fetchResponseBody", []map[string]interface{}{ + { + "fetchUid": r.fetchUid(), + }, + }) + if err != nil { + return nil, err + } + body := result.(map[string]interface{})["binary"] + if body == nil { + return nil, errors.New("response has been disposed") + } + return base64.StdEncoding.DecodeString(body.(string)) +} + +func (r *apiResponseImpl) Dispose() error { + _, err := r.request.channel.Send("disposeAPIResponse", []map[string]interface{}{ + { + "fetchUid": r.fetchUid(), + }, + }) + return err +} + +func (r *apiResponseImpl) Headers() map[string]string { + return r.headers.Headers() +} + +func (r *apiResponseImpl) HeadersArray() HeadersArray { + return r.headers.HeadersArray() +} + +func (r *apiResponseImpl) JSON(v interface{}) error { + body, err := r.Body() + if err != nil { + return err + } + return json.Unmarshal(body, &v) +} + +func (r *apiResponseImpl) Ok() bool { + return r.Status() == 0 || (r.Status() >= 200 && r.Status() <= 299) +} + +func (r *apiResponseImpl) Status() int { + return int(r.initializer["status"].(float64)) +} + +func (r *apiResponseImpl) StatusText() string { + return r.initializer["statusText"].(string) +} + +func (r *apiResponseImpl) Text() (string, error) { + body, err := r.Body() + if err != nil { + return "", err + } + return string(body), nil +} + +func (r *apiResponseImpl) URL() string { + return r.initializer["url"].(string) +} + +func (r *apiResponseImpl) fetchUid() string { + return r.initializer["fetchUid"].(string) +} + +func newAPIResponse(context *apiRequestContextImpl, initializer map[string]interface{}) *apiResponseImpl { + return &apiResponseImpl{ + request: context, + initializer: initializer, + headers: newRawHeaders(initializer["headers"]), + } +} + +func countNonNil(args ...interface{}) int { + count := 0 + for _, v := range args { + if v != nil { + count++ + } + } + return count +} + +func isJsonContentType(headers []map[string]string) bool { + if len(headers) > 0 { + for _, v := range headers { + if v["name"] == "Content-Type" { + if v["value"] == "application/json" { + return true + } + } + } + + } + return false +} + +func serializeMapToNameValue(data map[string]interface{}) []map[string]string { + serialized := make([]map[string]string, 0, len(data)) + for k, v := range data { + serialized = append(serialized, map[string]string{ + "name": k, + "value": fmt.Sprintf("%v", v), + }) + } + return serialized +} diff --git a/file_chooser.go b/file_chooser.go index 81cb72b9..ef3b24ec 100644 --- a/file_chooser.go +++ b/file_chooser.go @@ -23,9 +23,9 @@ func (f *fileChooserImpl) IsMultiple() bool { // - ElementHandle.SetInputFiles() // - Page.SetInputFiles() type InputFile struct { - Name string - MimeType string - Buffer []byte + Name string `json:"name"` + MimeType string `json:"mimeType,omitempty"` + Buffer []byte `json:"buffer"` } func (f *fileChooserImpl) SetFiles(files []InputFile, options ...ElementHandleSetInputFilesOptions) error { diff --git a/frame.go b/frame.go index 3547eb91..6d5d3a80 100644 --- a/frame.go +++ b/frame.go @@ -1,8 +1,9 @@ package playwright import ( + "errors" "fmt" - "io/ioutil" + "os" "sync" "time" ) @@ -78,6 +79,7 @@ func (f *frameImpl) Goto(url string, options ...PageGotoOptions) (Response, erro } channelOwner := fromNullableChannel(channel) if channelOwner == nil { + // navigation to about:blank or navigation to the same URL with a different hash return nil, nil } return channelOwner.(*responseImpl), nil @@ -85,7 +87,7 @@ func (f *frameImpl) Goto(url string, options ...PageGotoOptions) (Response, erro func (f *frameImpl) AddScriptTag(options PageAddScriptTagOptions) (ElementHandle, error) { if options.Path != nil { - file, err := ioutil.ReadFile(*options.Path) + file, err := os.ReadFile(*options.Path) if err != nil { return nil, err } @@ -101,7 +103,7 @@ func (f *frameImpl) AddScriptTag(options PageAddScriptTagOptions) (ElementHandle func (f *frameImpl) AddStyleTag(options PageAddStyleTagOptions) (ElementHandle, error) { if options.Path != nil { - file, err := ioutil.ReadFile(*options.Path) + file, err := os.ReadFile(*options.Path) if err != nil { return nil, err } @@ -119,26 +121,53 @@ func (f *frameImpl) Page() Page { return f.page } -func (f *frameImpl) WaitForLoadState(given ...string) { - state := "load" - if len(given) == 1 { - state = given[0] +func (f *frameImpl) WaitForLoadState(options ...PageWaitForLoadStateOptions) error { + option := PageWaitForLoadStateOptions{} + if len(options) == 1 { + option = options[0] + } + if option.State == nil { + option.State = LoadStateLoad + } + if option.Timeout == nil { + option.Timeout = Float(f.page.timeoutSettings.NavigationTimeout()) } + return f.waitForLoadStateImpl(string(*option.State), option.Timeout, nil) +} + +func (f *frameImpl) waitForLoadStateImpl(state string, timeout *float64, cb func() error) error { if f.loadStates.Has(state) { - return - } - var wg sync.WaitGroup - wg.Add(1) - f.On("loadstate", func(ev ...interface{}) { - gotState := ev[0].(string) - if gotState == state { - wg.Done() - } + return nil + } + waiter := f.setNavigationWaiter(timeout) + waiter.WaitForEvent(f, "loadstate", func(payload interface{}) bool { + gotState := payload.(string) + return gotState == state }) - wg.Wait() + if cb == nil { + _, err := waiter.Wait() + return err + } else { + _, err := waiter.Expect(cb) + return err + } } func (f *frameImpl) WaitForURL(url string, options ...FrameWaitForURLOptions) error { + matcher := newURLMatcher(url, f.page.browserContext.options.BaseURL) + if matcher.Matches(f.URL()) { + state := "load" + timeout := Float(f.page.timeoutSettings.NavigationTimeout()) + if len(options) == 1 { + if options[0].WaitUntil != nil { + state = string(*options[0].WaitUntil) + } + if options[0].Timeout != nil { + timeout = options[0].Timeout + } + } + return f.waitForLoadStateImpl(state, timeout, nil) + } navigationOptions := PageWaitForNavigationOptions{URL: url} if len(options) > 0 { navigationOptions.Timeout = options[0].Timeout @@ -150,8 +179,19 @@ func (f *frameImpl) WaitForURL(url string, options ...FrameWaitForURLOptions) er return nil } -func (f *frameImpl) WaitForEvent(event string, predicate ...interface{}) interface{} { - return <-waitForEvent(f, event, predicate...) +func (f *frameImpl) WaitForEvent(event string, options ...PageWaitForEventOptions) (interface{}, error) { + timeout := f.page.timeoutSettings.Timeout() + var predicate interface{} = nil + if len(options) == 1 { + if options[0].Timeout != nil { + timeout = *options[0].Timeout + } + predicate = options[0].Predicate + } + waiter := newWaiter().WithTimeout(timeout) + waiter.RejectOnEvent(f.page, "close", errors.New("page closed")) + waiter.RejectOnEvent(f.page, "crash", errors.New("page crashed")) + return waiter.WaitForEvent(f, event, predicate).Wait() } func (f *frameImpl) WaitForNavigation(options ...PageWaitForNavigationOptions) (Response, error) { @@ -165,10 +205,10 @@ func (f *frameImpl) WaitForNavigation(options ...PageWaitForNavigationOptions) ( if option.Timeout == nil { option.Timeout = Float(f.page.timeoutSettings.NavigationTimeout()) } - deadline := time.After(time.Duration(*option.Timeout) * time.Millisecond) + deadline := time.Now().Add(time.Duration(*option.Timeout) * time.Millisecond) var matcher *urlMatcher if option.URL != nil { - matcher = newURLMatcher(option.URL) + matcher = newURLMatcher(option.URL, f.page.browserContext.options.BaseURL) } predicate := func(events ...interface{}) bool { ev := events[0].(map[string]interface{}) @@ -177,19 +217,42 @@ func (f *frameImpl) WaitForNavigation(options ...PageWaitForNavigationOptions) ( } return matcher == nil || matcher.Matches(ev["url"].(string)) } - select { - case <-deadline: - return nil, fmt.Errorf("Timeout %.2fms exceeded.", *option.Timeout) - case eventData := <-waitForEvent(f, "navigated", predicate): - event := eventData.(map[string]interface{}) - if event["newDocument"] != nil && event["newDocument"].(map[string]interface{})["request"] != nil { - request := fromChannel(event["newDocument"].(map[string]interface{})["request"]).(*requestImpl) - return request.Response() + waiter := f.setNavigationWaiter(option.Timeout) + + eventData, err := waiter.WaitForEvent(f, "navigated", predicate).Wait() + if err != nil || eventData == nil { + return nil, err + } + + t := time.Until(deadline).Milliseconds() + if t > 0 { + err = f.waitForLoadStateImpl(string(*option.WaitUntil), Float(float64(t)), nil) + if err != nil { + return nil, err } } + event := eventData.(map[string]interface{}) + if event["newDocument"] != nil && event["newDocument"].(map[string]interface{})["request"] != nil { + request := fromChannel(event["newDocument"].(map[string]interface{})["request"]).(*requestImpl) + return request.Response() + } return nil, nil } +func (f *frameImpl) setNavigationWaiter(timeout *float64) *waiter { + waiter := newWaiter().WithTimeout(*timeout) + waiter.RejectOnEvent(f.page, "close", fmt.Errorf("Navigation failed because page was closed!")) + waiter.RejectOnEvent(f.page, "crash", fmt.Errorf("Navigation failed because page crashed!")) + waiter.RejectOnEvent(f.page, "framedetached", fmt.Errorf("Navigating frame was detached!"), func(payload interface{}) bool { + frame, ok := payload.(*frameImpl) + if ok && frame == f { + return true + } + return false + }) + return waiter +} + func (f *frameImpl) onFrameNavigated(ev map[string]interface{}) { f.Lock() f.url = ev["url"].(string) diff --git a/frame_locator.go b/frame_locator.go index 7fde4391..7de12dba 100644 --- a/frame_locator.go +++ b/frame_locator.go @@ -16,7 +16,7 @@ func (fl *frameLocatorImpl) First() FrameLocator { } func (fl *frameLocatorImpl) FrameLocator(selector string) FrameLocator { - return newFrameLocator(fl.frame, fl.frameSelector+" >> control=enter-frame >> "+selector) + return newFrameLocator(fl.frame, fl.frameSelector+" >> internal:control=enter-frame >> "+selector) } func (fl *frameLocatorImpl) Last() FrameLocator { @@ -24,7 +24,7 @@ func (fl *frameLocatorImpl) Last() FrameLocator { } func (fl *frameLocatorImpl) Locator(selector string, options ...LocatorLocatorOptions) (Locator, error) { - return newLocator(fl.frame, fl.frameSelector+" >> control=enter-frame >> "+selector, options...) + return newLocator(fl.frame, fl.frameSelector+" >> internal:control=enter-frame >> "+selector, options...) } func (fl *frameLocatorImpl) Nth(index int) FrameLocator { diff --git a/generated-enums.go b/generated-enums.go index e75cfba0..07b445ee 100644 --- a/generated-enums.go +++ b/generated-enums.go @@ -24,6 +24,7 @@ var ( ColorSchemeLight *ColorScheme = getColorScheme("light") ColorSchemeDark = getColorScheme("dark") ColorSchemeNoPreference = getColorScheme("no-preference") + ColorSchemeNoOverride = getColorScheme("no-override") ) func getForcedColors(in string) *ForcedColors { @@ -34,8 +35,34 @@ func getForcedColors(in string) *ForcedColors { type ForcedColors string var ( - ForcedColorsActive *ForcedColors = getForcedColors("active") - ForcedColorsNone = getForcedColors("none") + ForcedColorsActive *ForcedColors = getForcedColors("active") + ForcedColorsNone = getForcedColors("none") + ForcedColorsNoOverride = getForcedColors("no-override") +) + +func getHarContentPolicy(in string) *HarContentPolicy { + v := HarContentPolicy(in) + return &v +} + +type HarContentPolicy string + +var ( + HarContentPolicyOmit *HarContentPolicy = getHarContentPolicy("omit") + HarContentPolicyEmbed = getHarContentPolicy("embed") + HarContentPolicyAttach = getHarContentPolicy("attach") +) + +func getHarMode(in string) *HarMode { + v := HarMode(in) + return &v +} + +type HarMode string + +var ( + HarModeFull *HarMode = getHarMode("full") + HarModeMinimal = getHarMode("minimal") ) func getReducedMotion(in string) *ReducedMotion { @@ -48,6 +75,7 @@ type ReducedMotion string var ( ReducedMotionReduce *ReducedMotion = getReducedMotion("reduce") ReducedMotionNoPreference = getReducedMotion("no-preference") + ReducedMotionNoOverride = getReducedMotion("no-override") ) func getServiceWorkerPolicy(in string) *ServiceWorkerPolicy { @@ -179,6 +207,98 @@ var ( WaitForSelectorStateHidden = getWaitForSelectorState("hidden") ) +func getAriaRole(in string) *AriaRole { + v := AriaRole(in) + return &v +} + +type AriaRole string + +var ( + AriaRoleAlert *AriaRole = getAriaRole("alert") + AriaRoleAlertdialog = getAriaRole("alertdialog") + AriaRoleApplication = getAriaRole("application") + AriaRoleArticle = getAriaRole("article") + AriaRoleBanner = getAriaRole("banner") + AriaRoleBlockquote = getAriaRole("blockquote") + AriaRoleButton = getAriaRole("button") + AriaRoleCaption = getAriaRole("caption") + AriaRoleCell = getAriaRole("cell") + AriaRoleCheckbox = getAriaRole("checkbox") + AriaRoleCode = getAriaRole("code") + AriaRoleColumnheader = getAriaRole("columnheader") + AriaRoleCombobox = getAriaRole("combobox") + AriaRoleComplementary = getAriaRole("complementary") + AriaRoleContentinfo = getAriaRole("contentinfo") + AriaRoleDefinition = getAriaRole("definition") + AriaRoleDeletion = getAriaRole("deletion") + AriaRoleDialog = getAriaRole("dialog") + AriaRoleDirectory = getAriaRole("directory") + AriaRoleDocument = getAriaRole("document") + AriaRoleEmphasis = getAriaRole("emphasis") + AriaRoleFeed = getAriaRole("feed") + AriaRoleFigure = getAriaRole("figure") + AriaRoleForm = getAriaRole("form") + AriaRoleGeneric = getAriaRole("generic") + AriaRoleGrid = getAriaRole("grid") + AriaRoleGridcell = getAriaRole("gridcell") + AriaRoleGroup = getAriaRole("group") + AriaRoleHeading = getAriaRole("heading") + AriaRoleImg = getAriaRole("img") + AriaRoleInsertion = getAriaRole("insertion") + AriaRoleLink = getAriaRole("link") + AriaRoleList = getAriaRole("list") + AriaRoleListbox = getAriaRole("listbox") + AriaRoleListitem = getAriaRole("listitem") + AriaRoleLog = getAriaRole("log") + AriaRoleMain = getAriaRole("main") + AriaRoleMarquee = getAriaRole("marquee") + AriaRoleMath = getAriaRole("math") + AriaRoleMeter = getAriaRole("meter") + AriaRoleMenu = getAriaRole("menu") + AriaRoleMenubar = getAriaRole("menubar") + AriaRoleMenuitem = getAriaRole("menuitem") + AriaRoleMenuitemcheckbox = getAriaRole("menuitemcheckbox") + AriaRoleMenuitemradio = getAriaRole("menuitemradio") + AriaRoleNavigation = getAriaRole("navigation") + AriaRoleNone = getAriaRole("none") + AriaRoleNote = getAriaRole("note") + AriaRoleOption = getAriaRole("option") + AriaRoleParagraph = getAriaRole("paragraph") + AriaRolePresentation = getAriaRole("presentation") + AriaRoleProgressbar = getAriaRole("progressbar") + AriaRoleRadio = getAriaRole("radio") + AriaRoleRadiogroup = getAriaRole("radiogroup") + AriaRoleRegion = getAriaRole("region") + AriaRoleRow = getAriaRole("row") + AriaRoleRowgroup = getAriaRole("rowgroup") + AriaRoleRowheader = getAriaRole("rowheader") + AriaRoleScrollbar = getAriaRole("scrollbar") + AriaRoleSearch = getAriaRole("search") + AriaRoleSearchbox = getAriaRole("searchbox") + AriaRoleSeparator = getAriaRole("separator") + AriaRoleSlider = getAriaRole("slider") + AriaRoleSpinbutton = getAriaRole("spinbutton") + AriaRoleStatus = getAriaRole("status") + AriaRoleStrong = getAriaRole("strong") + AriaRoleSubscript = getAriaRole("subscript") + AriaRoleSuperscript = getAriaRole("superscript") + AriaRoleSwitch = getAriaRole("switch") + AriaRoleTab = getAriaRole("tab") + AriaRoleTable = getAriaRole("table") + AriaRoleTablist = getAriaRole("tablist") + AriaRoleTabpanel = getAriaRole("tabpanel") + AriaRoleTerm = getAriaRole("term") + AriaRoleTextbox = getAriaRole("textbox") + AriaRoleTime = getAriaRole("time") + AriaRoleTimer = getAriaRole("timer") + AriaRoleToolbar = getAriaRole("toolbar") + AriaRoleTooltip = getAriaRole("tooltip") + AriaRoleTree = getAriaRole("tree") + AriaRoleTreegrid = getAriaRole("treegrid") + AriaRoleTreeitem = getAriaRole("treeitem") +) + func getWaitUntilState(in string) *WaitUntilState { v := WaitUntilState(in) return &v @@ -214,9 +334,9 @@ func getMedia(in string) *Media { type Media string var ( - MediaScreen *Media = getMedia("screen") - MediaPrint = getMedia("print") - MediaNull = getMedia("null") + MediaScreen *Media = getMedia("screen") + MediaPrint = getMedia("print") + MediaNoOverride = getMedia("no-override") ) func getSameSiteAttribute(in string) *SameSiteAttribute { diff --git a/generated-interfaces.go b/generated-interfaces.go index 0dcf5df2..8f629b56 100644 --- a/generated-interfaces.go +++ b/generated-interfaces.go @@ -1,5 +1,157 @@ package playwright +// Exposes API that can be used for the Web API testing. This class is used for creating `APIRequestContext` instance +// which in turn can be used for sending web requests. An instance of this class can be obtained via +// [`property: Playwright.request`]. For more information see `APIRequestContext`. +type APIRequest interface { + EventEmitter + // Creates new instances of `APIRequestContext`. + NewContext(options ...APIRequestNewContextOptions) (APIRequestContext, error) +} + +// This API is used for the Web API testing. You can use it to trigger API endpoints, configure micro-services, +// prepare environment or the service to your e2e test. +// Each Playwright browser context has associated with it `APIRequestContext` instance which shares cookie storage +// with the browser context and can be accessed via [`property: BrowserContext.request`] or +// [`property: Page.request`]. It is also possible to create a new APIRequestContext instance manually by calling +// APIRequest.newContext(). +// **Cookie management** +// `APIRequestContext` returned by [`property: BrowserContext.request`] and [`property: Page.request`] shares cookie +// storage with the corresponding `BrowserContext`. Each API request will have `Cookie` header populated with the +// values from the browser context. If the API response contains `Set-Cookie` header it will automatically update +// `BrowserContext` cookies and requests made from the page will pick them up. This means that if you log in using +// this API, your e2e test will be logged in and vice versa. +// If you want API requests to not interfere with the browser cookies you should create a new `APIRequestContext` by +// calling APIRequest.newContext(). Such `APIRequestContext` object will have its own isolated cookie +// storage. +type APIRequestContext interface { + EventEmitter + // Sends HTTP(S) [DELETE](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE) request and returns its + // response. The method will populate request cookies from the context and update context cookies from the response. + // The method will automatically follow redirects. + Delete(url string, options ...APIRequestContextDeleteOptions) (APIResponse, error) + // All responses returned by APIRequestContext.get() and similar methods are stored in the memory, so that + // you can later call APIResponse.body(). This method discards all stored responses, and makes + // APIResponse.body() throw "Response disposed" error. + Dispose() error + // Sends HTTP(S) request and returns its response. The method will populate request cookies from the context and + // update context cookies from the response. The method will automatically follow redirects. JSON objects can be + // passed directly to the request. + // **Usage** + // ```python + // data = { + // "title": "Book Title", + // "body": "John Doe", + // } + // api_request_context.fetch("https://example.com/api/createBook", method="post", data=data) + // ``` + // The common way to send file(s) in the body of a request is to encode it as form fields with `multipart/form-data` + // encoding. You can achieve that with Playwright API like this: + // ```python + // api_request_context.fetch( + // "https://example.com/api/uploadScrip'", + // method="post", + // multipart={ + // "fileField": { + // "name": "f.js", + // "mimeType": "text/javascript", + // "buffer": b"console.log(2022);", + // }, + // }) + // ``` + Fetch(urlOrRequest interface{}, options ...APIRequestContextFetchOptions) (APIResponse, error) + // Sends HTTP(S) [GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) request and returns its + // response. The method will populate request cookies from the context and update context cookies from the response. + // The method will automatically follow redirects. + // **Usage** + // Request parameters can be configured with `params` option, they will be serialized into the URL search parameters: + // ```python + // query_params = { + // "isbn": "1234", + // "page": "23" + // } + // api_request_context.get("https://example.com/api/getText", params=query_params) + // ``` + Get(url string, options ...APIRequestContextGetOptions) (APIResponse, error) + // Sends HTTP(S) [HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) request and returns its + // response. The method will populate request cookies from the context and update context cookies from the response. + // The method will automatically follow redirects. + Head(url string, options ...APIRequestContextHeadOptions) (APIResponse, error) + // Sends HTTP(S) [POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) request and returns its + // response. The method will populate request cookies from the context and update context cookies from the response. + // The method will automatically follow redirects. + // **Usage** + // JSON objects can be passed directly to the request: + // ```python + // data = { + // "title": "Book Title", + // "body": "John Doe", + // } + // api_request_context.post("https://example.com/api/createBook", data=data) + // ``` + // To send form data to the server use `form` option. Its value will be encoded into the request body with + // `application/x-www-form-urlencoded` encoding (see below how to use `multipart/form-data` form encoding to send + // files): + // ```python + // formData = { + // "title": "Book Title", + // "body": "John Doe", + // } + // api_request_context.post("https://example.com/api/findBook", form=formData) + // ``` + // The common way to send file(s) in the body of a request is to upload them as form fields with `multipart/form-data` + // encoding. You can achieve that with Playwright API like this: + // ```python + // api_request_context.post( + // "https://example.com/api/uploadScrip'", + // multipart={ + // "fileField": { + // "name": "f.js", + // "mimeType": "text/javascript", + // "buffer": b"console.log(2022);", + // }, + // }) + // ``` + Post(url string, options ...APIRequestContextPostOptions) (APIResponse, error) + // Sends HTTP(S) [PUT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) request and returns its + // response. The method will populate request cookies from the context and update context cookies from the response. + // The method will automatically follow redirects. + Put(url string, options ...APIRequestContextPutOptions) (APIResponse, error) + // Sends HTTP(S) [PATCH](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH) request and returns its + // response. The method will populate request cookies from the context and update context cookies from the response. + // The method will automatically follow redirects. + Patch(url string, options ...APIRequestContextPatchOptions) (APIResponse, error) + // Returns storage state for this request context, contains current cookies and local storage snapshot if it was + // passed to the constructor. + StorageState(path ...string) (*StorageState, error) +} + +// `APIResponse` class represents responses returned by APIRequestContext.get() and similar methods. +type APIResponse interface { + // Returns the buffer with response body. + Body() ([]byte, error) + // Disposes the body of this response. If not called then the body will stay in memory until the context closes. + Dispose() error + // An object with all the response HTTP headers associated with this response. + Headers() map[string]string + // An array with all the request HTTP headers associated with this response. Header names are not lower-cased. Headers + // with multiple entries, such as `Set-Cookie`, appear in the array multiple times. + HeadersArray() HeadersArray + // Returns the JSON representation of response body. + // This method will throw if the response body is not parsable via `JSON.parse`. + JSON(v interface{}) error + // Contains a boolean stating whether the response was successful (status in the range 200-299) or not. + Ok() bool + // Contains the status code of the response (e.g., 200 for a success). + Status() int + // Contains the status text of the response (e.g. usually an "OK" for a success). + StatusText() string + // Returns the text representation of response body. + Text() (string, error) + // Contains the URL of the response. + URL() string +} + type BindingCall interface { Call(f BindingCallFunction) } @@ -7,31 +159,35 @@ type BindingCall interface { // A Browser is created via BrowserType.launch(). An example of using a `Browser` to create a `Page`: type Browser interface { EventEmitter - // In case this browser is obtained using BrowserType.launch(), closes the browser and all of its pages (if any - // were opened). - // In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from the - // browser server. - // > NOTE: This is similar to force quitting the browser. Therefore, you should call BrowserContext.close() on - // any `BrowserContext`'s you explicitly created earlier with Browser.newContext() **before** calling + // Get the browser type (chromium, firefox or webkit) that the browser belongs to. + BrowserType() BrowserType + // In case this browser is obtained using BrowserType.launch(), closes the browser and all of its pages (if + // any were opened). + // In case this browser is connected to, clears all created contexts belonging to this browser and disconnects from + // the browser server. + // **NOTE** This is similar to force quitting the browser. Therefore, you should call BrowserContext.close() + // on any `BrowserContext`'s you explicitly created earlier with Browser.newContext() **before** calling // Browser.close(). // The `Browser` object itself is considered to be disposed and cannot be used anymore. Close() error // Returns an array of all open browser contexts. In a newly created browser, this will return zero browser contexts. + // **Usage** Contexts() []BrowserContext // Indicates that the browser is connected. IsConnected() bool // Creates a new browser context. It won't share cookies/cache with other browser contexts. - // > NOTE: If directly using this method to create `BrowserContext`s, it is best practice to explicitly close the returned - // context via BrowserContext.close() when your code is done with the `BrowserContext`, and before calling - // Browser.close(). This will ensure the `context` is closed gracefully and any artifacts—like HARs and - // videos—are fully flushed and saved. + // **NOTE** If directly using this method to create `BrowserContext`s, it is best practice to explicitly close the + // returned context via BrowserContext.close() when your code is done with the `BrowserContext`, and before + // calling Browser.close(). This will ensure the `context` is closed gracefully and any artifacts—like HARs + // and videos—are fully flushed and saved. + // **Usage** NewContext(options ...BrowserNewContextOptions) (BrowserContext, error) // Creates a new page in a new browser context. Closing this page will close the context as well. - // This is a convenience API that should only be used for the single-page scenarios and short snippets. Production code and - // testing frameworks should explicitly create Browser.newContext() followed by the + // This is a convenience API that should only be used for the single-page scenarios and short snippets. Production + // code and testing frameworks should explicitly create Browser.newContext() followed by the // BrowserContext.newPage() to control their exact life times. NewPage(options ...BrowserNewContextOptions) (Page, error) - // > NOTE: CDP Sessions are only supported on Chromium-based browsers. + // **NOTE** CDP Sessions are only supported on Chromium-based browsers. // Returns the newly created browser session. NewBrowserCDPSession() (CDPSession, error) // Returns the browser version. @@ -48,8 +204,8 @@ type Browser interface { // https://github.com/aslushnikov/getting-started-with-cdp/blob/master/README.md type CDPSession interface { EventEmitter - // Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be used to - // send messages. + // Detaches the CDPSession from the target. Once detached, the CDPSession object won't emit any events and can't be + // used to send messages. Detach() error Send(method string, params map[string]interface{}) (interface{}, error) } @@ -57,21 +213,23 @@ type CDPSession interface { // BrowserContexts provide a way to operate multiple independent browser sessions. // If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser // context. -// Playwright allows creating "incognito" browser contexts with Browser.newContext() method. "Incognito" browser -// contexts don't write any browsing data to disk. +// Playwright allows creating "incognito" browser contexts with Browser.newContext() method. "Incognito" +// browser contexts don't write any browsing data to disk. type BrowserContext interface { EventEmitter - // Adds cookies into this browser context. All pages within this context will have these cookies installed. Cookies can be - // obtained via BrowserContext.cookies(). - AddCookies(cookies ...BrowserContextAddCookiesOptionsCookies) error + // Adds cookies into this browser context. All pages within this context will have these cookies installed. Cookies + // can be obtained via BrowserContext.cookies(). + // **Usage** + AddCookies(cookies ...OptionalCookie) error // Adds a script which would be evaluated in one of the following scenarios: // - Whenever a page is created in the browser context or is navigated. // - Whenever a child frame is attached or navigated in any page in the browser context. In this case, the script is // evaluated in the context of the newly attached frame. - // The script is evaluated after the document was created but before any of its scripts were run. This is useful to amend - // the JavaScript environment, e.g. to seed `Math.random`. + // The script is evaluated after the document was created but before any of its scripts were run. This is useful to + // amend the JavaScript environment, e.g. to seed `Math.random`. + // **Usage** // An example of overriding `Math.random` before the page loads: - // > NOTE: The order of evaluation of multiple scripts installed via BrowserContext.addInitScript() and + // **NOTE** The order of evaluation of multiple scripts installed via BrowserContext.addInitScript() and // Page.addInitScript() is not defined. AddInitScript(script BrowserContextAddInitScriptOptions) error // Returns the browser instance of the context. If it was launched as a persistent context null gets returned. @@ -79,20 +237,25 @@ type BrowserContext interface { // Clears context cookies. ClearCookies() error // Clears all permission overrides for the browser context. + // **Usage** ClearPermissions() error // Closes the browser context. All the pages that belong to the browser context will be closed. - // > NOTE: The default browser context cannot be closed. + // **NOTE** The default browser context cannot be closed. Close() error - // If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those URLs - // are returned. - Cookies(urls ...string) ([]*BrowserContextCookiesResult, error) - ExpectEvent(event string, cb func() error) (interface{}, error) + // If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those + // URLs are returned. + Cookies(urls ...string) ([]*Cookie, error) + // Waits for event to fire and passes its value into the predicate function. Returns when the predicate returns truthy + // value. Will throw an error if the context closes before the event is fired. Returns the event data value. + // **Usage** + ExpectEvent(event string, cb func() error, options ...BrowserContextWaitForEventOptions) (interface{}, error) // The method adds a function called `name` on the `window` object of every frame in every page in the context. When - // called, the function executes `callback` and returns a [Promise] which resolves to the return value of `callback`. If - // the `callback` returns a [Promise], it will be awaited. - // The first argument of the `callback` function contains information about the caller: `{ browserContext: BrowserContext, - // page: Page, frame: Frame }`. + // called, the function executes `callback` and returns a [Promise] which resolves to the return value of `callback`. + // If the `callback` returns a [Promise], it will be awaited. + // The first argument of the `callback` function contains information about the caller: `{ browserContext: + // BrowserContext, page: Page, frame: Frame }`. // See Page.exposeBinding() for page-only version. + // **Usage** // An example of exposing page URL to all frames in all pages in the context: // An example of passing an element handle: ExposeBinding(name string, binding BindingCallFunction, handle ...bool) error @@ -100,12 +263,13 @@ type BrowserContext interface { // called, the function executes `callback` and returns a [Promise] which resolves to the return value of `callback`. // If the `callback` returns a [Promise], it will be awaited. // See Page.exposeFunction() for page-only version. + // **Usage** // An example of adding a `sha256` function to all pages in the context: ExposeFunction(name string, binding ExposedFunction) error // Grants specified permissions to the browser context. Only grants corresponding permissions to the given origin if // specified. GrantPermissions(permissions []string, options ...BrowserContextGrantPermissionsOptions) error - // > NOTE: CDP sessions are only supported on Chromium-based browsers. + // **NOTE** CDP sessions are only supported on Chromium-based browsers. // Returns the newly created session. NewCDPSession(page Page) (CDPSession, error) // Creates a new page in the browser context. @@ -119,87 +283,96 @@ type BrowserContext interface { // - Page.reload() // - Page.setContent() // - Page.waitForNavigation() - // > NOTE: Page.setDefaultNavigationTimeout`] and [`method: Page.setDefaultTimeout() take priority over + // **NOTE** Page.setDefaultNavigationTimeout`] and [`method: Page.setDefaultTimeout() take priority over // BrowserContext.setDefaultNavigationTimeout(). SetDefaultNavigationTimeout(timeout float64) // This setting will change the default maximum time for all the methods accepting `timeout` option. - // > NOTE: Page.setDefaultNavigationTimeout`], [`method: Page.setDefaultTimeout() and - // BrowserContext.setDefaultNavigationTimeout`] take priority over [`method: BrowserContext.setDefaultTimeout(). + // **NOTE** Page.setDefaultNavigationTimeout`], [`method: Page.setDefaultTimeout() and + // BrowserContext.setDefaultNavigationTimeout() take priority over + // BrowserContext.setDefaultTimeout(). SetDefaultTimeout(timeout float64) - // The extra HTTP headers will be sent with every request initiated by any page in the context. These headers are merged - // with page-specific extra HTTP headers set with Page.setExtraHTTPHeaders(). If page overrides a particular - // header, page-specific header value will be used instead of the browser context header value. - // > NOTE: BrowserContext.setExtraHTTPHeaders() does not guarantee the order of headers in the outgoing requests. + // The extra HTTP headers will be sent with every request initiated by any page in the context. These headers are + // merged with page-specific extra HTTP headers set with Page.setExtraHTTPHeaders(). If page overrides a + // particular header, page-specific header value will be used instead of the browser context header value. + // **NOTE** BrowserContext.setExtraHTTPHeaders() does not guarantee the order of headers in the outgoing + // requests. SetExtraHTTPHeaders(headers map[string]string) error // Sets the context's geolocation. Passing `null` or `undefined` emulates position unavailable. - // > NOTE: Consider using BrowserContext.grantPermissions() to grant permissions for the browser context pages to - // read its geolocation. - SetGeolocation(gelocation *SetGeolocationOptions) error + // **Usage** + // **NOTE** Consider using BrowserContext.grantPermissions() to grant permissions for the browser context + // pages to read its geolocation. + SetGeolocation(gelocation *Geolocation) error ResetGeolocation() error - // Routing provides the capability to modify network requests that are made by any page in the browser context. Once route - // is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. - // > NOTE: BrowserContext.route() will not intercept requests intercepted by Service Worker. See - // [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when using - // request interception by setting `Browser.newContext.serviceWorkers` to `'block'`. + // Routing provides the capability to modify network requests that are made by any page in the browser context. Once + // route is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. + // **NOTE** BrowserContext.route() will not intercept requests intercepted by Service Worker. See + // [this](https://github.com/microsoft/playwright/issues/1090) issue. We recommend disabling Service Workers when + // using request interception by setting `Browser.newContext.serviceWorkers` to `'block'`. + // **Usage** // An example of a naive handler that aborts all image requests: // or the same snippet using a regex pattern instead: - // It is possible to examine the request to decide the route action. For example, mocking all requests that contain some - // post data, and leaving all other requests as is: - // Page routes (set up with Page.route()) take precedence over browser context routes when request matches both - // handlers. + // It is possible to examine the request to decide the route action. For example, mocking all requests that contain + // some post data, and leaving all other requests as is: + // Page routes (set up with Page.route()) take precedence over browser context routes when request matches + // both handlers. // To remove a route with its handler you can use BrowserContext.unroute(). - // > NOTE: Enabling routing disables http cache. - Route(url interface{}, handler routeHandler) error + // **NOTE** Enabling routing disables http cache. + Route(url interface{}, handler routeHandler, times ...int) error SetOffline(offline bool) error // Returns storage state for this browser context, contains current cookies and local storage snapshot. StorageState(path ...string) (*StorageState, error) - // Removes a route created with BrowserContext.route(). When `handler` is not specified, removes all routes for - // the `url`. + // Removes a route created with BrowserContext.route(). When `handler` is not specified, removes all routes + // for the `url`. Unroute(url interface{}, handler ...routeHandler) error - // Waits for event to fire and passes its value into the predicate function. Returns when the predicate returns truthy - // value. Will throw an error if the context closes before the event is fired. Returns the event data value. - WaitForEvent(event string, predicate ...interface{}) interface{} + // **NOTE** In most cases, you should use BrowserContext.waitForEvent(). + // Waits for given `event` to fire. If predicate is provided, it passes event's value into the `predicate` function + // and waits for `predicate(event)` to return a truthy value. Will throw an error if the browser context is closed + // before the `event` is fired. + WaitForEvent(event string, options ...BrowserContextWaitForEventOptions) (interface{}, error) Tracing() Tracing - // > NOTE: Background pages are only supported on Chromium-based browsers. + // **NOTE** Background pages are only supported on Chromium-based browsers. // All existing background pages in the context. BackgroundPages() []Page } -// API for collecting and saving Playwright traces. Playwright traces can be opened in [Trace Viewer](../trace-viewer.md) -// after Playwright script runs. +// API for collecting and saving Playwright traces. Playwright traces can be opened in +// [Trace Viewer](../trace-viewer.md) after Playwright script runs. // Start recording a trace before performing actions. At the end, stop tracing and save it to a file. type Tracing interface { // Start tracing. + // **Usage** Start(options ...TracingStartOptions) error // Stop tracing. Stop(options ...TracingStopOptions) error // Start a new trace chunk. If you'd like to record multiple traces on the same `BrowserContext`, use // Tracing.start`] once, and then create multiple trace chunks with [`method: Tracing.startChunk() and // Tracing.stopChunk(). + // **Usage** StartChunk(options ...TracingStartChunkOptions) error // Stop the trace chunk. See Tracing.startChunk() for more details about multiple trace chunks. StopChunk(options ...TracingStopChunkOptions) error } -// BrowserType provides methods to launch a specific browser instance or connect to an existing one. The following is a -// typical example of using Playwright to drive automation: +// BrowserType provides methods to launch a specific browser instance or connect to an existing one. The following is +// a typical example of using Playwright to drive automation: type BrowserType interface { // A path where Playwright expects to find a bundled browser executable. ExecutablePath() string // Returns the browser instance. + // **Usage** // You can use `ignoreDefaultArgs` to filter out `--mute-audio` from default arguments: - // > **Chromium-only** Playwright can also be used to control the Google Chrome or Microsoft Edge browsers, but it works - // best with the version of Chromium it is bundled with. There is no guarantee it will work with any other version. Use - // `executablePath` option with extreme caution. + // > **Chromium-only** Playwright can also be used to control the Google Chrome or Microsoft Edge browsers, but it + // works best with the version of Chromium it is bundled with. There is no guarantee it will work with any other + // version. Use `executablePath` option with extreme caution. // > // > If Google Chrome (rather than Chromium) is preferred, a // [Chrome Canary](https://www.google.com/chrome/browser/canary.html) or // [Dev Channel](https://www.chromium.org/getting-involved/dev-channel) build is suggested. // > - // > Stock browsers like Google Chrome and Microsoft Edge are suitable for tests that require proprietary media codecs for - // video playback. See - // [this article](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for other - // differences between Chromium and Chrome. + // > Stock browsers like Google Chrome and Microsoft Edge are suitable for tests that require proprietary media codecs + // for video playback. See + // [this article](https://www.howtogeek.com/202825/what%E2%80%99s-the-difference-between-chromium-and-chrome/) for + // other differences between Chromium and Chrome. // [This article](https://chromium.googlesource.com/chromium/src/+/lkgr/docs/chromium_browser_vs_google_chrome.md) // describes some differences for Linux users. Launch(options ...BrowserTypeLaunchOptions) (Browser, error) @@ -215,12 +388,13 @@ type BrowserType interface { Connect(url string, options ...BrowserTypeConnectOptions) (Browser, error) // This method attaches Playwright to an existing browser instance using the Chrome DevTools Protocol. // The default browser context is accessible via Browser.contexts(). - // > NOTE: Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. + // **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. + // **Usage** ConnectOverCDP(endpointURL string, options ...BrowserTypeConnectOverCDPOptions) (Browser, error) } -// `ConsoleMessage` objects are dispatched by page via the [`event: Page.console`] event. For each console messages logged -// in the page there will be corresponding event in the Playwright context. +// `ConsoleMessage` objects are dispatched by page via the [`event: Page.console`] event. For each console messages +// logged in the page there will be corresponding event in the Playwright context. type ConsoleMessage interface { // List of arguments passed to a `console` function call. See also [`event: Page.console`]. Args() []JSHandle @@ -229,17 +403,17 @@ type ConsoleMessage interface { // The text of the console message. Text() string // One of the following values: `'log'`, `'debug'`, `'info'`, `'error'`, `'warning'`, `'dir'`, `'dirxml'`, `'table'`, - // `'trace'`, `'clear'`, `'startGroup'`, `'startGroupCollapsed'`, `'endGroup'`, `'assert'`, `'profile'`, `'profileEnd'`, - // `'count'`, `'timeEnd'`. + // `'trace'`, `'clear'`, `'startGroup'`, `'startGroupCollapsed'`, `'endGroup'`, `'assert'`, `'profile'`, + // `'profileEnd'`, `'count'`, `'timeEnd'`. Type() string } // `Dialog` objects are dispatched by page via the [`event: Page.dialog`] event. // An example of using `Dialog` class: -// > NOTE: Dialogs are dismissed automatically, unless there is a [`event: Page.dialog`] listener. When listener is -// present, it **must** either Dialog.accept`] or [`method: Dialog.dismiss() the dialog - otherwise the page will -// [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the dialog, and -// actions like click will never finish. +// **NOTE** Dialogs are dismissed automatically, unless there is a [`event: Page.dialog`] listener. When listener is +// present, it **must** either Dialog.accept`] or [`method: Dialog.dismiss() the dialog - otherwise the page +// will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#never_blocking) waiting for the +// dialog, and actions like click will never finish. type Dialog interface { // Returns when the dialog has been accepted. Accept(texts ...string) error @@ -261,19 +435,19 @@ type Download interface { Delete() error // Returns download error if any. Will wait for the download to finish if necessary. Failure() (string, error) - // Returns path to the downloaded file in case of successful download. The method will wait for the download to finish if - // necessary. The method throws when connected remotely. - // Note that the download's file name is a random GUID, use Download.suggestedFilename() to get suggested file - // name. + // Returns path to the downloaded file in case of successful download. The method will wait for the download to finish + // if necessary. The method throws when connected remotely. + // Note that the download's file name is a random GUID, use Download.suggestedFilename() to get suggested + // file name. Path() (string, error) - // Copy the download to a user-specified path. It is safe to call this method while the download is still in progress. Will - // wait for the download to finish if necessary. + // Copy the download to a user-specified path. It is safe to call this method while the download is still in progress. + // Will wait for the download to finish if necessary. SaveAs(path string) error String() string // Returns suggested filename for this download. It is typically computed by the browser from the - // [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) response header - // or the `download` attribute. See the spec on [whatwg](https://html.spec.whatwg.org/#downloading-resources). Different - // browsers can use different logic for computing it. + // [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) response + // header or the `download` attribute. See the spec on [whatwg](https://html.spec.whatwg.org/#downloading-resources). + // Different browsers can use different logic for computing it. SuggestedFilename() string // Returns downloaded url. URL() string @@ -284,55 +458,56 @@ type Download interface { Cancel() error } -// ElementHandle represents an in-page DOM element. ElementHandles can be created with the Page.querySelector() -// method. -// > NOTE: The use of ElementHandle is discouraged, use `Locator` objects and web-first assertions instead. +// ElementHandle represents an in-page DOM element. ElementHandles can be created with the +// Page.querySelector() method. +// **NOTE** The use of ElementHandle is discouraged, use `Locator` objects and web-first assertions instead. // ElementHandle prevents DOM element from garbage collection unless the handle is disposed with // JSHandle.dispose(). ElementHandles are auto-disposed when their origin frame gets navigated. // ElementHandle instances can be used as an argument in Page.evalOnSelector`] and [`method: Page.evaluate() // methods. -// The difference between the `Locator` and ElementHandle is that the ElementHandle points to a particular element, while -// `Locator` captures the logic of how to retrieve an element. +// The difference between the `Locator` and ElementHandle is that the ElementHandle points to a particular element, +// while `Locator` captures the logic of how to retrieve an element. // In the example below, handle points to a particular DOM element on page. If that element changes text or is used by -// React to render an entirely different component, handle is still pointing to that very DOM element. This can lead to -// unexpected behaviors. -// With the locator, every time the `element` is used, up-to-date DOM element is located in the page using the selector. So -// in the snippet below, underlying DOM element is going to be located twice. +// React to render an entirely different component, handle is still pointing to that very DOM element. This can lead +// to unexpected behaviors. +// With the locator, every time the `element` is used, up-to-date DOM element is located in the page using the +// selector. So in the snippet below, underlying DOM element is going to be located twice. type ElementHandle interface { JSHandle // This method returns the bounding box of the element, or `null` if the element is not visible. The bounding box is // calculated relative to the main frame viewport - which is usually the same as the browser window. // Scrolling affects the returned bounding box, similarly to - // [Element.getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). That - // means `x` and/or `y` may be negative. + // [Element.getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). + // That means `x` and/or `y` may be negative. // Elements from child frames return the bounding box relative to the main frame, unlike the // [Element.getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect). - // Assuming the page is static, it is safe to use bounding box coordinates to perform input. For example, the following - // snippet should click the center of the element. + // Assuming the page is static, it is safe to use bounding box coordinates to perform input. For example, the + // following snippet should click the center of the element. + // **Usage** BoundingBox() (*Rect, error) // This method checks the element by performing the following steps: - // 1. Ensure that element is a checkbox or a radio input. If not, this method throws. If the element is already checked, - // this method returns immediately. + // 1. Ensure that element is a checkbox or a radio input. If not, this method throws. If the element is already + // checked, this method returns immediately. // 1. Wait for [actionability](../actionability.md) checks on the element, unless `force` option is set. // 1. Scroll the element into view if needed. // 1. Use [`property: Page.mouse`] to click in the center of the element. // 1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set. // 1. Ensure that the element is now checked. If not, this method throws. // If the element is detached from the DOM at any moment during the action, this method throws. - // When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`. Passing - // zero timeout disables this. + // When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`. + // Passing zero timeout disables this. Check(options ...ElementHandleCheckOptions) error // This method checks or unchecks an element by performing the following steps: // 1. Ensure that element is a checkbox or a radio input. If not, this method throws. // 1. If the element already has the right checked state, this method returns immediately. - // 1. Wait for [actionability](../actionability.md) checks on the matched element, unless `force` option is set. If the - // element is detached during the checks, the whole action is retried. + // 1. Wait for [actionability](../actionability.md) checks on the matched element, unless `force` option is set. If + // the element is detached during the checks, the whole action is retried. // 1. Scroll the element into view if needed. // 1. Use [`property: Page.mouse`] to click in the center of the element. // 1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set. // 1. Ensure that the element is now checked or unchecked. If not, this method throws. - // When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`. Passing - // zero timeout disables this. + // When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`. + // Passing zero timeout disables this. SetChecked(checked bool, options ...ElementHandleSetCheckedOptions) error // This method clicks the element by performing the following steps: // 1. Wait for [actionability](../actionability.md) checks on the element, unless `force` option is set. @@ -340,8 +515,8 @@ type ElementHandle interface { // 1. Use [`property: Page.mouse`] to click in the center of the element, or the specified `position`. // 1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set. // If the element is detached from the DOM at any moment during the action, this method throws. - // When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`. Passing - // zero timeout disables this. + // When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`. + // Passing zero timeout disables this. Click(options ...ElementHandleClickOptions) error // Returns the content frame for element handles referencing iframe nodes, or `null` otherwise ContentFrame() (Frame, error) @@ -349,18 +524,19 @@ type ElementHandle interface { // 1. Wait for [actionability](../actionability.md) checks on the element, unless `force` option is set. // 1. Scroll the element into view if needed. // 1. Use [`property: Page.mouse`] to double click in the center of the element, or the specified `position`. - // 1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set. Note that if the - // first click of the `dblclick()` triggers a navigation event, this method will throw. + // 1. Wait for initiated navigations to either succeed or fail, unless `noWaitAfter` option is set. Note that if + // the first click of the `dblclick()` triggers a navigation event, this method will throw. // If the element is detached from the DOM at any moment during the action, this method throws. - // When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`. Passing - // zero timeout disables this. - // > NOTE: `elementHandle.dblclick()` dispatches two `click` events and a single `dblclick` event. + // When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`. + // Passing zero timeout disables this. + // **NOTE** `elementHandle.dblclick()` dispatches two `click` events and a single `dblclick` event. Dblclick(options ...ElementHandleDblclickOptions) error // The snippet below dispatches the `click` event on the element. Regardless of the visibility state of the element, // `click` is dispatched. This is equivalent to calling // [element.click()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click). - // Under the hood, it creates an instance of an event based on the given `type`, initializes it with `eventInit` properties - // and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default. + // **Usage** + // Under the hood, it creates an instance of an event based on the given `type`, initializes it with `eventInit` + // properties and dispatches it on the element. Events are `composed`, `cancelable` and bubble by default. // Since `eventInit` is event-specific, please refer to the events documentation for the lists of initial properties: // - [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) // - [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) @@ -372,19 +548,18 @@ type ElementHandle interface { // You can also specify `JSHandle` as the property value if you want live objects to be passed into the event: DispatchEvent(typ string, initObjects ...interface{}) error // Returns the return value of `expression`. - // The method finds an element matching the specified selector in the `ElementHandle`s subtree and passes it as a first - // argument to `expression`. See [Working with selectors](../selectors.md) for more details. If no elements match the - // selector, the method throws an error. - // If `expression` returns a [Promise], then ElementHandle.evalOnSelector() would wait for the promise to resolve - // and return its value. - // Examples: + // The method finds an element matching the specified selector in the `ElementHandle`s subtree and passes it as a + // first argument to `expression`. If no elements match the selector, the method throws an error. + // If `expression` returns a [Promise], then ElementHandle.evalOnSelector() would wait for the promise to + // resolve and return its value. + // **Usage** EvalOnSelector(selector string, expression string, options ...interface{}) (interface{}, error) // Returns the return value of `expression`. - // The method finds all elements matching the specified selector in the `ElementHandle`'s subtree and passes an array of - // matched elements as a first argument to `expression`. See [Working with selectors](../selectors.md) for more details. + // The method finds all elements matching the specified selector in the `ElementHandle`'s subtree and passes an array + // of matched elements as a first argument to `expression`. // If `expression` returns a [Promise], then ElementHandle.evalOnSelectorAll() would wait for the promise to // resolve and return its value. - // Examples: + // **Usage** // ```html //
//
Hello!
@@ -392,10 +567,10 @@ type ElementHandle interface { //
// ``` EvalOnSelectorAll(selector string, expression string, options ...interface{}) (interface{}, error) - // This method waits for [actionability](../actionability.md) checks, focuses the element, fills it and triggers an `input` - // event after filling. Note that you can pass an empty string to clear the input field. - // If the target element is not an ``, `