From 5a60a96410b2e00217f19065c30a658a084c9b99 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 16 Dec 2019 22:02:33 -0800 Subject: [PATCH] chore: reuse navigation methods between browsers (#271) --- src/chromium/FrameManager.ts | 76 +++-------------------------- src/firefox/FrameManager.ts | 64 ++---------------------- src/frames.ts | 94 +++++++++++++++++++++++++++++++++--- src/page.ts | 5 +- src/webkit/FrameManager.ts | 57 ++-------------------- test/navigation.spec.js | 3 +- test/page.spec.js | 5 ++ 7 files changed, 113 insertions(+), 191 deletions(-) diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index f8f5617694..0eb69b212c 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -107,79 +107,17 @@ export class FrameManager implements PageDelegate { this._page._didClose(); } - async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}): Promise { - const { - referer = this._networkManager.extraHTTPHeaders()['referer'], - waitUntil = (['load'] as frames.LifecycleEvent[]), - timeout = this._page._timeoutSettings.navigationTimeout(), - } = options; - - const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); - let ensureNewDocumentNavigation = false; - let error = await Promise.race([ - navigate(this._client, url, referer, frame._id), - watcher.timeoutOrTerminationPromise, - ]); - if (!error) { - error = await Promise.race([ - watcher.timeoutOrTerminationPromise, - ensureNewDocumentNavigation ? watcher.newDocumentNavigationPromise : watcher.sameDocumentNavigationPromise, - ]); - } - watcher.dispose(); - if (error) - throw error; - return watcher.navigationResponse(); - - async function navigate(client: CDPSession, url: string, referrer: string, frameId: string): Promise { - try { - const response = await client.send('Page.navigate', {url, referrer, frameId}); - ensureNewDocumentNavigation = !!response.loaderId; - return response.errorText ? new Error(`${response.errorText} at ${url}`) : null; - } catch (error) { - return error; - } - } + async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise { + const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id }); + if (response.errorText) + throw new Error(`${response.errorText} at ${url}`); + return { newDocumentId: response.loaderId, isSameDocument: !response.loaderId }; } - async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}): Promise { - const { - waitUntil = (['load'] as frames.LifecycleEvent[]), - timeout = this._page._timeoutSettings.navigationTimeout(), - } = options; - const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); - const error = await Promise.race([ - watcher.timeoutOrTerminationPromise, - watcher.sameDocumentNavigationPromise, - watcher.newDocumentNavigationPromise, - ]); - watcher.dispose(); - if (error) - throw error; - return watcher.navigationResponse(); - } - - async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) { - const { - waitUntil = (['load'] as frames.LifecycleEvent[]), - timeout = this._page._timeoutSettings.navigationTimeout(), - } = options; - const context = await frame._utilityContext(); + needsLifecycleResetOnSetContent(): boolean { // We rely upon the fact that document.open() will reset frame lifecycle with "init" // lifecycle event. @see https://crrev.com/608658 - await context.evaluate(html => { - document.open(); - document.write(html); - document.close(); - }, html); - const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); - const error = await Promise.race([ - watcher.timeoutOrTerminationPromise, - watcher.lifecyclePromise, - ]); - watcher.dispose(); - if (error) - throw error; + return false; } _onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) { diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 6d5a357b0d..cb96f7dfe6 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -173,67 +173,13 @@ export class FrameManager implements PageDelegate { this._page._didClose(); } - async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}) { - const { - timeout = this._page._timeoutSettings.navigationTimeout(), - waitUntil = (['load'] as frames.LifecycleEvent[]), - } = options; - - const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); - const error = await Promise.race([ - watcher.timeoutOrTerminationPromise, - watcher.newDocumentNavigationPromise, - watcher.sameDocumentNavigationPromise, - ]); - watcher.dispose(); - if (error) - throw error; - return watcher.navigationResponse(); + async navigateFrame(frame: frames.Frame, url: string, referer: string | undefined): Promise { + const response = await this._session.send('Page.navigate', { url, referer, frameId: frame._id }); + return { newDocumentId: response.navigationId, isSameDocument: !response.navigationId }; } - async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}) { - const { - timeout = this._page._timeoutSettings.navigationTimeout(), - waitUntil = (['load'] as frames.LifecycleEvent[]), - referer, - } = options; - const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); - await this._session.send('Page.navigate', { - frameId: frame._id, - referer, - url, - }); - const error = await Promise.race([ - watcher.timeoutOrTerminationPromise, - watcher.newDocumentNavigationPromise, - watcher.sameDocumentNavigationPromise, - ]); - watcher.dispose(); - if (error) - throw error; - return watcher.navigationResponse(); - } - - async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) { - const { - waitUntil = (['load'] as frames.LifecycleEvent[]), - timeout = this._page._timeoutSettings.navigationTimeout(), - } = options; - const context = await frame._utilityContext(); - frame._firedLifecycleEvents.clear(); - await context.evaluate(html => { - document.open(); - document.write(html); - document.close(); - }, html); - const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); - const error = await Promise.race([ - watcher.timeoutOrTerminationPromise, - watcher.lifecyclePromise, - ]); - watcher.dispose(); - if (error) - throw error; + needsLifecycleResetOnSetContent(): boolean { + return true; } setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise { diff --git a/src/frames.ts b/src/frames.ts index e6d3b71d89..9789f2697b 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -45,6 +45,10 @@ export type NavigateOptions = { export type GotoOptions = NavigateOptions & { referer?: string, }; +export type GotoResult = { + newDocumentId?: string, + isSameDocument?: boolean, +}; export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'; const kLifecycleEvents: Set = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']); @@ -280,12 +284,60 @@ export class Frame { this._parentFrame._childFrames.add(this); } - async goto(url: string, options?: GotoOptions): Promise { - return this._page._delegate.navigateFrame(this, url, options); + async goto(url: string, options: GotoOptions = {}): Promise { + const { + referer = (this._page._state.extraHTTPHeaders || {})['referer'], + waitUntil = (['load'] as LifecycleEvent[]), + timeout = this._page._timeoutSettings.navigationTimeout(), + } = options; + const watcher = new LifecycleWatcher(this, waitUntil, timeout); + + let navigateResult: GotoResult; + const navigate = async () => { + try { + navigateResult = await this._page._delegate.navigateFrame(this, url, referer); + } catch (error) { + return error; + } + }; + + let error = await Promise.race([ + navigate(), + watcher.timeoutOrTerminationPromise, + ]); + if (!error) { + const promises = [watcher.timeoutOrTerminationPromise]; + if (navigateResult.newDocumentId) { + watcher.setExpectedDocumentId(navigateResult.newDocumentId, url); + promises.push(watcher.newDocumentNavigationPromise); + } else if (navigateResult.isSameDocument) { + promises.push(watcher.sameDocumentNavigationPromise); + } else { + promises.push(watcher.sameDocumentNavigationPromise, watcher.newDocumentNavigationPromise); + } + error = await Promise.race(promises); + } + watcher.dispose(); + if (error) + throw error; + return watcher.navigationResponse(); } - async waitForNavigation(options?: NavigateOptions): Promise { - return this._page._delegate.waitForFrameNavigation(this, options); + async waitForNavigation(options: NavigateOptions = {}): Promise { + const { + waitUntil = (['load'] as LifecycleEvent[]), + timeout = this._page._timeoutSettings.navigationTimeout(), + } = options; + const watcher = new LifecycleWatcher(this, waitUntil, timeout); + const error = await Promise.race([ + watcher.timeoutOrTerminationPromise, + watcher.sameDocumentNavigationPromise, + watcher.newDocumentNavigationPromise, + ]); + watcher.dispose(); + if (error) + throw error; + return watcher.navigationResponse(); } _mainContext(): Promise { @@ -351,8 +403,27 @@ export class Frame { }); } - async setContent(html: string, options?: NavigateOptions): Promise { - return this._page._delegate.setFrameContent(this, html, options); + async setContent(html: string, options: NavigateOptions = {}): Promise { + const { + waitUntil = (['load'] as LifecycleEvent[]), + timeout = this._page._timeoutSettings.navigationTimeout(), + } = options; + const context = await this._utilityContext(); + if (this._page._delegate.needsLifecycleResetOnSetContent()) + this._firedLifecycleEvents.clear(); + await context.evaluate(html => { + document.open(); + document.write(html); + document.close(); + }, html); + const watcher = new LifecycleWatcher(this, waitUntil, timeout); + const error = await Promise.race([ + watcher.timeoutOrTerminationPromise, + watcher.lifecyclePromise, + ]); + watcher.dispose(); + if (error) + throw error; } name(): string { @@ -805,6 +876,13 @@ export class LifecycleWatcher { this._checkLifecycleComplete(); } + setExpectedDocumentId(documentId: string, url: string) { + this._expectedDocumentId = documentId; + this._targetUrl = url; + if (this._navigationRequest && this._navigationRequest._documentId !== documentId) + this._navigationRequest = null; + } + _onFrameDetached(frame: Frame) { if (this._frame === frame) { this._frameDetachedCallback.call(null, new Error('Navigating frame was detached')); @@ -822,7 +900,9 @@ export class LifecycleWatcher { _onNavigationRequest(frame: Frame, request: network.Request) { assert(request._documentId); - if (frame === this._frame && this._expectedDocumentId === undefined) { + if (frame !== this._frame) + return; + if (this._expectedDocumentId === undefined || this._expectedDocumentId === request._documentId) { this._navigationRequest = request; this._expectedDocumentId = request._documentId; this._targetUrl = request.url(); diff --git a/src/page.ts b/src/page.ts index 5d2fc4cab9..638e798b18 100644 --- a/src/page.ts +++ b/src/page.ts @@ -41,9 +41,8 @@ export interface PageDelegate { evaluateOnNewDocument(source: string): Promise; closePage(runBeforeUnload: boolean): Promise; - navigateFrame(frame: frames.Frame, url: string, options?: frames.GotoOptions): Promise; - waitForFrameNavigation(frame: frames.Frame, options?: frames.NavigateOptions): Promise; - setFrameContent(frame: frames.Frame, html: string, options?: frames.NavigateOptions): Promise; + navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise; + needsLifecycleResetOnSetContent(): boolean; setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise; setUserAgent(userAgent: string): Promise; diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index f0da9baa98..dbccef357b 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -187,60 +187,13 @@ export class FrameManager implements PageDelegate { this._contextIdToContext.set(contextPayload.id, context); } - async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}): Promise { - const { - timeout = this._page._timeoutSettings.navigationTimeout(), - waitUntil = (['load'] as frames.LifecycleEvent[]) - } = options; - const watchDog = new frames.LifecycleWatcher(frame, waitUntil, timeout); - await this._session.send('Page.navigate', {url, frameId: frame._id}); - const error = await Promise.race([ - watchDog.timeoutOrTerminationPromise, - watchDog.newDocumentNavigationPromise, - watchDog.sameDocumentNavigationPromise, - ]); - watchDog.dispose(); - if (error) - throw error; - return watchDog.navigationResponse(); + async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise { + await this._session.send('Page.navigate', { url, frameId: frame._id }); + return {}; // We cannot get loaderId of cross-process navigation in advance. } - async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}): Promise { - const { - timeout = this._page._timeoutSettings.navigationTimeout(), - waitUntil = (['load'] as frames.LifecycleEvent[]) - } = options; - const watchDog = new frames.LifecycleWatcher(frame, waitUntil, timeout); - const error = await Promise.race([ - watchDog.timeoutOrTerminationPromise, - watchDog.newDocumentNavigationPromise, - watchDog.sameDocumentNavigationPromise, - ]); - watchDog.dispose(); - if (error) - throw error; - return watchDog.navigationResponse(); - } - - async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) { - // We rely upon the fact that document.open() will trigger Page.loadEventFired. - const { - timeout = this._page._timeoutSettings.navigationTimeout(), - waitUntil = (['load'] as frames.LifecycleEvent[]) - } = options; - const watchDog = new frames.LifecycleWatcher(frame, waitUntil, timeout); - await frame.evaluate(html => { - document.open(); - document.write(html); - document.close(); - }, html); - const error = await Promise.race([ - watchDog.timeoutOrTerminationPromise, - watchDog.lifecyclePromise, - ]); - watchDog.dispose(); - if (error) - throw error; + needsLifecycleResetOnSetContent(): boolean { + return true; } async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) { diff --git a/test/navigation.spec.js b/test/navigation.spec.js index 052dd88f17..a1f0cfe1ca 100644 --- a/test/navigation.spec.js +++ b/test/navigation.spec.js @@ -125,7 +125,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'domcontentloaded'}); expect(response.status()).toBe(200); }); - it.skip(WEBKIT)('should work when page calls history API in beforeunload', async({page, server}) => { + it('should work when page calls history API in beforeunload', async({page, server}) => { await page.goto(server.EMPTY_PAGE); await page.evaluate(() => { window.addEventListener('beforeunload', () => history.replaceState(null, 'initial', window.location.href), false); @@ -427,6 +427,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME expect(request1.headers['referer']).toBe('http://google.com/'); // Make sure subresources do not inherit referer. expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html'); + expect(page.url()).toBe(server.PREFIX + '/grid.html'); }); }); diff --git a/test/page.spec.js b/test/page.spec.js index c64abffd1b..5b4d56458f 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -530,6 +530,11 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF const result = await page.content(); expect(result).toBe(expectedOutput); }); + it('should work with domcontentloaded', async({page, server}) => { + await page.setContent('
hello
', { waitUntil: 'domcontentloaded' }); + const result = await page.content(); + expect(result).toBe(expectedOutput); + }); it('should not confuse with previous navigation', async({page, server}) => { const imgPath = '/img.png'; let imgResponse = null;