Skip to content
This repository was archived by the owner on Jan 30, 2025. It is now read-only.

Commit 9b08fdb

Browse files
Ivan Mirićimiric
authored andcommitted
Setup waitForNavigation waiters outside of the promise
This (mostly) ensures that the waiters are started before `waitForNavigation()` returns, and that the promise handler simply waits for them. It reduces the chances of a race condition where e.g. a navigation event produced by a `click()` is received before the waiters are setup, leading to 30s timeouts. This used to happen even when Promise.all() was used.
1 parent e911d28 commit 9b08fdb

File tree

2 files changed

+82
-85
lines changed

2 files changed

+82
-85
lines changed

common/frame.go

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1822,8 +1822,89 @@ func (f *Frame) WaitForLoadState(state string, opts goja.Value) {
18221822

18231823
// WaitForNavigation waits for the given navigation lifecycle event to happen.
18241824
func (f *Frame) WaitForNavigation(opts goja.Value) *goja.Promise {
1825+
f.log.Debugf("Frame:WaitForNavigation",
1826+
"fid:%s furl:%s", f.ID(), f.URL())
1827+
defer f.log.Debugf("Frame:WaitForNavigation:return",
1828+
"fid:%s furl:%s", f.ID(), f.URL())
1829+
1830+
parsedOpts := NewFrameWaitForNavigationOptions(
1831+
time.Duration(f.manager.timeoutSettings.timeout()) * time.Second)
1832+
if err := parsedOpts.Parse(f.ctx, opts); err != nil {
1833+
k6ext.Panic(f.ctx, "parsing wait for navigation options: %w", err)
1834+
}
1835+
1836+
timeoutCtx, timeoutCancel := context.WithTimeout(f.ctx, parsedOpts.Timeout)
1837+
1838+
navEvtCh, navEvtCancel := createWaitForEventHandler(timeoutCtx, f, []string{EventFrameNavigation},
1839+
func(data interface{}) bool {
1840+
return true // Both successful and failed navigations are considered
1841+
})
1842+
1843+
lifecycleEvtCh, lifecycleEvtCancel := createWaitForEventPredicateHandler(
1844+
timeoutCtx, f, []string{EventFrameAddLifecycle},
1845+
func(data interface{}) bool {
1846+
if le, ok := data.(LifecycleEvent); ok {
1847+
return le == parsedOpts.WaitUntil
1848+
}
1849+
return false
1850+
})
1851+
1852+
handleTimeoutError := func(err error) {
1853+
if errors.Is(err, context.DeadlineExceeded) {
1854+
err = &k6ext.UserFriendlyError{
1855+
Err: err,
1856+
Timeout: parsedOpts.Timeout,
1857+
}
1858+
k6ext.Panic(f.ctx, "waiting for navigation: %w", err)
1859+
}
1860+
f.log.Debugf("Frame:WaitForNavigation",
1861+
"fid:%v furl:%s timeoutCtx done: %v",
1862+
f.ID(), f.URL(), err)
1863+
}
1864+
18251865
return k6ext.Promise(f.ctx, func() (result interface{}, reason error) {
1826-
return f.manager.waitForFrameNavigation(f, opts)
1866+
defer func() {
1867+
timeoutCancel()
1868+
navEvtCancel()
1869+
lifecycleEvtCancel()
1870+
}()
1871+
1872+
var (
1873+
resp *Response
1874+
sameDocNav bool
1875+
)
1876+
select {
1877+
case evt := <-navEvtCh:
1878+
if e, ok := evt.(*NavigationEvent); ok {
1879+
if e.newDocument == nil {
1880+
sameDocNav = true
1881+
break
1882+
}
1883+
// request could be nil if navigating to e.g. about:blank
1884+
req := e.newDocument.request
1885+
if req != nil {
1886+
req.responseMu.RLock()
1887+
resp = req.response
1888+
req.responseMu.RUnlock()
1889+
}
1890+
}
1891+
case <-timeoutCtx.Done():
1892+
handleTimeoutError(timeoutCtx.Err())
1893+
return nil, nil
1894+
}
1895+
1896+
// A lifecycle event won't be received when navigating within the same
1897+
// document, so don't wait for it. The event might've also already been
1898+
// fired once we're here, so also skip waiting in that case.
1899+
if !sameDocNav && !f.hasSubtreeLifecycleEventFired(parsedOpts.WaitUntil) {
1900+
select {
1901+
case <-lifecycleEvtCh:
1902+
case <-timeoutCtx.Done():
1903+
handleTimeoutError(timeoutCtx.Err())
1904+
}
1905+
}
1906+
1907+
return resp, nil
18271908
})
18281909
}
18291910

common/frame_manager.go

Lines changed: 0 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -685,90 +685,6 @@ func (m *FrameManager) Page() api.Page {
685685
return nil
686686
}
687687

688-
// waitForFrameNavigation waits for the given navigation lifecycle event to happen.
689-
func (m *FrameManager) waitForFrameNavigation(frame *Frame, opts goja.Value) (api.Response, error) {
690-
m.logger.Debugf("FrameManager:WaitForFrameNavigation",
691-
"fmid:%d fid:%s furl:%s",
692-
m.ID(), frame.ID(), frame.URL())
693-
defer m.logger.Debugf("FrameManager:WaitForFrameNavigation:return",
694-
"fmid:%d fid:%s furl:%s",
695-
m.ID(), frame.ID(), frame.URL())
696-
697-
parsedOpts := NewFrameWaitForNavigationOptions(time.Duration(m.timeoutSettings.timeout()) * time.Second)
698-
if err := parsedOpts.Parse(m.ctx, opts); err != nil {
699-
return nil, fmt.Errorf("parsing wait for frame navigation options: %w", err)
700-
}
701-
702-
timeoutCtx, timeoutCancelFn := context.WithTimeout(m.ctx, parsedOpts.Timeout)
703-
defer timeoutCancelFn()
704-
705-
navEvtCh, navEvtCancel := createWaitForEventHandler(timeoutCtx, frame, []string{EventFrameNavigation},
706-
func(data interface{}) bool {
707-
return true // Both successful and failed navigations are considered
708-
})
709-
defer navEvtCancel() // Remove event handler
710-
711-
lifecycleEvtCh, lifecycleEvtCancel := createWaitForEventPredicateHandler(
712-
timeoutCtx, frame, []string{EventFrameAddLifecycle},
713-
func(data interface{}) bool {
714-
if le, ok := data.(LifecycleEvent); ok {
715-
return le == parsedOpts.WaitUntil
716-
}
717-
return false
718-
})
719-
defer lifecycleEvtCancel()
720-
721-
handleTimeoutError := func(err error) {
722-
if errors.Is(err, context.DeadlineExceeded) {
723-
err = &k6ext.UserFriendlyError{
724-
Err: err,
725-
Timeout: parsedOpts.Timeout,
726-
}
727-
k6ext.Panic(m.ctx, "waiting for navigation: %w", err)
728-
}
729-
m.logger.Debugf("FrameManager:waitForFrameNavigation",
730-
"fmid:%d fid:%v furl:%s timeoutCtx done: %v",
731-
m.ID(), frame.ID(), frame.URL(), err)
732-
}
733-
734-
var (
735-
resp *Response
736-
sameDocNav bool
737-
)
738-
select {
739-
case evt := <-navEvtCh:
740-
if e, ok := evt.(*NavigationEvent); ok {
741-
if e.newDocument == nil {
742-
sameDocNav = true
743-
break
744-
}
745-
// request could be nil if navigating to e.g. about:blank
746-
req := e.newDocument.request
747-
if req != nil {
748-
req.responseMu.RLock()
749-
resp = req.response
750-
req.responseMu.RUnlock()
751-
}
752-
}
753-
case <-timeoutCtx.Done():
754-
handleTimeoutError(timeoutCtx.Err())
755-
return nil, nil
756-
}
757-
758-
// A lifecycle event won't be received when navigating within the same
759-
// document, so don't wait for it. The event might've also already been
760-
// fired once we're here, so also skip waiting in that case.
761-
if !sameDocNav && !frame.hasSubtreeLifecycleEventFired(parsedOpts.WaitUntil) {
762-
select {
763-
case <-lifecycleEvtCh:
764-
case <-timeoutCtx.Done():
765-
handleTimeoutError(timeoutCtx.Err())
766-
}
767-
}
768-
769-
return resp, nil
770-
}
771-
772688
// ID returns the unique ID of a FrameManager value.
773689
func (m *FrameManager) ID() int64 {
774690
return atomic.LoadInt64(&m.id)

0 commit comments

Comments
 (0)