From a76482072ffc15bd980933ccc48006855ef648bc Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Tue, 11 Oct 2022 11:54:48 +0300 Subject: [PATCH] Implement async goto Updates #428 --- api/frame.go | 2 +- api/page.go | 2 +- common/frame.go | 17 +++-- common/frame_manager.go | 28 +++----- common/page.go | 2 +- examples/browser_args.js | 14 ++-- examples/browser_on.js | 2 +- examples/colorscheme.js | 22 +++--- examples/device_emulation.js | 42 +++++------ examples/evaluate.js | 35 ++++----- examples/fillform.js | 12 ++-- examples/getattribute.js | 15 ++-- examples/grant_permission.js | 13 ++-- examples/hosts.js | 12 ++-- examples/locator.js | 92 ++++++++++++------------ examples/locator_pom.js | 8 +-- examples/multiple-scenario.js | 16 ++--- examples/querying.js | 18 ++--- examples/screenshot.js | 13 ++-- tests/browser_context_options_test.go | 23 +++--- tests/element_handle_test.go | 37 ++++++---- tests/frame_manager_test.go | 67 +++++++++-------- tests/launch_options_slowmo_test.go | 5 +- tests/locator_test.go | 58 ++++++++++----- tests/network_manager_test.go | 93 ++++++++++++++++-------- tests/page_test.go | 100 ++++++++++++++++---------- tests/test_browser.go | 26 +++++-- 27 files changed, 451 insertions(+), 323 deletions(-) diff --git a/api/frame.go b/api/frame.go index 07542bf91..82b3a476c 100644 --- a/api/frame.go +++ b/api/frame.go @@ -18,7 +18,7 @@ type Frame interface { Focus(selector string, opts goja.Value) FrameElement() ElementHandle GetAttribute(selector string, name string, opts goja.Value) goja.Value - Goto(url string, opts goja.Value) Response + Goto(url string, opts goja.Value) *goja.Promise Hover(selector string, opts goja.Value) InnerHTML(selector string, opts goja.Value) string InnerText(selector string, opts goja.Value) string diff --git a/api/page.go b/api/page.go index 348be14f5..c3a881f99 100644 --- a/api/page.go +++ b/api/page.go @@ -49,7 +49,7 @@ type Page interface { GetAttribute(selector string, name string, opts goja.Value) goja.Value GoBack(opts goja.Value) Response GoForward(opts goja.Value) Response - Goto(url string, opts goja.Value) Response + Goto(url string, opts goja.Value) *goja.Promise Hover(selector string, opts goja.Value) InnerHTML(selector string, opts goja.Value) string InnerText(selector string, opts goja.Value) string diff --git a/common/frame.go b/common/frame.go index bed0b9562..9feceed86 100644 --- a/common/frame.go +++ b/common/frame.go @@ -1054,10 +1054,19 @@ func (f *Frame) getAttribute(selector, name string, opts *FrameBaseOptions) (goj } // Goto will navigate the frame to the specified URL and return a HTTP response object. -func (f *Frame) Goto(url string, opts goja.Value) api.Response { - resp := f.manager.NavigateFrame(f, url, opts) - applySlowMo(f.ctx) - return resp +func (f *Frame) Goto(url string, opts goja.Value) *goja.Promise { + netMgr := f.manager.page.mainFrameSession.getNetworkManager() + defaultReferer := netMgr.extraHTTPHeaders["referer"] + parsedOpts := NewFrameGotoOptions(defaultReferer, time.Duration(f.manager.timeoutSettings.navigationTimeout())*time.Second) + if err := parsedOpts.Parse(f.ctx, opts); err != nil { + k6ext.Panic(f.ctx, "parsing frame navigation options to %q: %w", url, err) + return nil + } + return k6ext.Promise(f.ctx, func() (interface{}, error) { + resp, err := f.manager.NavigateFrame(f, url, parsedOpts) + applySlowMo(f.ctx) + return resp, err + }) } // Hover moves the pointer over the first element that matches the selector. diff --git a/common/frame_manager.go b/common/frame_manager.go index 99ff12cd1..94ff25708 100644 --- a/common/frame_manager.go +++ b/common/frame_manager.go @@ -26,7 +26,6 @@ import ( "fmt" "sync" "sync/atomic" - "time" "github.com/grafana/xk6-browser/api" "github.com/grafana/xk6-browser/k6ext" @@ -36,7 +35,6 @@ import ( "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/network" - "github.com/dop251/goja" ) // FrameManager manages all frames in a page and their life-cycles, it's a purely internal component. @@ -569,7 +567,7 @@ func (m *FrameManager) setMainFrame(f *Frame) { } // NavigateFrame will navigate specified frame to specified URL. -func (m *FrameManager) NavigateFrame(frame *Frame, url string, opts goja.Value) api.Response { +func (m *FrameManager) NavigateFrame(frame *Frame, url string, parsedOpts *FrameGotoOptions) (api.Response, error) { var ( fmid = m.ID() fid = frame.ID() @@ -580,13 +578,6 @@ func (m *FrameManager) NavigateFrame(frame *Frame, url string, opts goja.Value) defer m.logger.Debugf("FrameManager:NavigateFrame:return", "fmid:%d fid:%v furl:%s url:%s", fmid, fid, furl, url) - netMgr := m.page.mainFrameSession.getNetworkManager() - defaultReferer := netMgr.extraHTTPHeaders["referer"] - parsedOpts := NewFrameGotoOptions(defaultReferer, time.Duration(m.timeoutSettings.navigationTimeout())*time.Second) - if err := parsedOpts.Parse(m.ctx, opts); err != nil { - k6ext.Panic(m.ctx, "parsing frame navigation options to %q: %v", url, err) - } - timeoutCtx, timeoutCancelFn := context.WithTimeout(m.ctx, parsedOpts.Timeout) defer timeoutCancelFn() @@ -620,35 +611,37 @@ func (m *FrameManager) NavigateFrame(frame *Frame, url string, opts goja.Value) // Attaching an iframe to an existing page doesn't seem to trigger a "Target.attachedToTarget" event // from the browser even when "Target.setAutoAttach" is true. If this is the case fallback to the + // main frame's session. fs = frame.page.mainFrameSession } newDocumentID, err := fs.navigateFrame(frame, url, parsedOpts.Referer) if err != nil { - k6ext.Panic(m.ctx, "navigating to %q: %v", url, err) + return nil, fmt.Errorf("navigating to %q: %w", url, err) } if newDocumentID == "" { // It's a navigation within the same document (e.g. via anchor links or // the History API), so don't wait for a response nor any lifecycle // events. - return nil + return nil, nil } // unblock the waiter goroutine newDocIDCh <- newDocumentID - handleTimeoutError := func(err error) { + wrapTimeoutError := func(err error) error { if errors.Is(err, context.DeadlineExceeded) { err = &k6ext.UserFriendlyError{ Err: err, Timeout: parsedOpts.Timeout, } - k6ext.Panic(m.ctx, "navigating to %q: %w", url, err) + return fmt.Errorf("navigating to %q: %w", url, err) } m.logger.Debugf("FrameManager:NavigateFrame", "fmid:%d fid:%v furl:%s url:%s timeoutCtx done: %v", fmid, fid, furl, url, err) + return err // TODO maybe wrap this as well? } var resp *Response @@ -664,17 +657,16 @@ func (m *FrameManager) NavigateFrame(frame *Frame, url string, opts goja.Value) } } case <-timeoutCtx.Done(): - handleTimeoutError(timeoutCtx.Err()) - return nil + return nil, wrapTimeoutError(timeoutCtx.Err()) } select { case <-lifecycleEvtCh: case <-timeoutCtx.Done(): - handleTimeoutError(timeoutCtx.Err()) + return nil, wrapTimeoutError(timeoutCtx.Err()) } - return resp + return resp, nil } // Page returns the page that this frame manager belongs to. diff --git a/common/page.go b/common/page.go index 862d55997..79730ac66 100644 --- a/common/page.go +++ b/common/page.go @@ -560,7 +560,7 @@ func (p *Page) GoForward(opts goja.Value) api.Response { } // Goto will navigate the page to the specified URL and return a HTTP response object. -func (p *Page) Goto(url string, opts goja.Value) api.Response { +func (p *Page) Goto(url string, opts goja.Value) *goja.Promise { p.logger.Debugf("Page:Goto", "sid:%v url:%q", p.sessionID(), url) return p.MainFrame().Goto(url, opts) diff --git a/examples/browser_args.js b/examples/browser_args.js index 66c9bffeb..afbe089bb 100644 --- a/examples/browser_args.js +++ b/examples/browser_args.js @@ -15,12 +15,12 @@ export default function() { const context = browser.newContext(); const page = context.newPage(); - const res = page.goto('http://test.k6.io/', { waitUntil: 'load' }); + page.goto('http://test.k6.io/', { waitUntil: 'load' }).then((res) => { + check(res, { + 'null response': r => r === null, + }); - check(res, { - 'null response': r => r === null, - }); - - page.close(); - browser.close(); + page.close(); + browser.close(); + }) } diff --git a/examples/browser_on.js b/examples/browser_on.js index feae342d4..b77522a46 100644 --- a/examples/browser_on.js +++ b/examples/browser_on.js @@ -25,7 +25,7 @@ export default function() { 'should be disconnected on event': !browser.isConnected(), }); return handlerCalled; - // The promise reject/failure handler + // The promise reject/failure handler }, (val) => { console.error(`promise rejected: ${val}`); }); diff --git a/examples/colorscheme.js b/examples/colorscheme.js index 875a36980..40a958cb4 100644 --- a/examples/colorscheme.js +++ b/examples/colorscheme.js @@ -23,17 +23,17 @@ export default function() { page.goto( 'https://googlechromelabs.github.io/dark-mode-toggle/demo/', { waitUntil: 'load' }, - ); + ).then(() => { + const colorScheme = page.evaluate(() => { + return { + isDarkColorScheme: window.matchMedia('(prefers-color-scheme: dark)').matches + }; + }); + check(colorScheme, { + 'isDarkColorScheme': cs => cs.isDarkColorScheme + }); - const colorScheme = page.evaluate(() => { - return { - isDarkColorScheme: window.matchMedia('(prefers-color-scheme: dark)').matches - }; + page.close(); + browser.close(); }); - check(colorScheme, { - 'isDarkColorScheme': cs => cs.isDarkColorScheme - }); - - page.close(); - browser.close(); } diff --git a/examples/device_emulation.js b/examples/device_emulation.js index 83c797a38..7335c9a29 100644 --- a/examples/device_emulation.js +++ b/examples/device_emulation.js @@ -20,26 +20,26 @@ export default function() { const context = browser.newContext(options); const page = context.newPage(); - page.goto('https://k6.io/', { waitUntil: 'networkidle' }); - - const dimensions = page.evaluate(() => { - return { - width: document.documentElement.clientWidth, - height: document.documentElement.clientHeight, - deviceScaleFactor: window.devicePixelRatio - }; - }); - - check(dimensions, { - 'width': d => d.width === device.viewport.width, - 'height': d => d.height === device.viewport.height, - 'scale': d => d.deviceScaleFactor === device.deviceScaleFactor, + page.goto('https://k6.io/', { waitUntil: 'networkidle' }).then(() => { + const dimensions = page.evaluate(() => { + return { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + deviceScaleFactor: window.devicePixelRatio + }; + }); + + check(dimensions, { + 'width': d => d.width === device.viewport.width, + 'height': d => d.height === device.viewport.height, + 'scale': d => d.deviceScaleFactor === device.deviceScaleFactor, + }); + + if (!__ENV.XK6_HEADLESS) { + sleep(10); + } + + page.close(); + browser.close(); }); - - if (!__ENV.XK6_HEADLESS) { - sleep(10); - } - - page.close(); - browser.close(); } diff --git a/examples/evaluate.js b/examples/evaluate.js index 0ad9034a5..9ff34fed3 100644 --- a/examples/evaluate.js +++ b/examples/evaluate.js @@ -15,23 +15,24 @@ export default function() { const context = browser.newContext(); const page = context.newPage(); - page.goto('https://test.k6.io/', { waitUntil: 'load' }); - const dimensions = page.evaluate(() => { - const obj = { - width: document.documentElement.clientWidth, - height: document.documentElement.clientHeight, - deviceScaleFactor: window.devicePixelRatio - }; - console.log(obj); // tests #120 - return obj; - }); + page.goto('https://test.k6.io/', { waitUntil: 'load' }).then(() => { + const dimensions = page.evaluate(() => { + const obj = { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + deviceScaleFactor: window.devicePixelRatio + }; + console.log(obj); // tests #120 + return obj; + }); - check(dimensions, { - 'width': d => d.width === 1265, - 'height': d => d.height === 720, - 'scale': d => d.deviceScaleFactor === 1, - }); + check(dimensions, { + 'width': d => d.width === 1265, + 'height': d => d.height === 720, + 'scale': d => d.deviceScaleFactor === 1, + }); - page.close(); - browser.close(); + page.close(); + browser.close(); + }); } diff --git a/examples/fillform.js b/examples/fillform.js index fb841c62b..108765cd1 100644 --- a/examples/fillform.js +++ b/examples/fillform.js @@ -15,11 +15,13 @@ export default function() { const page = context.newPage(); // Goto front page, find login link and click it - page.goto('https://test.k6.io/', { waitUntil: 'networkidle' }); - Promise.all([ - page.waitForNavigation(), - page.locator('a[href="/my_messages.php"]').click(), - ]).then(() => { + page.goto('https://test.k6.io/', { waitUntil: 'networkidle' }).then(() => { + return Promise.all([ + page.waitForNavigation(), + page.locator('a[href="/my_messages.php"]').click(), + ]) + + }).then(() => { // Enter login credentials and login page.locator('input[name="login"]').type('admin'); page.locator('input[name="password"]').type('123'); diff --git a/examples/getattribute.js b/examples/getattribute.js index 162724b24..b963bc9e6 100644 --- a/examples/getattribute.js +++ b/examples/getattribute.js @@ -16,12 +16,13 @@ export default function() { page.goto('https://googlechromelabs.github.io/dark-mode-toggle/demo/', { waitUntil: 'load', - }); - let el = page.$('#dark-mode-toggle-3') - check(el, { - "GetAttribute('mode')": e => e.getAttribute('mode') == 'light', - }); + }).then(() => { + let el = page.$('#dark-mode-toggle-3') + check(el, { + "GetAttribute('mode')": e => e.getAttribute('mode') == 'light', + }); - page.close(); - browser.close(); + page.close(); + browser.close(); + }); } diff --git a/examples/grant_permission.js b/examples/grant_permission.js index d779c52be..30ed4ebed 100644 --- a/examples/grant_permission.js +++ b/examples/grant_permission.js @@ -16,11 +16,12 @@ export default function() { const context = browser.newContext({ permissions: ["camera", "microphone"], }); - + const page = context.newPage(); - page.goto('http://whatsmyuseragent.org/'); - page.screenshot({ path: `example-chromium.png` }); + page.goto('http://whatsmyuseragent.org/').then(() => { + page.screenshot({ path: `example-chromium.png` }); - page.close(); - browser.close(); -} \ No newline at end of file + page.close(); + browser.close(); + }) +} diff --git a/examples/hosts.js b/examples/hosts.js index 2d312b559..8045df5dd 100644 --- a/examples/hosts.js +++ b/examples/hosts.js @@ -15,12 +15,12 @@ export default function() { const context = browser.newContext(); const page = context.newPage(); - const res = page.goto('http://test.k6.io/', { waitUntil: 'load' }); + page.goto('http://test.k6.io/', { waitUntil: 'load' }).then((res) => { + check(res, { + 'null response': r => r === null, + }); - check(res, { - 'null response': r => r === null, + page.close(); + browser.close(); }); - - page.close(); - browser.close(); } diff --git a/examples/locator.js b/examples/locator.js index 966e12bc2..c570ae41a 100644 --- a/examples/locator.js +++ b/examples/locator.js @@ -14,54 +14,54 @@ export default function() { const page = context.newPage(); page.goto("https://test.k6.io/flip_coin.php", { waitUntil: "networkidle", - }); - - /* - In this example, we will use two locators, matching a - different betting button on the page. If you were to query - the buttons once and save them as below, you would see an - error after the initial navigation. Try it! - - const heads = page.$("input[value='Bet on heads!']"); - const tails = page.$("input[value='Bet on tails!']"); - - The Locator API allows you to get a fresh element handle each - time you use one of the locator methods. And, you can carry a - locator across frame navigations. Let's create two locators; - each locates a button on the page. - */ - const heads = page.locator("input[value='Bet on heads!']"); - const tails = page.locator("input[value='Bet on tails!']"); + }).then(() => { + /* + In this example, we will use two locators, matching a + different betting button on the page. If you were to query + the buttons once and save them as below, you would see an + error after the initial navigation. Try it! + + const heads = page.$("input[value='Bet on heads!']"); + const tails = page.$("input[value='Bet on tails!']"); + + The Locator API allows you to get a fresh element handle each + time you use one of the locator methods. And, you can carry a + locator across frame navigations. Let's create two locators; + each locates a button on the page. + */ + const heads = page.locator("input[value='Bet on heads!']"); + const tails = page.locator("input[value='Bet on tails!']"); - const currentBet = page.locator("//p[starts-with(text(),'Your bet: ')]"); + const currentBet = page.locator("//p[starts-with(text(),'Your bet: ')]"); - // 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 -- this is because the page - // won't be ready until the navigation completes. - // 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([ + // 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 -- this is because the page + // won't be ready until the navigation completes. + // Setting up the waitForNavigation first before the click + // is important to avoid race conditions. + Promise.all([ page.waitForNavigation(), tails.click(), - ]); - }).finally(() => { - console.log(currentBet.innerText()); - page.close(); - browser.close(); - }) + ]).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(); + }) + }); } diff --git a/examples/locator_pom.js b/examples/locator_pom.js index acea4e1dc..87bd6054f 100644 --- a/examples/locator_pom.js +++ b/examples/locator_pom.js @@ -24,7 +24,7 @@ export class Bet { } goto() { - this.page.goto("https://test.k6.io/flip_coin.php", { waitUntil: "networkidle" }); + return this.page.goto("https://test.k6.io/flip_coin.php", { waitUntil: "networkidle" }); } heads() { @@ -54,9 +54,9 @@ export default function() { const page = context.newPage(); const bet = new Bet(page); - bet.goto(); - - bet.tails().then(() => { + bet.goto().then(() => { + return bet.tails(); + }).then(() => { console.log("Current bet:", bet.current()); return bet.heads(); }).then(() => { diff --git a/examples/multiple-scenario.js b/examples/multiple-scenario.js index 9a3297d65..42a33c735 100644 --- a/examples/multiple-scenario.js +++ b/examples/multiple-scenario.js @@ -29,10 +29,10 @@ export function messages() { }); const page = browser.newPage(); - page.goto('https://test.k6.io/my_messages.php', { waitUntil: 'networkidle' }) - - page.close(); - browser.close(); + page.goto('https://test.k6.io/my_messages.php', { waitUntil: 'networkidle' }).then(() => { + page.close(); + browser.close(); + }); } export function news() { @@ -41,8 +41,8 @@ export function news() { }); const page = browser.newPage(); - page.goto('https://test.k6.io/news.php', { waitUntil: 'networkidle' }) - - page.close(); - browser.close(); + page.goto('https://test.k6.io/news.php', { waitUntil: 'networkidle' }).then(() => { + page.close(); + browser.close(); + }); } diff --git a/examples/querying.js b/examples/querying.js index 52b0e064a..e4c2f077b 100644 --- a/examples/querying.js +++ b/examples/querying.js @@ -14,15 +14,15 @@ export default function() { const context = browser.newContext(); const page = context.newPage(); - page.goto('https://test.k6.io/'); + page.goto('https://test.k6.io/').then(() => { + check(page, { + 'Title with CSS selector': + p => p.$('header h1.title').textContent() == 'test.k6.io', + 'Title with XPath selector': + p => p.$(`//header//h1[@class="title"]`).textContent() == 'test.k6.io', + }); - check(page, { - 'Title with CSS selector': - p => p.$('header h1.title').textContent() == 'test.k6.io', - 'Title with XPath selector': - p => p.$(`//header//h1[@class="title"]`).textContent() == 'test.k6.io', + page.close(); + browser.close(); }); - - page.close(); - browser.close(); } diff --git a/examples/screenshot.js b/examples/screenshot.js index a6eaa0d25..79d40f3a7 100644 --- a/examples/screenshot.js +++ b/examples/screenshot.js @@ -12,10 +12,11 @@ export default function() { }); const context = browser.newContext(); const page = context.newPage(); - page.goto('https://test.k6.io/'); - page.screenshot({ path: 'screenshot.png' }); - // TODO: Assert this somehow. Upload as CI artifact or just an external `ls`? - // Maybe even do a fuzzy image comparison against a preset known good screenshot? - page.close(); - browser.close(); + page.goto('https://test.k6.io/').then(() => { + page.screenshot({ path: 'screenshot.png' }); + // TODO: Assert this somehow. Upload as CI artifact or just an external `ls`? + // Maybe even do a fuzzy image comparison against a preset known good screenshot? + page.close(); + browser.close(); + }); } diff --git a/tests/browser_context_options_test.go b/tests/browser_context_options_test.go index d39e9c0d6..4572e8aed 100644 --- a/tests/browser_context_options_test.go +++ b/tests/browser_context_options_test.go @@ -24,7 +24,9 @@ import ( _ "embed" "encoding/json" "testing" + "time" + "github.com/grafana/xk6-browser/api" "github.com/grafana/xk6-browser/common" "github.com/stretchr/testify/assert" @@ -92,15 +94,20 @@ func TestBrowserContextOptionsExtraHTTPHeaders(t *testing.T) { }, })) t.Cleanup(bctx.Close) - p := bctx.NewPage() - resp := p.Goto(tb.URL("/get"), nil) - require.NotNil(t, resp) - var body struct{ Headers map[string][]string } - err := json.Unmarshal(resp.Body().Bytes(), &body) + err := tb.awaitWithTimeout(time.Second*5, func() error { + tb.promiseThen(p.Goto(tb.URL("/get"), nil), + func(resp api.Response) { + require.NotNil(t, resp) + var body struct{ Headers map[string][]string } + require.NoError(t, json.Unmarshal(resp.Body().Bytes(), &body)) + h := body.Headers["Some-Header"] + require.NotEmpty(t, h) + assert.Equal(t, "Some-Value", h[0]) + }) + return nil + }) + require.NoError(t, err) - h := body.Headers["Some-Header"] - require.NotEmpty(t, h) - assert.Equal(t, "Some-Value", h[0]) } diff --git a/tests/element_handle_test.go b/tests/element_handle_test.go index b7fcdf6c7..d43ad4a7d 100644 --- a/tests/element_handle_test.go +++ b/tests/element_handle_test.go @@ -188,27 +188,38 @@ func TestElementHandleClickConcealedLink(t *testing.T) { cr := p.Evaluate(tb.toGojaValue(cmd)) return tb.asGojaValue(cr).String() } - require.NotNil(t, p.Goto(tb.staticURL("/concealed_link.html"), nil)) - require.Equal(t, wantBefore, clickResult()) - - err := tb.await(func() error { - _ = p.Click("#concealed", nil) + require.NoError(t, tb.await(func() error { + tb.promiseThen(p.Goto(tb.staticURL("/concealed_link.html"), nil), + func() *goja.Promise { + require.Equal(t, wantBefore, clickResult()) + + return p.Click("#concealed", nil) + }).then( + func() { + require.Equal(t, wantAfter, clickResult()) + }) return nil - }) - require.NoError(t, err, "element should be clickable") - require.Equal(t, wantAfter, clickResult()) + })) } func TestElementHandleNonClickable(t *testing.T) { tb := newTestBrowser(t, withFileServer()) p := tb.NewContext(nil).NewPage() - require.NotNil(t, p.Goto(tb.staticURL("/non_clickable.html"), nil)) - err := tb.await(func() error { - _ = p.Click("#non-clickable", nil) + var done bool + require.NoError(t, tb.await(func() error { + tb.promiseThen(p.Goto(tb.staticURL("/non_clickable.html"), nil), + func() *goja.Promise { + return p.Click("#non-clickable", nil) + }).then(func() { + t.Fatal("element should not be clickable") + }, func() { + done = true + }) + return nil - }) - require.Error(t, err, "element should not be clickable") + })) + require.True(t, done, "element should not be clickable") } func TestElementHandleGetAttribute(t *testing.T) { diff --git a/tests/frame_manager_test.go b/tests/frame_manager_test.go index aa3108531..fbce54809 100644 --- a/tests/frame_manager_test.go +++ b/tests/frame_manager_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/grafana/xk6-browser/api" "github.com/grafana/xk6-browser/common" "github.com/dop251/goja" @@ -34,29 +35,32 @@ func TestWaitForFrameNavigationWithinDocument(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() + var done bool tb := newTestBrowser(t, withFileServer()) err := tb.awaitWithTimeout(timeout, func() error { p := tb.NewPage(nil) - resp := p.Goto(tb.staticURL("/nav_in_doc.html"), tb.toGojaValue(&common.FrameGotoOptions{ + gotoPromise := p.Goto(tb.staticURL("/nav_in_doc.html"), tb.toGojaValue(&common.FrameGotoOptions{ WaitUntil: common.LifecycleEventNetworkIdle, Timeout: time.Duration(timeout.Milliseconds()), // interpreted as ms })) - require.NotNil(t, resp) - wfnPromise := p.WaitForNavigation(tb.toGojaValue(&common.FrameWaitForNavigationOptions{ - Timeout: time.Duration(timeout.Milliseconds()), // interpreted as ms - })) - cPromise := p.Click(tc.selector, nil) - tb.promiseThen(tb.promiseAll(wfnPromise, cPromise), func(_ goja.Value) { - // this is a bit pointless :shrug: - assert.Equal(t, goja.PromiseStateFulfilled, wfnPromise.State()) - assert.Equal(t, goja.PromiseStateFulfilled, cPromise.State()) - }, func(val goja.Value) { t.Fatal(val) }) + + tb.promiseThen(gotoPromise, func(resp api.Response) *goja.Promise { + require.NotNil(t, resp) + wfnPromise := p.WaitForNavigation(tb.toGojaValue(&common.FrameWaitForNavigationOptions{ + Timeout: time.Duration(timeout.Milliseconds()), // interpreted as ms + })) + cPromise := p.Click(tc.selector, nil) + return tb.promiseAll(wfnPromise, cPromise) + }).then(func() { + done = true + }) return nil }) require.NoError(t, err) + require.True(t, done) }) } } @@ -90,28 +94,23 @@ func TestWaitForFrameNavigation(t *testing.T) { `) }) - require.NotNil(t, p.Goto(tb.URL("/first"), tb.toGojaValue(&common.FrameGotoOptions{ - WaitUntil: common.LifecycleEventNetworkIdle, - Timeout: common.DefaultTimeout, - }))) - - var timeout time.Duration = 5000 // interpreted as ms - - var wfnPromise, cPromise *goja.Promise - err := tb.await(func() error { - wfnPromise = p.WaitForNavigation(tb.toGojaValue(&common.FrameWaitForNavigationOptions{ - Timeout: timeout, // interpreted as ms - })) - cPromise = p.Click(`a`, nil) - - assert.Equal(t, goja.PromiseStatePending, wfnPromise.State()) - assert.Equal(t, goja.PromiseStatePending, cPromise.State()) - + var done bool + require.NoError(t, tb.await(func() error { + tb.promiseThen(p.Goto(tb.URL("/first"), tb.toGojaValue(&common.FrameGotoOptions{ + WaitUntil: common.LifecycleEventNetworkIdle, + Timeout: common.DefaultTimeout, + })), func() *goja.Promise { + var timeout time.Duration = 5000 // interpreted as ms + wfnPromise := p.WaitForNavigation(tb.toGojaValue(&common.FrameWaitForNavigationOptions{ + Timeout: timeout, // interpreted as ms + })) + cPromise := p.Click(`a`, nil) + return tb.promiseAll(wfnPromise, cPromise) + }).then(func() { + assert.Equal(t, "Second page", p.Title()) + done = true + }) return nil - }) - require.NoError(t, err) - - assert.Equal(t, goja.PromiseStateFulfilled, wfnPromise.State()) - assert.Equal(t, goja.PromiseStateFulfilled, cPromise.State()) - assert.Equal(t, "Second page", p.Title()) + })) + require.True(t, done) } diff --git a/tests/launch_options_slowmo_test.go b/tests/launch_options_slowmo_test.go index 756d77b82..45ec9244d 100644 --- a/tests/launch_options_slowmo_test.go +++ b/tests/launch_options_slowmo_test.go @@ -115,7 +115,10 @@ func TestLaunchOptionsSlowMo(t *testing.T) { t.Parallel() tb := newTestBrowser(t, withFileServer()) testPageSlowMoImpl(t, tb, func(_ *testBrowser, p api.Page) { - p.Goto("about:blank", nil) + tb.await(func() error { + p.Goto("about:blank", nil) + return nil + }) }) }) t.Run("hover", func(t *testing.T) { diff --git a/tests/locator_test.go b/tests/locator_test.go index 0f966d397..265548782 100644 --- a/tests/locator_test.go +++ b/tests/locator_test.go @@ -185,8 +185,13 @@ func TestLocator(t *testing.T) { tb := newTestBrowser(t, withFileServer()) p := tb.NewPage(nil) - require.NotNil(t, p.Goto(tb.staticURL("locators.html"), nil)) - tt.do(tb, p) + require.NoError(t, tb.await(func() error { + tb.promiseThen(p.Goto(tb.staticURL("locators.html"), nil), func() { + tt.do(tb, p) + }) + + return nil + })) }) } @@ -266,12 +271,21 @@ func TestLocator(t *testing.T) { }) } - tb := newTestBrowser(t, withFileServer()) - p := tb.NewPage(nil) - require.NotNil(t, p.Goto(tb.staticURL("locators.html"), nil)) for _, tt := range sanityTests { + tt := tt t.Run("strict/"+tt.name, func(t *testing.T) { - assert.Panics(t, func() { tt.do(p.Locator("a", nil), tb) }) + t.Parallel() + tb := newTestBrowser(t, withFileServer()) + p := tb.NewPage(nil) + require.NoError(t, tb.await(func() error { + tb.promiseThen( + p.Goto(tb.staticURL("locators.html"), nil), + func() { + assert.Panics(t, func() { tt.do(p.Locator("a", nil), tb) }) + }) + + return nil + })) }) } } @@ -317,13 +331,18 @@ func TestLocatorElementState(t *testing.T) { tb := newTestBrowser(t, withFileServer()) p := tb.NewPage(nil) - require.NotNil(t, p.Goto(tb.staticURL("locators.html"), nil)) - - l := p.Locator("#inputText", nil) - require.True(t, tt.query(l)) + require.NoError(t, tb.await(func() error { + tb.promiseThen( + p.Goto(tb.staticURL("locators.html"), nil), + func() { + l := p.Locator("#inputText", nil) + require.True(t, tt.query(l)) - p.Evaluate(tb.toGojaValue(tt.eval)) - require.False(t, tt.query(l)) + p.Evaluate(tb.toGojaValue(tt.eval)) + require.False(t, tt.query(l)) + }) + return nil + })) }) } @@ -365,12 +384,19 @@ func TestLocatorElementState(t *testing.T) { }) } - tb := newTestBrowser(t, withFileServer()) - p := tb.NewPage(nil) - require.NotNil(t, p.Goto(tb.staticURL("locators.html"), nil)) for _, tt := range sanityTests { + tt := tt t.Run("strict/"+tt.name, func(t *testing.T) { - assert.Panics(t, func() { tt.do(p.Locator("a", nil), tb) }) + t.Parallel() + tb := newTestBrowser(t, withFileServer()) + p := tb.NewPage(nil) + require.NoError(t, tb.await(func() error { + tb.promiseThen(p.Goto(tb.staticURL("locators.html"), nil), + func() { + assert.Panics(t, func() { tt.do(p.Locator("a", nil), tb) }) + }) + return nil + })) }) } } diff --git a/tests/network_manager_test.go b/tests/network_manager_test.go index e4c8ffebe..4715474db 100644 --- a/tests/network_manager_test.go +++ b/tests/network_manager_test.go @@ -25,6 +25,7 @@ import ( "net/http" "testing" + "github.com/dop251/goja" "github.com/grafana/xk6-browser/api" "github.com/grafana/xk6-browser/common" @@ -41,11 +42,18 @@ func TestURLSkipRequest(t *testing.T) { tb := newTestBrowser(t, withLogCache()) p := tb.NewPage(nil) - p.Goto("data:text/html,hello", nil) - assert.True(t, tb.logCache.contains("skipping request handling of data URL")) - - p.Goto("blob:something", nil) - assert.True(t, tb.logCache.contains("skipping request handling of blob URL")) + tb.await(func() error { + tb.promiseThen(p.Goto("data:text/html,hello", nil), + func() *goja.Promise { + assert.True(t, tb.logCache.contains("skipping request handling of data URL")) + return p.Goto("blob:something", nil) + }, + ).then(func() { + assert.True(t, tb.logCache.contains("skipping request handling of blob URL")) + }) + + return nil + }) } func TestBlockHostnames(t *testing.T) { @@ -56,14 +64,20 @@ func TestBlockHostnames(t *testing.T) { tb.vu.State().Options.BlockedHostnames = blocked p := tb.NewPage(nil) - res := p.Goto("http://host.test/", nil) - assert.Nil(t, res) - - assert.True(t, tb.logCache.contains("was interrupted: hostname host.test is in a blocked pattern")) - // Ensure other requests go through - resp := p.Goto(tb.URL("/get"), nil) - assert.NotNil(t, resp) + require.NoError(t, tb.await(func() error { + tb.promiseThen( + p.Goto("http://host.test/", nil), + func(res api.Response) *goja.Promise { + require.Nil(t, res) + require.True(t, tb.logCache.contains("was interrupted: hostname host.test is in a blocked pattern")) + return p.Goto(tb.URL("/get"), nil) + }, + ).then(func(res api.Response) { + assert.NotNil(t, res) + }) + return nil + })) } func TestBlockIPs(t *testing.T) { @@ -74,15 +88,22 @@ func TestBlockIPs(t *testing.T) { tb.vu.State().Options.BlacklistIPs = []*k6lib.IPNet{ipnet} p := tb.NewPage(nil) - res := p.Goto("http://10.0.0.1:8000/", nil) - assert.Nil(t, res) - - assert.True(t, tb.logCache.contains( - `was interrupted: IP 10.0.0.1 is in a blacklisted range "10.0.0.0/8"`)) - - // Ensure other requests go through - resp := p.Goto(tb.URL("/get"), nil) - assert.NotNil(t, resp) + require.NoError(t, tb.await(func() error { + tb.promiseThen( + p.Goto("http://10.0.0.1:8000/", nil), + func(res api.Response) *goja.Promise { + require.Nil(t, res) + assert.True(t, tb.logCache.contains( + `was interrupted: IP 10.0.0.1 is in a blacklisted range "10.0.0.0/8"`)) + return p.Goto(tb.URL("/get"), nil) + }, + ).then(func(res api.Response) { + // Ensure other requests go through + assert.NotNil(t, res) + }) + + return nil + })) } func TestBasicAuth(t *testing.T) { @@ -96,7 +117,7 @@ func TestBasicAuth(t *testing.T) { auth := func(tb testing.TB, user, pass string) api.Response { tb.Helper() - return browser.NewContext( + p := browser.NewContext( browser.toGojaValue(struct { HttpCredentials *common.Credentials `js:"httpCredentials"` //nolint:revive }{ @@ -105,15 +126,25 @@ func TestBasicAuth(t *testing.T) { Password: pass, }, })). - NewPage(). - Goto( - browser.URL(fmt.Sprintf("/basic-auth/%s/%s", validUser, validPassword)), - browser.toGojaValue(struct { - WaitUntil string `js:"waitUntil"` - }{ - WaitUntil: "load", - }), - ) + NewPage() + + var res api.Response + require.NoError(t, browser.await(func() error { + browser.promiseThen( + p.Goto( + browser.URL(fmt.Sprintf("/basic-auth/%s/%s", validUser, validPassword)), + browser.toGojaValue(struct { + WaitUntil string `js:"waitUntil"` + }{ + WaitUntil: "load", + }), + ), + func(resp api.Response) { + res = resp + }) + return nil + })) + return res } t.Run("valid", func(t *testing.T) { diff --git a/tests/page_test.go b/tests/page_test.go index bd17ec150..5dfc1eea5 100644 --- a/tests/page_test.go +++ b/tests/page_test.go @@ -31,6 +31,7 @@ import ( "testing" "github.com/dop251/goja" + "github.com/grafana/xk6-browser/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -150,18 +151,30 @@ func TestPageGoto(t *testing.T) { p := b.NewPage(nil) url := b.staticURL("empty.html") - r := p.Goto(url, nil) - require.NotNil(t, r) + require.NoError(t, b.await(func() error { + b.promiseThen(p.Goto(url, nil), + func(r api.Response) { + require.NotNil(t, r) + assert.Equal(t, url, r.URL(), `expected URL to be %q, result of navigation was %q`, url, r.URL()) + }) - assert.Equal(t, url, r.URL(), `expected URL to be %q, result of navigation was %q`, url, r.URL()) + return nil + })) } func TestPageGotoDataURI(t *testing.T) { - p := newTestBrowser(t).NewPage(nil) + b := newTestBrowser(t) + p := b.NewPage(nil) - r := p.Goto("data:text/html,hello", nil) + require.NoError(t, b.await(func() error { + b.promiseThen( + p.Goto("data:text/html,hello", nil), + func(r api.Response) { + assert.Nil(t, r, `expected response to be nil`) + }) - assert.Nil(t, r, `expected response to be nil`) + return nil + })) } func TestPageGotoWaitUntilLoad(t *testing.T) { @@ -169,17 +182,21 @@ func TestPageGotoWaitUntilLoad(t *testing.T) { p := b.NewPage(nil) - p.Goto(b.staticURL("wait_until.html"), b.toGojaValue(struct { - WaitUntil string `js:"waitUntil"` - }{WaitUntil: "load"})) - - var ( - results = p.Evaluate(b.toGojaValue("() => window.results")) - actual []string - ) - _ = b.runtime().ExportTo(b.asGojaValue(results), &actual) - - assert.EqualValues(t, []string{"DOMContentLoaded", "load"}, actual, `expected "load" event to have fired`) + require.NoError(t, b.await(func() error { + b.promiseThen( + p.Goto(b.staticURL("wait_until.html"), b.toGojaValue(struct { + WaitUntil string `js:"waitUntil"` + }{WaitUntil: "load"})), func() { + var ( + results = p.Evaluate(b.toGojaValue("() => window.results")) + actual []string + ) + _ = b.runtime().ExportTo(b.asGojaValue(results), &actual) + + assert.EqualValues(t, []string{"DOMContentLoaded", "load"}, actual, `expected "load" event to have fired`) + }) + return nil + })) } func TestPageGotoWaitUntilDOMContentLoaded(t *testing.T) { @@ -187,17 +204,21 @@ func TestPageGotoWaitUntilDOMContentLoaded(t *testing.T) { p := b.NewPage(nil) - p.Goto(b.staticURL("wait_until.html"), b.toGojaValue(struct { - WaitUntil string `js:"waitUntil"` - }{WaitUntil: "domcontentloaded"})) - - var ( - results = p.Evaluate(b.toGojaValue("() => window.results")) - actual []string - ) - _ = b.runtime().ExportTo(b.asGojaValue(results), &actual) - - assert.EqualValues(t, "DOMContentLoaded", actual[0], `expected "DOMContentLoaded" event to have fired`) + require.NoError(t, b.await(func() error { + b.promiseThen( + p.Goto(b.staticURL("wait_until.html"), b.toGojaValue(struct { + WaitUntil string `js:"waitUntil"` + }{WaitUntil: "domcontentloaded"})), func() { + var ( + results = p.Evaluate(b.toGojaValue("() => window.results")) + actual []string + ) + _ = b.runtime().ExportTo(b.asGojaValue(results), &actual) + + assert.EqualValues(t, "DOMContentLoaded", actual[0], `expected "DOMContentLoaded" event to have fired`) + }) + return nil + })) } func TestPageInnerHTML(t *testing.T) { @@ -442,15 +463,22 @@ func TestPageSetExtraHTTPHeaders(t *testing.T) { } p.SetExtraHTTPHeaders(headers) - resp := p.Goto(b.URL("/get"), nil) + require.NoError(t, b.await(func() error { + b.promiseThen( + p.Goto(b.URL("/get"), nil), + + func(resp api.Response) { + require.NotNil(t, resp) + var body struct{ Headers map[string][]string } + err := json.Unmarshal(resp.Body().Bytes(), &body) + require.NoError(t, err) + h := body.Headers["Some-Header"] + require.NotEmpty(t, h) + assert.Equal(t, "Some-Value", h[0]) + }) - require.NotNil(t, resp) - var body struct{ Headers map[string][]string } - err := json.Unmarshal(resp.Body().Bytes(), &body) - require.NoError(t, err) - h := body.Headers["Some-Header"] - require.NotEmpty(t, h) - assert.Equal(t, "Some-Value", h[0]) + return nil + })) } func TestPageWaitForFunction(t *testing.T) { diff --git a/tests/test_browser.go b/tests/test_browser.go index 43cbfaf4f..6b70d7d08 100644 --- a/tests/test_browser.go +++ b/tests/test_browser.go @@ -280,7 +280,7 @@ func (b *testBrowser) awaitWithTimeout(timeout time.Duration, fn func() error) e } } -func (b *testBrowser) promiseThen(promise *goja.Promise, onFulfilled, onRejected func(goja.Value)) *goja.Promise { +func (b *testBrowser) promiseThen(promise *goja.Promise, onFulfilled interface{}, onRejected ...interface{}) testPromise { b.t.Helper() rt := b.runtime() val, err := rt.RunString( @@ -288,16 +288,32 @@ func (b *testBrowser) promiseThen(promise *goja.Promise, onFulfilled, onRejected require.NoError(b.t, err) cal, ok := goja.AssertFunction(val) require.True(b.t, ok) - if onRejected == nil { + + switch len(onRejected) { + case 0: val, err = cal(goja.Undefined(), rt.ToValue(promise), rt.ToValue(onFulfilled)) - } else { - val, err = cal(goja.Undefined(), rt.ToValue(promise), rt.ToValue(onFulfilled), rt.ToValue(onRejected)) + case 1: + val, err = cal(goja.Undefined(), rt.ToValue(promise), rt.ToValue(onFulfilled), rt.ToValue(onRejected[0])) + default: + panic("too many arguments to promiseThen") } require.NoError(b.t, err) newPromise, ok := val.Export().(*goja.Promise) require.True(b.t, ok) - return newPromise + return testPromise{ + Promise: newPromise, + tb: b, + } +} + +type testPromise struct { + *goja.Promise + tb *testBrowser +} + +func (t testPromise) then(onFulfilled interface{}, onRejected ...interface{}) testPromise { + return t.tb.promiseThen(t.Promise, onFulfilled, onRejected...) } func (b *testBrowser) promiseAll(promises ...*goja.Promise) *goja.Promise {