Skip to content

Commit

Permalink
feat: handle WaitFor error and timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
canstand committed May 19, 2023
1 parent bda8ca7 commit 60deecd
Show file tree
Hide file tree
Showing 18 changed files with 553 additions and 155 deletions.
10 changes: 5 additions & 5 deletions browser_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ func (b *browserContextImpl) ExposeFunction(name string, binding ExposedFunction
}

func (b *browserContextImpl) Route(url interface{}, handler routeHandler, times ...int) error {
b.routes = append(b.routes, newRouteHandlerEntry(newURLMatcher(url), handler, times...))
b.routes = append(b.routes, newRouteHandlerEntry(newURLMatcher(url, b.options["baseURL"]), handler, times...))
return b.updateInterceptionPatterns()
}

Expand Down Expand Up @@ -242,8 +242,8 @@ func (b *browserContextImpl) RouteFromHAR(har string, options ...BrowserContextR
return router.addContextRoute(b)
}

func (b *browserContextImpl) WaitForEvent(event string, predicate ...interface{}) interface{} {
return <-waitForEvent(b, event, predicate...)
func (b *browserContextImpl) WaitForEvent(event string, predicate ...interface{}) (interface{}, error) {
return newWaiter().WaitForEvent(b, event, predicate...).Wait()
}

func (b *browserContextImpl) ExpectEvent(event string, cb func() error) (interface{}, error) {
Expand Down Expand Up @@ -467,7 +467,7 @@ func (b *browserContextImpl) OnBackgroundPage(ev map[string]interface{}) {
p.browserContext = b
b.backgroundPages = append(b.backgroundPages, p)
b.Unlock()
b.Emit("backgroundPage", p)
b.Emit("backgroundpage", p)
}

func (b *browserContextImpl) BackgroundPages() []Page {
Expand Down Expand Up @@ -567,7 +567,7 @@ func newBrowserContext(parent *channelOwner, objectType string, guid string, ini
bt.channel.On("route", func(params map[string]interface{}) {
bt.onRoute(fromChannel(params["route"]).(*routeImpl))
})
bt.channel.On("backgroundPage", bt.OnBackgroundPage)
bt.channel.On("backgroundpage", bt.OnBackgroundPage)
bt.setEventSubscriptionMapping(map[string]string{
"request": "request",
"response": "response",
Expand Down
21 changes: 19 additions & 2 deletions expect_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,35 @@ import (

func newExpectWrapper(f interface{}, args []interface{}, cb func() error) (interface{}, error) {
val := make(chan interface{}, 1)
errChan := make(chan error, 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()
var evVal interface{}
if len(result) > 0 {
evVal = result[0].Interface()
}
if len(result) > 1 {
errVal := result[1].Interface()
err, ok := errVal.(error)
if ok && err != nil {
errChan <- err
return
}
}
val <- evVal
}()

if err := cb(); err != nil {
return nil, err
}
return <-val, nil
select {
case err := <-errChan:
return nil, err
case val := <-val:
return val, nil
}
}
84 changes: 58 additions & 26 deletions frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,31 @@ 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)
}

func (f *frameImpl) waitForLoadStateImpl(state string, timeout *float64) 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()
_, err := waiter.Wait()
return err
}

func (f *frameImpl) WaitForURL(url string, options ...FrameWaitForURLOptions) error {
Expand All @@ -150,8 +158,8 @@ 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, predicate ...interface{}) (interface{}, error) {
return newWaiter().WaitForEvent(f, event, predicate...).Wait()
}

func (f *frameImpl) WaitForNavigation(options ...PageWaitForNavigationOptions) (Response, error) {
Expand All @@ -165,10 +173,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{})
Expand All @@ -177,19 +185,43 @@ 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)))
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)
Expand Down
15 changes: 8 additions & 7 deletions generated-interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ type BrowserContext interface {
// **NOTE** Consider using BrowserContext.grantPermissions() to grant permissions for the browser context
// pages to read its geolocation.
SetGeolocation(gelocation *Geolocation) error
// API testing helper associated with this context. Requests made with this API will use context cookies.
Request() APIRequestContext
ResetGeolocation() error
// Routing provides the capability to modify network requests that are made by any page in the browser context. Once
Expand Down Expand Up @@ -331,7 +332,7 @@ type BrowserContext interface {
// 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**
WaitForEvent(event string, predicate ...interface{}) interface{}
WaitForEvent(event string, predicate ...interface{}) (interface{}, error)
Tracing() Tracing
// **NOTE** Background pages are only supported on Chromium-based browsers.
// All existing background pages in the context.
Expand Down Expand Up @@ -1028,7 +1029,7 @@ type Frame interface {
// When all steps combined have not finished during the specified `timeout`, this method throws a `TimeoutError`.
// Passing zero timeout disables this.
Uncheck(selector string, options ...FrameUncheckOptions) error
WaitForEvent(event string, predicate ...interface{}) interface{}
WaitForEvent(event string, predicate ...interface{}) (interface{}, error)
// Returns when the `expression` returns a truthy value, returns that value.
// **Usage**
// The Frame.waitForFunction() can be used to observe viewport size change:
Expand All @@ -1039,7 +1040,7 @@ type Frame interface {
// committed when this method is called. If current document has already reached the required state, resolves
// immediately.
// **Usage**
WaitForLoadState(given ...string)
WaitForLoadState(options ...PageWaitForLoadStateOptions) error
// Waits for the frame navigation and returns the main resource response. In case of multiple redirects, the
// navigation will resolve with the response of the last redirect. In case of navigation to a different anchor or
// navigation due to History API usage, the navigation will resolve with `null`.
Expand Down Expand Up @@ -1970,7 +1971,7 @@ type Page interface {
// 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 page is closed before the event is fired. Returns the event data value.
// **Usage**
WaitForEvent(event string, predicate ...interface{}) interface{}
WaitForEvent(event string, predicate ...interface{}) (interface{}, error)
// Returns when the `expression` returns a truthy value. It resolves to a JSHandle of the truthy value.
// **Usage**
// The Page.waitForFunction() can be used to observe viewport size change:
Expand All @@ -1981,7 +1982,7 @@ type Page interface {
// committed when this method is called. If current document has already reached the required state, resolves
// immediately.
// **Usage**
WaitForLoadState(state ...string)
WaitForLoadState(options ...PageWaitForLoadStateOptions) error
// Waits for the main frame navigation and returns the main resource response. In case of multiple redirects, the
// navigation will resolve with the response of the last redirect. In case of navigation to a different anchor or
// navigation due to History API usage, the navigation will resolve with `null`.
Expand Down Expand Up @@ -2213,7 +2214,7 @@ type WebSocket interface {
URL() string
// 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 webSocket is closed before the event is fired. Returns the event data value.
WaitForEvent(event string, predicate ...interface{}) interface{}
WaitForEvent(event string, predicate ...interface{}) (interface{}, error)
}

// When browser context is created with the `recordVideo` option, each page has a video object associated with it.
Expand Down Expand Up @@ -2247,6 +2248,6 @@ type Worker interface {
// Worker.evaluateHandle() would wait for the promise to resolve and return its value.
EvaluateHandle(expression string, options ...interface{}) (JSHandle, error)
URL() string
WaitForEvent(event string, predicate ...interface{}) interface{}
WaitForEvent(event string, predicate ...interface{}) (interface{}, error)
ExpectEvent(event string, cb func() error, predicates ...interface{}) (interface{}, error)
}
16 changes: 16 additions & 0 deletions generated-structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1724,6 +1724,14 @@ type FrameWaitForFunctionOptions struct {
Timeout *float64 `json:"timeout"`
}
type FrameWaitForLoadStateOptions struct {
// Optional load state to wait for, defaults to `load`. If the state has been already
// reached while loading current document, the method resolves immediately. Can be
// one of:
// `'load'` - wait for the `load` event to be fired.
// `'domcontentloaded'` - wait for the `DOMContentLoaded` event to be fired.
// `'networkidle'` - wait until there are no network connections for at least `500`
// ms.
State *LoadState `json:"state"`
// Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable
// timeout. The default value can be changed by using the BrowserContext.SetDefaultNavigationTimeout(),
// BrowserContext.SetDefaultTimeout(), Page.SetDefaultNavigationTimeout() or Page.SetDefaultTimeout()
Expand Down Expand Up @@ -3235,6 +3243,14 @@ type PageWaitForFunctionOptions struct {
Timeout *float64 `json:"timeout"`
}
type PageWaitForLoadStateOptions struct {
// Optional load state to wait for, defaults to `load`. If the state has been already
// reached while loading current document, the method resolves immediately. Can be
// one of:
// `'load'` - wait for the `load` event to be fired.
// `'domcontentloaded'` - wait for the `DOMContentLoaded` event to be fired.
// `'networkidle'` - wait until there are no network connections for at least `500`
// ms.
State *LoadState `json:"state"`
// Maximum operation time in milliseconds, defaults to 30 seconds, pass `0` to disable
// timeout. The default value can be changed by using the BrowserContext.SetDefaultNavigationTimeout(),
// BrowserContext.SetDefaultTimeout(), Page.SetDefaultNavigationTimeout() or Page.SetDefaultTimeout()
Expand Down
42 changes: 14 additions & 28 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package playwright

import (
"fmt"
"path"
"reflect"
"regexp"
"strings"
Expand Down Expand Up @@ -194,7 +195,19 @@ type urlMatcher struct {
urlOrPredicate interface{}
}

func newURLMatcher(urlOrPredicate interface{}) *urlMatcher {
func newURLMatcher(urlOrPredicate, baseURL interface{}) *urlMatcher {
if baseURL != nil {
url, ok := urlOrPredicate.(string)
if ok && !strings.HasPrefix(url, "*") {
base, ok := baseURL.(*string)
if ok {
url = path.Join(*base, url)
return &urlMatcher{
urlOrPredicate: url,
}
}
}
}
return &urlMatcher{
urlOrPredicate: urlOrPredicate,
}
Expand Down Expand Up @@ -371,33 +384,6 @@ func newTimeoutSettings(parent *timeoutSettings) *timeoutSettings {
}
}

func waitForEvent(emitter EventEmitter, event string, predicate ...interface{}) <-chan interface{} {
evChan := make(chan interface{}, 1)
removeHandler := make(chan bool, 1)
handler := func(ev ...interface{}) {
if len(predicate) == 0 {
if len(ev) == 1 {
evChan <- ev[0]
} else {
evChan <- nil
}
removeHandler <- true
} else if len(predicate) == 1 {
result := reflect.ValueOf(predicate[0]).Call([]reflect.Value{reflect.ValueOf(ev[0])})
if result[0].Bool() {
evChan <- ev[0]
removeHandler <- true
}
}
}
go func() {
<-removeHandler
emitter.RemoveListener(event, handler)
}()
emitter.On(event, handler)
return evChan
}

// SelectOptionValues is the option struct for ElementHandle.Select() etc.
type SelectOptionValues struct {
ValuesOrLabels *[]string
Expand Down
Loading

0 comments on commit 60deecd

Please sign in to comment.