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

Async waitForNavigation #467

Merged
merged 20 commits into from
Aug 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 27 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,27 +357,35 @@ export default function () {

const currentBet = page.locator("//p[starts-with(text(),'Your bet: ')]");

// the tails locator clicks on the tails button by using the
// locator's selector.
tails.click();
// In the following Promise.all the tails locator clicks
// on the tails button by using the locator's selector.
// Since clicking on each button causes page navigation,
// waitForNavigation is needed. It's because the page
// waitForNavigation is needed -- this is because the page
// won't be ready until the navigation completes.
page.waitForNavigation();
console.log(currentBet.innerText());

// the heads locator clicks on the heads button by using the
// locator's selector.
heads.click();
page.waitForNavigation();
console.log(currentBet.innerText());

tails.click();
page.waitForNavigation();
console.log(currentBet.innerText());

page.close();
browser.close();
// Setting up the waitForNavigation first before the click
// is important to avoid race conditions.
Promise.all([
page.waitForNavigation(),
tails.click(),
]).then(() => {
console.log(currentBet.innerText());
// the heads locator clicks on the heads button
// by using the locator's selector.
return Promise.all([
page.waitForNavigation(),
heads.click(),
]);
}).then(() => {
console.log(currentBet.innerText());
return Promise.all([
page.waitForNavigation(),
tails.click(),
]);
}).finally(() => {
console.log(currentBet.innerText());
page.close();
browser.close();
})
}
```

Expand Down
2 changes: 1 addition & 1 deletion api/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type Frame interface {
URL() string
WaitForFunction(pageFunc, opts goja.Value, args ...goja.Value) *goja.Promise
WaitForLoadState(state string, opts goja.Value)
WaitForNavigation(opts goja.Value) Response
WaitForNavigation(opts goja.Value) *goja.Promise
WaitForSelector(selector string, opts goja.Value) ElementHandle
WaitForTimeout(timeout int64)
}
2 changes: 1 addition & 1 deletion api/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ type Page interface {
WaitForEvent(event string, optsOrPredicate goja.Value) interface{}
WaitForFunction(fn, opts goja.Value, args ...goja.Value) *goja.Promise
WaitForLoadState(state string, opts goja.Value)
WaitForNavigation(opts goja.Value) Response
WaitForNavigation(opts goja.Value) *goja.Promise
WaitForRequest(urlOrPredicate, opts goja.Value) Request
WaitForResponse(urlOrPredicate, opts goja.Value) Response
WaitForSelector(selector string, opts goja.Value) ElementHandle
Expand Down
6 changes: 4 additions & 2 deletions common/frame.go
Original file line number Diff line number Diff line change
Expand Up @@ -1821,8 +1821,10 @@ func (f *Frame) WaitForLoadState(state string, opts goja.Value) {
}

// WaitForNavigation waits for the given navigation lifecycle event to happen.
func (f *Frame) WaitForNavigation(opts goja.Value) api.Response {
return f.manager.WaitForFrameNavigation(f, opts)
func (f *Frame) WaitForNavigation(opts goja.Value) *goja.Promise {
return k6ext.Promise(f.ctx, func() (result interface{}, reason error) {
return f.manager.waitForFrameNavigation(f, opts)
})
}

// WaitForSelector waits for the given selector to match the waiting criteria.
Expand Down
16 changes: 8 additions & 8 deletions common/frame_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -685,8 +685,8 @@ func (m *FrameManager) Page() api.Page {
return nil
}

// WaitForFrameNavigation waits for the given navigation lifecycle event to happen.
func (m *FrameManager) WaitForFrameNavigation(frame *Frame, opts goja.Value) api.Response {
// waitForFrameNavigation waits for the given navigation lifecycle event to happen.
func (m *FrameManager) waitForFrameNavigation(frame *Frame, opts goja.Value) (api.Response, error) {
m.logger.Debugf("FrameManager:WaitForFrameNavigation",
"fmid:%d fid:%s furl:%s",
m.ID(), frame.ID(), frame.URL())
Expand All @@ -696,7 +696,7 @@ func (m *FrameManager) WaitForFrameNavigation(frame *Frame, opts goja.Value) api

parsedOpts := NewFrameWaitForNavigationOptions(time.Duration(m.timeoutSettings.timeout()) * time.Second)
if err := parsedOpts.Parse(m.ctx, opts); err != nil {
k6ext.Panic(m.ctx, "parsing wait for frame navigation options: %v", err)
return nil, fmt.Errorf("parsing wait for frame navigation options: %w", err)
}

ch, evCancelFn := createWaitForEventHandler(m.ctx, frame, []string{EventFrameNavigation},
Expand All @@ -712,9 +712,9 @@ func (m *FrameManager) WaitForFrameNavigation(frame *Frame, opts goja.Value) api
m.logger.Warnf("FrameManager:WaitForFrameNavigation:<-ctx.Done",
"fmid:%d furl:%s err:%v",
m.ID(), frame.URL(), m.ctx.Err())
return nil
return nil, nil
case <-time.After(parsedOpts.Timeout):
k6ext.Panic(m.ctx, "waiting for frame navigation timed out after %s", parsedOpts.Timeout)
return nil, fmt.Errorf("waiting for frame navigation timed out after %s", parsedOpts.Timeout)
case data := <-ch:
event = data.(*NavigationEvent)
}
Expand All @@ -723,7 +723,7 @@ func (m *FrameManager) WaitForFrameNavigation(frame *Frame, opts goja.Value) api
// In case of navigation within the same document (e.g. via an anchor
// link or the History API), there is no new document and a
// LifecycleEvent will not be fired, so we don't need to wait for it.
return nil
return nil, nil
}

if frame.hasSubtreeLifecycleEventFired(parsedOpts.WaitUntil) {
Expand All @@ -735,15 +735,15 @@ func (m *FrameManager) WaitForFrameNavigation(frame *Frame, opts goja.Value) api
return data.(LifecycleEvent) == parsedOpts.WaitUntil
}, parsedOpts.Timeout)
if err != nil {
k6ext.Panic(m.ctx, "waiting for frame navigation until %q: %v", parsedOpts.WaitUntil, err)
return nil, fmt.Errorf("waiting for frame navigation until %q: %w", parsedOpts.WaitUntil, err)
}
}

req := event.newDocument.request
req.responseMu.RLock()
defer req.responseMu.RUnlock()

return req.response
return req.response, nil
}

// ID returns the unique ID of a FrameManager value.
Expand Down
2 changes: 1 addition & 1 deletion common/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,7 @@ func (p *Page) WaitForLoadState(state string, opts goja.Value) {
}

// WaitForNavigation waits for the given navigation lifecycle event to happen.
func (p *Page) WaitForNavigation(opts goja.Value) api.Response {
func (p *Page) WaitForNavigation(opts goja.Value) *goja.Promise {
p.logger.Debugf("Page:WaitForNavigation", "sid:%v", p.sessionID())

return p.frameManager.MainFrame().WaitForNavigation(opts)
Expand Down
12 changes: 7 additions & 5 deletions examples/fillform.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ export default function() {
// Enter login credentials and login
page.$('input[name="login"]').type('admin');
page.$('input[name="password"]').type('123');
return page.$('input[type="submit"]').click();
// We expect the form submission to trigger a navigation, so to prevent a
// race condition, setup a waiter concurrently while waiting for the click
// to resolve.
return Promise.all([
page.waitForNavigation(),
page.$('input[type="submit"]').click(),
]);
}).then(() => {
// We expect the above form submission to trigger a navigation, so wait for it
// and the page to be loaded.
page.waitForNavigation();

check(page, {
'header': page.$('h2').textContent() == 'Welcome, admin!',
});
Expand Down
46 changes: 27 additions & 19 deletions examples/locator.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,33 @@ export default function () {

const currentBet = page.locator("//p[starts-with(text(),'Your bet: ')]");

// the tails locator clicks on the tails button by using the
// locator's selector.
tails.click();
// In the following Promise.all the tails locator clicks
// on the tails button by using the locator's selector.
// Since clicking on each button causes page navigation,
// waitForNavigation is needed. It's because the page
// waitForNavigation is needed -- this is because the page
// won't be ready until the navigation completes.
page.waitForNavigation();
console.log(currentBet.innerText());

// the heads locator clicks on the heads button by using the
// locator's selector.
heads.click();
page.waitForNavigation();
console.log(currentBet.innerText());

tails.click();
page.waitForNavigation();
console.log(currentBet.innerText());

page.close();
browser.close();
// Setting up the waitForNavigation first before the click
// is important to avoid race conditions.
Promise.all([
page.waitForNavigation(),
tails.click(),
]).then(() => {
console.log(currentBet.innerText());
// the heads locator clicks on the heads button
// by using the locator's selector.
return Promise.all([
page.waitForNavigation(),
heads.click(),
]);
}).then(() => {
console.log(currentBet.innerText());
return Promise.all([
page.waitForNavigation(),
tails.click(),
]);
}).finally(() => {
console.log(currentBet.innerText());
page.close();
browser.close();
})
}
40 changes: 22 additions & 18 deletions examples/locator_pom.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ export class Bet {
}

heads() {
this.headsButton.click();
this.page.waitForNavigation();
return Promise.all([
this.page.waitForNavigation(),
this.headsButton.click(),
]);
}

tails() {
this.tailsButton.click();
this.page.waitForNavigation();
return Promise.all([
this.page.waitForNavigation(),
this.tailsButton.click(),
]);
}

current() {
Expand All @@ -46,18 +50,18 @@ export default function () {
const bet = new Bet(page);
bet.goto();

bet.tails();
console.log("Current bet:", bet.current());

bet.heads();
console.log("Current bet:", bet.current());

bet.tails();
console.log("Current bet:", bet.current());

bet.heads();
console.log("Current bet:", bet.current());

page.close();
browser.close();
bet.tails().then(() => {
console.log("Current bet:", bet.current());
return bet.heads();
}).then(() => {
console.log("Current bet:", bet.current());
return bet.tails();
}).then(() => {
console.log("Current bet:", bet.current());
return bet.heads();
}).finally(() => {
console.log("Current bet:", bet.current());
page.close();
browser.close();
})
}
1 change: 1 addition & 0 deletions k6ext/k6test/vu.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func (v *VU) ToGojaValue(i interface{}) goja.Value { return v.Runtime().ToValue(

// RunLoop is a convenience method for running fn in the event loop.
func (v *VU) RunLoop(fn func() error) error {
v.Loop.WaitOnRegistered()
imiric marked this conversation as resolved.
Show resolved Hide resolved
return v.Loop.Start(fn)
}

Expand Down
24 changes: 13 additions & 11 deletions k6ext/promise.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,20 @@ func promise(ctx context.Context, fn PromisifiedFunc, d eventLoopDirective) *goj
cb = vu.RegisterCallback()
p, resolve, reject = vu.Runtime().NewPromise()
)
go cb(func() error {
go func() {
v, err := fn()
if err != nil {
reject(err)
} else {
resolve(v)
}
if d == continueEventLoop {
err = nil
}
return err
})
cb(func() error {
if err != nil {
reject(err)
} else {
resolve(v)
}
if d == continueEventLoop {
err = nil
}
return err
})
}()

return p
}
Loading