From 57acdfd8606d69008005529598946827cdd0acf0 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 11 Dec 2019 07:17:32 -0800 Subject: [PATCH] chore: reuse LifecycleWatcher between browsers (#208) --- src/chromium/FrameManager.ts | 52 ++++---- src/chromium/LifecycleWatcher.ts | 165 ------------------------ src/firefox/FrameManager.ts | 121 +++++++----------- src/firefox/NavigationWatchdog.ts | 149 ---------------------- src/frames.ts | 180 ++++++++++++++++++++++++-- src/page.ts | 1 + src/webkit/FrameManager.ts | 202 +++++------------------------- test/mouse.spec.js | 2 +- test/navigation.spec.js | 8 +- test/page.spec.js | 2 +- 10 files changed, 272 insertions(+), 610 deletions(-) delete mode 100644 src/chromium/LifecycleWatcher.ts delete mode 100644 src/firefox/NavigationWatchdog.ts diff --git a/src/chromium/FrameManager.ts b/src/chromium/FrameManager.ts index 544029830c..495cb40397 100644 --- a/src/chromium/FrameManager.ts +++ b/src/chromium/FrameManager.ts @@ -24,7 +24,6 @@ import * as network from '../network'; import { CDPSession } from './Connection'; import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate } from './ExecutionContext'; import { DOMWorldDelegate } from './JSHandle'; -import { LifecycleWatcher } from './LifecycleWatcher'; import { NetworkManager, NetworkManagerEvents } from './NetworkManager'; import { Page } from '../page'; import { Protocol } from './protocol'; @@ -59,7 +58,6 @@ export const FrameManagerEvents = { const frameDataSymbol = Symbol('frameData'); type FrameData = { id: string, - loaderId: string, }; export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate { @@ -149,16 +147,16 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, timeout = this._page._timeoutSettings.navigationTimeout(), } = options; - const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout); + const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); let ensureNewDocumentNavigation = false; let error = await Promise.race([ navigate(this._client, url, referer, this._frameData(frame).id), - watcher.timeoutOrTerminationPromise(), + watcher.timeoutOrTerminationPromise, ]); if (!error) { error = await Promise.race([ - watcher.timeoutOrTerminationPromise(), - ensureNewDocumentNavigation ? watcher.newDocumentNavigationPromise() : watcher.sameDocumentNavigationPromise(), + watcher.timeoutOrTerminationPromise, + ensureNewDocumentNavigation ? watcher.newDocumentNavigationPromise : watcher.sameDocumentNavigationPromise, ]); } watcher.dispose(); @@ -183,11 +181,11 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, waitUntil = (['load'] as frames.LifecycleEvent[]), timeout = this._page._timeoutSettings.navigationTimeout(), } = options; - const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout); + const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); const error = await Promise.race([ - watcher.timeoutOrTerminationPromise(), - watcher.sameDocumentNavigationPromise(), - watcher.newDocumentNavigationPromise() + watcher.timeoutOrTerminationPromise, + watcher.sameDocumentNavigationPromise, + watcher.newDocumentNavigationPromise, ]); watcher.dispose(); if (error) @@ -208,10 +206,10 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, document.write(html); document.close(); }, html); - const watcher = new LifecycleWatcher(this, frame, waitUntil, timeout); + const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); const error = await Promise.race([ - watcher.timeoutOrTerminationPromise(), - watcher.lifecyclePromise(), + watcher.timeoutOrTerminationPromise, + watcher.lifecyclePromise, ]); watcher.dispose(); if (error) @@ -222,15 +220,14 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, const frame = this._frames.get(event.frameId); if (!frame) return; - const data = this._frameData(frame); if (event.name === 'init') { - data.loaderId = event.loaderId; frame._firedLifecycleEvents.clear(); + frame._onExpectedNewDocumentNavigation(event.loaderId); + } else if (event.name === 'load') { + frame._lifecycleEvent('load'); + } else if (event.name === 'DOMContentLoaded') { + frame._lifecycleEvent('domcontentloaded'); } - if (event.name === 'load') - frame._firedLifecycleEvents.add('load'); - else if (event.name === 'DOMContentLoaded') - frame._firedLifecycleEvents.add('domcontentloaded'); this.emit(FrameManagerEvents.LifecycleEvent, frame); } @@ -238,8 +235,8 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, const frame = this._frames.get(frameId); if (!frame) return; - frame._firedLifecycleEvents.add('domcontentloaded'); - frame._firedLifecycleEvents.add('load'); + frame._lifecycleEvent('domcontentloaded'); + frame._lifecycleEvent('load'); this.emit(FrameManagerEvents.LifecycleEvent, frame); } @@ -275,10 +272,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, return; assert(parentFrameId); const parentFrame = this._frames.get(parentFrameId); - const frame = new frames.Frame(this, this._page._timeoutSettings, parentFrame); + const frame = new frames.Frame(this, this._page, parentFrame); const data: FrameData = { id: frameId, - loaderId: '', }; (frame as any)[frameDataSymbol] = data; this._frames.set(frameId, frame); @@ -306,10 +302,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, data.id = framePayload.id; } else { // Initial main frame navigation. - frame = new frames.Frame(this, this._page._timeoutSettings, null); + frame = new frames.Frame(this, this._page, null); const data: FrameData = { id: framePayload.id, - loaderId: '', }; (frame as any)[frameDataSymbol] = data; } @@ -317,8 +312,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, this._mainFrame = frame; } - // Update frame payload. - frame._navigated(framePayload.url, framePayload.name); + frame._onCommittedNewDocumentNavigation(framePayload.url, framePayload.name, framePayload.loaderId); this.emit(FrameManagerEvents.FrameNavigated, frame); this._page.emit(Events.Page.FrameNavigated, frame); @@ -343,7 +337,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, const frame = this._frames.get(frameId); if (!frame) return; - frame._navigated(url, frame.name()); + frame._onCommittedSameDocumentNavigation(url); this.emit(FrameManagerEvents.FrameNavigatedWithinDocument, frame); this.emit(FrameManagerEvents.FrameNavigated, frame); this._page.emit(Events.Page.FrameNavigated, frame); @@ -395,7 +389,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, _removeFramesRecursively(frame: frames.Frame) { for (const child of frame.childFrames()) this._removeFramesRecursively(child); - frame._detach(); + frame._onDetached(); this._frames.delete(this._frameData(frame).id); this.emit(FrameManagerEvents.FrameDetached, frame); this._page.emit(Events.Page.FrameDetached, frame); diff --git a/src/chromium/LifecycleWatcher.ts b/src/chromium/LifecycleWatcher.ts deleted file mode 100644 index 2f0c86b7ee..0000000000 --- a/src/chromium/LifecycleWatcher.ts +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Copyright 2019 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { CDPSessionEvents } from './Connection'; -import { TimeoutError } from '../Errors'; -import { FrameManager, FrameManagerEvents } from './FrameManager'; -import { helper, RegisteredListener } from '../helper'; -import { NetworkManagerEvents } from './NetworkManager'; -import * as frames from '../frames'; -import * as network from '../network'; - -export class LifecycleWatcher { - private _expectedLifecycle: frames.LifecycleEvent[]; - private _frameManager: FrameManager; - private _frame: frames.Frame; - private _initialLoaderId: string; - private _timeout: number; - private _navigationRequest: network.Request | null = null; - private _eventListeners: RegisteredListener[]; - private _sameDocumentNavigationPromise: Promise; - private _sameDocumentNavigationCompleteCallback: () => void; - private _lifecyclePromise: Promise; - private _lifecycleCallback: () => void; - private _newDocumentNavigationPromise: Promise; - private _newDocumentNavigationCompleteCallback: () => void; - private _timeoutPromise: Promise; - private _terminationPromise: Promise; - private _terminationCallback: (err: Error | null) => void; - private _maximumTimer: NodeJS.Timer; - private _hasSameDocumentNavigation: boolean; - - constructor(frameManager: FrameManager, frame: frames.Frame, waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[], timeout: number) { - if (Array.isArray(waitUntil)) - waitUntil = waitUntil.slice(); - else if (typeof waitUntil === 'string') - waitUntil = [waitUntil]; - this._expectedLifecycle = waitUntil.slice(); - this._frameManager = frameManager; - this._frame = frame; - this._initialLoaderId = frameManager._frameData(frame).loaderId; - this._timeout = timeout; - this._eventListeners = [ - helper.addEventListener(frameManager._client, CDPSessionEvents.Disconnected, () => this._terminate(new Error('Navigation failed because browser has disconnected!'))), - helper.addEventListener(this._frameManager, FrameManagerEvents.LifecycleEvent, this._checkLifecycleComplete.bind(this)), - helper.addEventListener(this._frameManager, FrameManagerEvents.FrameNavigatedWithinDocument, this._navigatedWithinDocument.bind(this)), - helper.addEventListener(this._frameManager, FrameManagerEvents.FrameDetached, this._onFrameDetached.bind(this)), - helper.addEventListener(this._frameManager.networkManager(), NetworkManagerEvents.Request, this._onRequest.bind(this)), - ]; - - this._sameDocumentNavigationPromise = new Promise(fulfill => { - this._sameDocumentNavigationCompleteCallback = fulfill; - }); - - this._lifecyclePromise = new Promise(fulfill => { - this._lifecycleCallback = fulfill; - }); - - this._newDocumentNavigationPromise = new Promise(fulfill => { - this._newDocumentNavigationCompleteCallback = fulfill; - }); - - this._timeoutPromise = this._createTimeoutPromise(); - this._terminationPromise = new Promise(fulfill => { - this._terminationCallback = fulfill; - }); - this._checkLifecycleComplete(); - } - - _onRequest(request: network.Request) { - if (request.frame() !== this._frame || !request.isNavigationRequest()) - return; - this._navigationRequest = request; - } - - _onFrameDetached(frame: frames.Frame) { - if (this._frame === frame) { - this._terminationCallback.call(null, new Error('Navigating frame was detached')); - return; - } - this._checkLifecycleComplete(); - } - - navigationResponse(): network.Response | null { - return this._navigationRequest ? this._navigationRequest.response() : null; - } - - _terminate(error: Error) { - this._terminationCallback.call(null, error); - } - - sameDocumentNavigationPromise(): Promise { - return this._sameDocumentNavigationPromise; - } - - newDocumentNavigationPromise(): Promise { - return this._newDocumentNavigationPromise; - } - - lifecyclePromise(): Promise { - return this._lifecyclePromise; - } - - timeoutOrTerminationPromise(): Promise { - return Promise.race([this._timeoutPromise, this._terminationPromise]); - } - - _createTimeoutPromise(): Promise { - if (!this._timeout) - return new Promise(() => {}); - const errorMessage = 'Navigation timeout of ' + this._timeout + ' ms exceeded'; - return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, this._timeout)) - .then(() => new TimeoutError(errorMessage)); - } - - _navigatedWithinDocument(frame: frames.Frame) { - if (frame !== this._frame) - return; - this._hasSameDocumentNavigation = true; - this._checkLifecycleComplete(); - } - - _checkLifecycleComplete() { - const checkLifecycle = (frame: frames.Frame, expectedLifecycle: frames.LifecycleEvent[]): boolean => { - for (const event of expectedLifecycle) { - if (!frame._firedLifecycleEvents.has(event)) - return false; - } - for (const child of frame.childFrames()) { - if (!checkLifecycle(child, expectedLifecycle)) - return false; - } - return true; - }; - - // We expect navigation to commit. - if (!checkLifecycle(this._frame, this._expectedLifecycle)) - return; - this._lifecycleCallback(); - if (this._frameManager._frameData(this._frame).loaderId === this._initialLoaderId && !this._hasSameDocumentNavigation) - return; - if (this._hasSameDocumentNavigation) - this._sameDocumentNavigationCompleteCallback(); - if (this._frameManager._frameData(this._frame).loaderId !== this._initialLoaderId) - this._newDocumentNavigationCompleteCallback(); - } - - dispose() { - helper.removeEventListeners(this._eventListeners); - clearTimeout(this._maximumTimer); - } -} diff --git a/src/firefox/FrameManager.ts b/src/firefox/FrameManager.ts index 7848daa2e9..4ba8fa63ee 100644 --- a/src/firefox/FrameManager.ts +++ b/src/firefox/FrameManager.ts @@ -16,14 +16,12 @@ */ import { EventEmitter } from 'events'; -import { TimeoutError } from '../Errors'; import * as frames from '../frames'; import { assert, helper, RegisteredListener, debugError } from '../helper'; import * as js from '../javascript'; import * as dom from '../dom'; import { JugglerSession } from './Connection'; import { ExecutionContextDelegate } from './ExecutionContext'; -import { NavigationWatchdog, NextNavigationWatchdog } from './NavigationWatchdog'; import { Page, PageDelegate } from '../page'; import { NetworkManager, NetworkManagerEvents } from './NetworkManager'; import { DOMWorldDelegate } from './JSHandle'; @@ -51,7 +49,6 @@ export const FrameManagerEvents = { const frameDataSymbol = Symbol('frameData'); type FrameData = { frameId: string, - lastCommittedNavigationId: string, }; export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate { @@ -80,7 +77,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, helper.addEventListener(this._session, 'Page.eventFired', this._onEventFired.bind(this)), helper.addEventListener(this._session, 'Page.frameAttached', this._onFrameAttached.bind(this)), helper.addEventListener(this._session, 'Page.frameDetached', this._onFrameDetached.bind(this)), + helper.addEventListener(this._session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)), helper.addEventListener(this._session, 'Page.navigationCommitted', this._onNavigationCommitted.bind(this)), + helper.addEventListener(this._session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)), helper.addEventListener(this._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)), helper.addEventListener(this._session, 'Runtime.executionContextCreated', this._onExecutionContextCreated.bind(this)), helper.addEventListener(this._session, 'Runtime.executionContextDestroyed', this._onExecutionContextDestroyed.bind(this)), @@ -157,29 +156,35 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, } } + _onNavigationStarted(params) { + const frame = this._frames.get(params.frameId); + frame._onExpectedNewDocumentNavigation(params.navigationId, params.url); + } + + _onNavigationAborted(params) { + const frame = this._frames.get(params.frameId); + frame._onAbortedNewDocumentNavigation(params.navigationId, params.errorText); + } + _onNavigationCommitted(params) { const frame = this._frames.get(params.frameId); - frame._navigated(params.url, params.name); - const data = this._frameData(frame); - data.lastCommittedNavigationId = params.navigationId; - frame._firedLifecycleEvents.clear(); + frame._onCommittedNewDocumentNavigation(params.url, params.name, params.navigationId); this.emit(FrameManagerEvents.FrameNavigated, frame); this._page.emit(Events.Page.FrameNavigated, frame); } _onSameDocumentNavigation(params) { const frame = this._frames.get(params.frameId); - frame._navigated(params.url, frame.name()); + frame._onCommittedSameDocumentNavigation(params.url); this.emit(FrameManagerEvents.FrameNavigated, frame); this._page.emit(Events.Page.FrameNavigated, frame); } _onFrameAttached(params) { const parentFrame = this._frames.get(params.parentFrameId) || null; - const frame = new frames.Frame(this, this._page._timeoutSettings, parentFrame); + const frame = new frames.Frame(this, this._page, parentFrame); const data: FrameData = { frameId: params.frameId, - lastCommittedNavigationId: '', }; frame[frameDataSymbol] = data; if (!parentFrame) { @@ -194,7 +199,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, _onFrameDetached(params) { const frame = this._frames.get(params.frameId); this._frames.delete(params.frameId); - frame._detach(); + frame._onDetached(); this.emit(FrameManagerEvents.FrameDetached, frame); this._page.emit(Events.Page.FrameDetached, frame); } @@ -202,14 +207,14 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, _onEventFired({frameId, name}) { const frame = this._frames.get(frameId); if (name === 'load') { - frame._firedLifecycleEvents.add('load'); + frame._lifecycleEvent('load'); if (frame === this._mainFrame) { this.emit(FrameManagerEvents.Load); this._page.emit(Events.Page.Load); } } if (name === 'DOMContentLoaded') { - frame._firedLifecycleEvents.add('domcontentloaded'); + frame._lifecycleEvent('domcontentloaded'); if (frame === this._mainFrame) { this.emit(FrameManagerEvents.DOMContentLoaded); this._page.emit(Events.Page.DOMContentLoaded); @@ -265,44 +270,17 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, timeout = this._page._timeoutSettings.navigationTimeout(), waitUntil = (['load'] as frames.LifecycleEvent[]), } = options; - const normalizedWaitUntil = normalizeWaitUntil(waitUntil); - const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded'); - let timeoutCallback; - const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); - const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - - const nextNavigationDog = new NextNavigationWatchdog(this, frame); - const error1 = await Promise.race([ - nextNavigationDog.promise(), - timeoutPromise, - ]); - nextNavigationDog.dispose(); - - // If timeout happened first - throw. - if (error1) { - clearTimeout(timeoutId); - throw error1; - } - - const {navigationId, url} = nextNavigationDog.navigation(); - - if (!navigationId) { - // Same document navigation happened. - clearTimeout(timeoutId); - return null; - } - - const watchDog = new NavigationWatchdog(this, frame, this._networkManager, navigationId, url, normalizedWaitUntil); + const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); const error = await Promise.race([ - timeoutPromise, - watchDog.promise(), + watcher.timeoutOrTerminationPromise, + watcher.newDocumentNavigationPromise, + watcher.sameDocumentNavigationPromise, ]); - watchDog.dispose(); - clearTimeout(timeoutId); + watcher.dispose(); if (error) throw error; - return watchDog.navigationResponse(); + return watcher.navigationResponse(); } async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}) { @@ -311,30 +289,21 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, waitUntil = (['load'] as frames.LifecycleEvent[]), referer, } = options; - const normalizedWaitUntil = normalizeWaitUntil(waitUntil); - const {navigationId} = await this._session.send('Page.navigate', { + const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); + await this._session.send('Page.navigate', { frameId: this._frameData(frame).frameId, referer, url, }); - if (!navigationId) - return; - - const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded'); - let timeoutCallback; - const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); - const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - - const watchDog = new NavigationWatchdog(this, frame, this._networkManager, navigationId, url, normalizedWaitUntil); const error = await Promise.race([ - timeoutPromise, - watchDog.promise(), + watcher.timeoutOrTerminationPromise, + watcher.newDocumentNavigationPromise, + watcher.sameDocumentNavigationPromise, ]); - watchDog.dispose(); - clearTimeout(timeoutId); + watcher.dispose(); if (error) throw error; - return watchDog.navigationResponse(); + return watcher.navigationResponse(); } async setFrameContent(frame: frames.Frame, html: string) { @@ -387,32 +356,28 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, await this._session.send('Page.setCacheDisabled', {cacheDisabled: !enabled}); } - private async _go(action: () => Promise<{ navigationId: string | null, navigationURL: string | null }>, options: frames.NavigateOptions = {}) { + private async _go(action: () => Promise<{ navigationId: string | null, navigationURL: string | null }>, options: frames.NavigateOptions = {}): Promise { const { timeout = this._page._timeoutSettings.navigationTimeout(), waitUntil = (['load'] as frames.LifecycleEvent[]), } = options; const frame = this.mainFrame(); - const normalizedWaitUntil = normalizeWaitUntil(waitUntil); - const { navigationId, navigationURL } = await action(); - if (!navigationId) + const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout); + const { navigationId } = await action(); + if (navigationId === null) { + // Cannot go back/forward. + watcher.dispose(); return null; - - const timeoutError = new TimeoutError('Navigation timeout of ' + timeout + ' ms exceeded'); - let timeoutCallback; - const timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); - const timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - - const watchDog = new NavigationWatchdog(this, frame, this._networkManager, navigationId, navigationURL, normalizedWaitUntil); + } const error = await Promise.race([ - timeoutPromise, - watchDog.promise(), + watcher.timeoutOrTerminationPromise, + watcher.newDocumentNavigationPromise, + watcher.sameDocumentNavigationPromise, ]); - watchDog.dispose(); - clearTimeout(timeoutId); + watcher.dispose(); if (error) throw error; - return watchDog.navigationResponse(); + return watcher.navigationResponse(); } reload(options?: frames.NavigateOptions): Promise { diff --git a/src/firefox/NavigationWatchdog.ts b/src/firefox/NavigationWatchdog.ts deleted file mode 100644 index 3549867be2..0000000000 --- a/src/firefox/NavigationWatchdog.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Copyright 2019 Google Inc. All rights reserved. - * Modifications copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { helper, RegisteredListener } from '../helper'; -import { JugglerSessionEvents } from './Connection'; -import { FrameManagerEvents, FrameManager } from './FrameManager'; -import { NetworkManager, NetworkManagerEvents } from './NetworkManager'; -import * as frames from '../frames'; -import * as network from '../network'; - -export class NextNavigationWatchdog { - private _frameManager: FrameManager; - private _navigatedFrame: frames.Frame; - private _promise: Promise; - private _resolveCallback: (value?: unknown) => void; - private _navigation: {navigationId: number|null, url?: string} = null; - private _eventListeners: RegisteredListener[]; - - constructor(frameManager: FrameManager, navigatedFrame: frames.Frame) { - this._frameManager = frameManager; - this._navigatedFrame = navigatedFrame; - this._promise = new Promise(x => this._resolveCallback = x); - this._eventListeners = [ - helper.addEventListener(frameManager._session, 'Page.navigationStarted', this._onNavigationStarted.bind(this)), - helper.addEventListener(frameManager._session, 'Page.sameDocumentNavigation', this._onSameDocumentNavigation.bind(this)), - ]; - } - - promise() { - return this._promise; - } - - navigation() { - return this._navigation; - } - - _onNavigationStarted(params) { - if (params.frameId === this._frameManager._frameData(this._navigatedFrame).frameId) { - this._navigation = { - navigationId: params.navigationId, - url: params.url, - }; - this._resolveCallback(); - } - } - - _onSameDocumentNavigation(params) { - if (params.frameId === this._frameManager._frameData(this._navigatedFrame).frameId) { - this._navigation = { - navigationId: null, - }; - this._resolveCallback(); - } - } - - dispose() { - helper.removeEventListeners(this._eventListeners); - } -} - -export class NavigationWatchdog { - private _frameManager: FrameManager; - private _navigatedFrame: frames.Frame; - private _targetNavigationId: any; - private _firedEvents: frames.LifecycleEvent[]; - private _targetURL: any; - private _promise: Promise; - private _resolveCallback: (value?: unknown) => void; - private _navigationRequest: network.Request | null; - private _eventListeners: RegisteredListener[]; - - constructor(frameManager: FrameManager, navigatedFrame: frames.Frame, networkManager: NetworkManager, targetNavigationId, targetURL, firedEvents: frames.LifecycleEvent[]) { - this._frameManager = frameManager; - this._navigatedFrame = navigatedFrame; - this._targetNavigationId = targetNavigationId; - this._firedEvents = firedEvents; - this._targetURL = targetURL; - - this._promise = new Promise(x => this._resolveCallback = x); - this._navigationRequest = null; - - const check = this._checkNavigationComplete.bind(this); - this._eventListeners = [ - helper.addEventListener(frameManager._session, JugglerSessionEvents.Disconnected, () => this._resolveCallback(new Error('Navigation failed because browser has disconnected!'))), - helper.addEventListener(frameManager._session, 'Page.eventFired', check), - helper.addEventListener(frameManager._session, 'Page.frameAttached', check), - helper.addEventListener(frameManager._session, 'Page.frameDetached', check), - helper.addEventListener(frameManager._session, 'Page.navigationStarted', check), - helper.addEventListener(frameManager._session, 'Page.navigationCommitted', check), - helper.addEventListener(frameManager._session, 'Page.navigationAborted', this._onNavigationAborted.bind(this)), - helper.addEventListener(networkManager, NetworkManagerEvents.Request, this._onRequest.bind(this)), - helper.addEventListener(frameManager, FrameManagerEvents.FrameDetached, check), - ]; - check(); - } - - _onRequest(request) { - if (request.frame() !== this._navigatedFrame || !request.isNavigationRequest()) - return; - this._navigationRequest = request; - } - - navigationResponse(): network.Response | null { - return this._navigationRequest ? this._navigationRequest.response() : null; - } - - _checkNavigationComplete() { - const checkFiredEvents = (frame: frames.Frame, firedEvents: frames.LifecycleEvent[]) => { - for (const subframe of frame.childFrames()) { - if (!checkFiredEvents(subframe, firedEvents)) - return false; - } - return firedEvents.every(event => frame._firedLifecycleEvents.has(event)); - }; - - if (this._navigatedFrame.isDetached()) - this._resolveCallback(new Error('Navigating frame was detached')); - else if (this._frameManager._frameData(this._navigatedFrame).lastCommittedNavigationId === this._targetNavigationId - && checkFiredEvents(this._navigatedFrame, this._firedEvents)) - this._resolveCallback(null); - } - - _onNavigationAborted(params) { - if (params.frameId === this._frameManager._frameData(this._navigatedFrame).frameId && params.navigationId === this._targetNavigationId) - this._resolveCallback(new Error('Navigation to ' + this._targetURL + ' failed: ' + params.errorText)); - } - - promise() { - return this._promise; - } - - dispose() { - helper.removeEventListeners(this._eventListeners); - } -} diff --git a/src/frames.ts b/src/frames.ts index d3bf71ab8a..553bd1f194 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -20,10 +20,12 @@ import * as fs from 'fs'; import * as js from './javascript'; import * as dom from './dom'; import * as network from './network'; -import { helper, assert } from './helper'; +import { helper, assert, RegisteredListener } from './helper'; import { ClickOptions, MultiClickOptions, PointerActionOptions, SelectOption } from './input'; import { TimeoutSettings } from './TimeoutSettings'; import { TimeoutError } from './Errors'; +import { Events } from './events'; +import { EventEmitter } from 'events'; const readFileAsync = helper.promisify(fs.readFile); @@ -50,12 +52,19 @@ export interface FrameDelegate { setFrameContent(frame: Frame, html: string, options?: NavigateOptions): Promise; } +interface Page extends EventEmitter { + _lifecycleWatchers: Set; + _timeoutSettings: TimeoutSettings; + _disconnectedPromise: Promise; +} + export type LifecycleEvent = 'load' | 'domcontentloaded'; export class Frame { readonly _delegate: FrameDelegate; readonly _firedLifecycleEvents: Set; - private _timeoutSettings: TimeoutSettings; + _lastDocumentId: string; + readonly _page: Page; private _parentFrame: Frame; private _url = ''; private _detached = false; @@ -63,10 +72,11 @@ export class Frame { private _childFrames = new Set(); private _name: string; - constructor(delegate: FrameDelegate, timeoutSettings: TimeoutSettings, parentFrame: Frame | null) { + constructor(delegate: FrameDelegate, page: Page, parentFrame: Frame | null) { this._delegate = delegate; this._firedLifecycleEvents = new Set(); - this._timeoutSettings = timeoutSettings; + this._lastDocumentId = ''; + this._page = page; this._parentFrame = parentFrame; this._worlds.set('main', { contextPromise: new Promise(() => {}), contextResolveCallback: () => {}, context: null, rerunnableTasks: new Set() }); @@ -390,7 +400,7 @@ export class Frame { } async waitForSelector(selector: string | types.Selector, options: types.TimeoutOptions = {}): Promise { - const { timeout = this._timeoutSettings.timeout() } = options; + const { timeout = this._page._timeoutSettings.timeout() } = options; const task = dom.waitForSelectorTask(types.clearSelector(selector), timeout); const handle = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${types.selectorToString(selector)}"`); if (!handle.asElement()) { @@ -410,7 +420,7 @@ export class Frame { } waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions = {}, ...args: any[]): Promise { - options = { timeout: this._timeoutSettings.timeout(), ...options }; + options = { timeout: this._page._timeoutSettings.timeout(), ...options }; const task = dom.waitForFunctionTask(pageFunction, options, ...args); return this._scheduleRerunnableTask(task, 'main', options.timeout); } @@ -420,12 +430,36 @@ export class Frame { return context.evaluate(() => document.title); } - _navigated(url: string, name: string) { - this._url = url; - this._name = name; + _onExpectedNewDocumentNavigation(documentId: string, url?: string) { + for (const watcher of this._page._lifecycleWatchers) + watcher._onExpectedNewDocumentNavigation(this, documentId, url); } - _detach() { + _onAbortedNewDocumentNavigation(documentId: string, errorText: string) { + for (const watcher of this._page._lifecycleWatchers) + watcher._onAbortedNewDocumentNavigation(this, documentId, errorText); + } + + _onCommittedNewDocumentNavigation(url: string, name: string, documentId: string) { + this._url = url; + this._name = name; + this._lastDocumentId = documentId; + this._firedLifecycleEvents.clear(); + } + + _onCommittedSameDocumentNavigation(url: string) { + this._url = url; + for (const watcher of this._page._lifecycleWatchers) + watcher._onNavigatedWithinDocument(this); + } + + _lifecycleEvent(event: LifecycleEvent) { + this._firedLifecycleEvents.add(event); + for (const watcher of this._page._lifecycleWatchers) + watcher._onLifecycleEvent(this); + } + + _onDetached() { this._detached = true; for (const world of this._worlds.values()) { for (const rerunnableTask of world.rerunnableTasks) @@ -434,6 +468,8 @@ export class Frame { if (this._parentFrame) this._parentFrame._childFrames.delete(this); this._parentFrame = null; + for (const watcher of this._page._lifecycleWatchers) + watcher._onFrameDetached(this); } private _scheduleRerunnableTask(task: dom.Task, worldType: WorldType, timeout?: number, title?: string): Promise { @@ -557,3 +593,127 @@ class RerunnableTask { this._world.rerunnableTasks.delete(this); } } + +export class LifecycleWatcher { + readonly sameDocumentNavigationPromise: Promise; + readonly lifecyclePromise: Promise; + readonly newDocumentNavigationPromise: Promise; + readonly timeoutOrTerminationPromise: Promise; + private _expectedLifecycle: LifecycleEvent[]; + private _frame: Frame; + private _navigationRequest: network.Request | null = null; + private _sameDocumentNavigationCompleteCallback: () => void; + private _lifecycleCallback: () => void; + private _newDocumentNavigationCompleteCallback: () => void; + private _frameDetachedCallback: (err: Error) => void; + private _navigationAbortedCallback: (err: Error) => void; + private _maximumTimer: NodeJS.Timer; + private _hasSameDocumentNavigation: boolean; + private _listeners: RegisteredListener[]; + private _targetUrl?: string; + private _expectedDocumentId?: string; + + constructor(frame: Frame, waitUntil: LifecycleEvent | LifecycleEvent[], timeout: number) { + if (Array.isArray(waitUntil)) + waitUntil = waitUntil.slice(); + else if (typeof waitUntil === 'string') + waitUntil = [waitUntil]; + if (waitUntil.some(e => e !== 'load' && e !== 'domcontentloaded')) + throw new Error('Unsupported waitUntil option'); + this._expectedLifecycle = waitUntil.slice(); + this._frame = frame; + this.sameDocumentNavigationPromise = new Promise(f => this._sameDocumentNavigationCompleteCallback = f); + this.lifecyclePromise = new Promise(f => this._lifecycleCallback = f); + this.newDocumentNavigationPromise = new Promise(f => this._newDocumentNavigationCompleteCallback = f); + this.timeoutOrTerminationPromise = Promise.race([ + this._createTimeoutPromise(timeout), + new Promise(f => this._frameDetachedCallback = f), + new Promise(f => this._navigationAbortedCallback = f), + this._frame._page._disconnectedPromise.then(() => new Error('Navigation failed because browser has disconnected!')), + ]); + frame._page._lifecycleWatchers.add(this); + this._listeners = [ + helper.addEventListener(this._frame._page, Events.Page.Request, (request: network.Request) => { + if (request.frame() === this._frame && request.isNavigationRequest()) + this._navigationRequest = request; + }), + ]; + this._checkLifecycleComplete(); + } + + _onFrameDetached(frame: Frame) { + if (this._frame === frame) { + this._frameDetachedCallback.call(null, new Error('Navigating frame was detached')); + return; + } + this._checkLifecycleComplete(); + } + + _onNavigatedWithinDocument(frame: Frame) { + if (frame !== this._frame) + return; + this._hasSameDocumentNavigation = true; + this._checkLifecycleComplete(); + } + + _onExpectedNewDocumentNavigation(frame: Frame, documentId: string, url?: string) { + if (frame === this._frame && this._expectedDocumentId === undefined) { + this._expectedDocumentId = documentId; + this._targetUrl = url; + } + } + + _onAbortedNewDocumentNavigation(frame: Frame, documentId: string, errorText: string) { + if (frame === this._frame && documentId === this._expectedDocumentId) { + if (this._targetUrl) + this._navigationAbortedCallback(new Error('Navigation to ' + this._targetUrl + ' failed: ' + errorText)); + else + this._navigationAbortedCallback(new Error('Navigation failed: ' + errorText)); + } + } + + _onLifecycleEvent(frame: Frame) { + this._checkLifecycleComplete(); + } + + navigationResponse(): network.Response | null { + return this._navigationRequest ? this._navigationRequest.response() : null; + } + + private _createTimeoutPromise(timeout: number): Promise { + if (!timeout) + return new Promise(() => {}); + const errorMessage = 'Navigation timeout of ' + timeout + ' ms exceeded'; + return new Promise(fulfill => this._maximumTimer = setTimeout(fulfill, timeout)) + .then(() => new TimeoutError(errorMessage)); + } + + private _checkLifecycleRecursively(frame: Frame, expectedLifecycle: LifecycleEvent[]): boolean { + for (const event of expectedLifecycle) { + if (!frame._firedLifecycleEvents.has(event)) + return false; + } + for (const child of frame.childFrames()) { + if (!this._checkLifecycleRecursively(child, expectedLifecycle)) + return false; + } + return true; + } + + private _checkLifecycleComplete() { + // We expect navigation to commit. + if (!this._checkLifecycleRecursively(this._frame, this._expectedLifecycle)) + return; + this._lifecycleCallback(); + if (this._hasSameDocumentNavigation) + this._sameDocumentNavigationCompleteCallback(); + if (this._frame._lastDocumentId === this._expectedDocumentId) + this._newDocumentNavigationCompleteCallback(); + } + + dispose() { + this._frame._page._lifecycleWatchers.delete(this); + helper.removeEventListeners(this._listeners); + clearTimeout(this._maximumTimer); + } +} diff --git a/src/page.ts b/src/page.ts index e7d4063c3e..2a9246ca68 100644 --- a/src/page.ts +++ b/src/page.ts @@ -85,6 +85,7 @@ export class Page extends EventEmitter { private _pageBindings = new Map(); readonly _screenshotter: Screenshotter; private _fileChooserInterceptors = new Set<(chooser: FileChooser) => void>(); + readonly _lifecycleWatchers = new Set(); constructor(delegate: PageDelegate, browserContext: BrowserContext) { super(); diff --git a/src/webkit/FrameManager.ts b/src/webkit/FrameManager.ts index 8c0fa1cc0a..64dea87cce 100644 --- a/src/webkit/FrameManager.ts +++ b/src/webkit/FrameManager.ts @@ -16,7 +16,6 @@ */ import * as EventEmitter from 'events'; -import { TimeoutError } from '../Errors'; import * as frames from '../frames'; import { assert, debugError, helper, RegisteredListener } from '../helper'; import * as js from '../javascript'; @@ -51,9 +50,10 @@ export const FrameManagerEvents = { const frameDataSymbol = Symbol('frameData'); type FrameData = { id: string, - loaderId: string, }; +let lastDocumentId = 0; + export class FrameManager extends EventEmitter implements frames.FrameDelegate, PageDelegate { readonly rawMouse: RawMouseImpl; readonly rawKeyboard: RawKeyboardImpl; @@ -132,7 +132,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, _addSessionListeners() { this._sessionListeners = [ - helper.addEventListener(this._session, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame)), + helper.addEventListener(this._session, 'Page.frameNavigated', event => this._onFrameNavigated(event.frame, false)), helper.addEventListener(this._session, 'Page.navigatedWithinDocument', event => this._onFrameNavigatedWithinDocument(event.frameId, event.url)), helper.addEventListener(this._session, 'Page.frameDetached', event => this._onFrameDetached(event.frameId)), helper.addEventListener(this._session, 'Page.frameStoppedLoading', event => this._onFrameStoppedLoading(event.frameId)), @@ -163,8 +163,8 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, return; const hasDOMContentLoaded = frame._firedLifecycleEvents.has('domcontentloaded'); const hasLoad = frame._firedLifecycleEvents.has('load'); - frame._firedLifecycleEvents.add('domcontentloaded'); - frame._firedLifecycleEvents.add('load'); + frame._lifecycleEvent('domcontentloaded'); + frame._lifecycleEvent('load'); this.emit(FrameManagerEvents.LifecycleEvent, frame); if (frame === this.mainFrame() && !hasDOMContentLoaded) this._page.emit(Events.Page.DOMContentLoaded); @@ -176,7 +176,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, const frame = this._frames.get(frameId); if (!frame) return; - frame._firedLifecycleEvents.add(event); + frame._lifecycleEvent(event); this.emit(FrameManagerEvents.LifecycleEvent, frame); if (frame === this.mainFrame()) { if (event === 'load') @@ -189,7 +189,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, _handleFrameTree(frameTree: Protocol.Page.FrameResourceTree) { if (frameTree.frame.parentId) this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId); - this._onFrameNavigated(frameTree.frame); + this._onFrameNavigated(frameTree.frame, true); if (!frameTree.childFrames) return; @@ -222,10 +222,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, return; assert(parentFrameId); const parentFrame = this._frames.get(parentFrameId); - const frame = new frames.Frame(this, this._page._timeoutSettings, parentFrame); + const frame = new frames.Frame(this, this._page, parentFrame); const data: FrameData = { id: frameId, - loaderId: '', }; frame[frameDataSymbol] = data; this._frames.set(frameId, frame); @@ -234,7 +233,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, return frame; } - _onFrameNavigated(framePayload: Protocol.Page.Frame) { + _onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) { const isMainFrame = !framePayload.parentId; let frame = isMainFrame ? this._mainFrame : this._frames.get(framePayload.id); @@ -251,10 +250,9 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, } } else if (isMainFrame) { // Initial frame navigation. - frame = new frames.Frame(this, this._page._timeoutSettings, null); + frame = new frames.Frame(this, this._page, null); const data: FrameData = { id: framePayload.id, - loaderId: framePayload.loaderId, }; frame[frameDataSymbol] = data; this._frames.set(framePayload.id, frame); @@ -266,12 +264,6 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, if (isMainFrame) this._mainFrame = frame; - // Update frame payload. - frame._navigated(framePayload.url, framePayload.name); - frame._firedLifecycleEvents.clear(); - const data = this._frameData(frame); - data.loaderId = framePayload.loaderId; - for (const context of this._contextIdToContext.values()) { if (context.frame() === frame) { const delegate = context._delegate as ExecutionContextDelegate; @@ -281,6 +273,12 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, } } + // Auto-increment to avoid cross-process loaderId clash. + const documentId = framePayload.loaderId + '::' + (++lastDocumentId); + if (!initial) + frame._onExpectedNewDocumentNavigation(documentId); + frame._onCommittedNewDocumentNavigation(framePayload.url, framePayload.name, documentId); + this.emit(FrameManagerEvents.FrameNavigated, frame); this._page.emit(Events.Page.FrameNavigated, frame); } @@ -289,7 +287,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, const frame = this._frames.get(frameId); if (!frame) return; - frame._navigated(url, frame.name()); + frame._onCommittedSameDocumentNavigation(url); this.emit(FrameManagerEvents.FrameNavigatedWithinDocument, frame); this.emit(FrameManagerEvents.FrameNavigated, frame); this._page.emit(Events.Page.FrameNavigated, frame); @@ -330,7 +328,7 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, _removeFramesRecursively(frame: frames.Frame) { for (const child of frame.childFrames()) this._removeFramesRecursively(child); - frame._detach(); + frame._onDetached(); this._frames.delete(this._frameData(frame).id); this.emit(FrameManagerEvents.FrameDetached, frame); this._page.emit(Events.Page.FrameDetached, frame); @@ -341,12 +339,12 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, timeout = this._page._timeoutSettings.navigationTimeout(), waitUntil = (['load'] as frames.LifecycleEvent[]) } = options; - const watchDog = new NextNavigationWatchdog(this, frame, waitUntil, timeout); + const watchDog = new frames.LifecycleWatcher(frame, waitUntil, timeout); await this._session.send('Page.navigate', {url, frameId: this._frameData(frame).id}); const error = await Promise.race([ - watchDog.timeoutOrTerminationPromise(), - watchDog.newDocumentNavigationPromise(), - watchDog.sameDocumentNavigationPromise(), + watchDog.timeoutOrTerminationPromise, + watchDog.newDocumentNavigationPromise, + watchDog.sameDocumentNavigationPromise, ]); watchDog.dispose(); if (error) @@ -359,11 +357,11 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, timeout = this._page._timeoutSettings.navigationTimeout(), waitUntil = (['load'] as frames.LifecycleEvent[]) } = options; - const watchDog = new NextNavigationWatchdog(this, frame, waitUntil, timeout); + const watchDog = new frames.LifecycleWatcher(frame, waitUntil, timeout); const error = await Promise.race([ - watchDog.timeoutOrTerminationPromise(), - watchDog.newDocumentNavigationPromise(), - watchDog.sameDocumentNavigationPromise(), + watchDog.timeoutOrTerminationPromise, + watchDog.newDocumentNavigationPromise, + watchDog.sameDocumentNavigationPromise, ]); watchDog.dispose(); if (error) @@ -377,15 +375,15 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, timeout = this._page._timeoutSettings.navigationTimeout(), waitUntil = (['load'] as frames.LifecycleEvent[]) } = options; - const watchDog = new NextNavigationWatchdog(this, frame, waitUntil, timeout); + 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.timeoutOrTerminationPromise, + watchDog.lifecyclePromise, ]); watchDog.dispose(); if (error) @@ -529,145 +527,3 @@ export class FrameManager extends EventEmitter implements frames.FrameDelegate, this._page.browser()._closePage(this._page); } } - -/** - * @internal - */ -class NextNavigationWatchdog { - private readonly _frameManager: FrameManager; - private readonly _frame: frames.Frame; - private readonly _newDocumentNavigationPromise: Promise; - private _newDocumentNavigationCallback: (value?: unknown) => void; - private readonly _sameDocumentNavigationPromise: Promise; - private _sameDocumentNavigationCallback: (value?: unknown) => void; - private readonly _lifecyclePromise: Promise; - private _lifecycleCallback: () => void; - private readonly _frameDetachPromise: Promise; - private _frameDetachCallback: (err: Error | null) => void; - private readonly _initialSession: TargetSession; - private _navigationRequest?: network.Request = null; - private readonly _eventListeners: RegisteredListener[]; - private readonly _timeoutPromise: Promise; - private readonly _timeoutId: NodeJS.Timer; - private _hasSameDocumentNavigation = false; - private readonly _expectedLifecycle: frames.LifecycleEvent[]; - private readonly _initialLoaderId: string; - - constructor(frameManager: FrameManager, frame: frames.Frame, waitUntil: frames.LifecycleEvent | frames.LifecycleEvent[], timeout) { - if (Array.isArray(waitUntil)) - waitUntil = waitUntil.slice(); - else if (typeof waitUntil === 'string') - waitUntil = [waitUntil]; - this._expectedLifecycle = waitUntil.slice(); - this._frameManager = frameManager; - this._frame = frame; - this._initialLoaderId = frameManager._frameData(frame).loaderId; - this._initialSession = frameManager._session; - this._newDocumentNavigationPromise = new Promise(fulfill => { - this._newDocumentNavigationCallback = fulfill; - }); - this._sameDocumentNavigationPromise = new Promise(fulfill => { - this._sameDocumentNavigationCallback = fulfill; - }); - this._lifecyclePromise = new Promise(fulfill => { - this._lifecycleCallback = fulfill; - }); - this._eventListeners = [ - helper.addEventListener(frameManager, FrameManagerEvents.LifecycleEvent, frame => this._onLifecycleEvent(frame)), - helper.addEventListener(frameManager, FrameManagerEvents.FrameNavigated, frame => this._onLifecycleEvent(frame)), - helper.addEventListener(frameManager, FrameManagerEvents.FrameNavigatedWithinDocument, frame => this._onSameDocumentNavigation(frame)), - helper.addEventListener(frameManager, FrameManagerEvents.FrameDetached, frame => this._onFrameDetached(frame)), - helper.addEventListener(frameManager.networkManager(), NetworkManagerEvents.Request, this._onRequest.bind(this)), - ]; - const timeoutError = new TimeoutError('Navigation Timeout Exceeded: ' + timeout + 'ms'); - let timeoutCallback; - this._timeoutPromise = new Promise(resolve => timeoutCallback = resolve.bind(null, timeoutError)); - this._timeoutId = timeout ? setTimeout(timeoutCallback, timeout) : null; - this._frameDetachPromise = new Promise(fulfill => { - this._frameDetachCallback = fulfill; - }); - } - - sameDocumentNavigationPromise(): Promise { - return this._sameDocumentNavigationPromise; - } - - newDocumentNavigationPromise(): Promise { - return this._newDocumentNavigationPromise; - } - - lifecyclePromise(): Promise { - return this._lifecyclePromise; - } - - timeoutOrTerminationPromise(): Promise { - return Promise.race([ - this._timeoutPromise, - this._frameDetachPromise, - this._frameManager._page._disconnectedPromise - ]); - } - - _onLifecycleEvent(frame: frames.Frame) { - this._checkLifecycle(); - } - - _onSameDocumentNavigation(frame) { - if (this._frame === frame) - this._hasSameDocumentNavigation = true; - this._checkLifecycle(); - } - - _checkLifecycle() { - const checkLifecycle = (frame: frames.Frame, expectedLifecycle: frames.LifecycleEvent[]): boolean => { - for (const event of expectedLifecycle) { - if (!frame._firedLifecycleEvents.has(event)) - return false; - } - for (const child of frame.childFrames()) { - if (!checkLifecycle(child, expectedLifecycle)) - return false; - } - return true; - }; - - if (this._frame.isDetached()) { - this._newDocumentNavigationCallback(new Error('Navigating frame was detached')); - this._sameDocumentNavigationCallback(new Error('Navigating frame was detached')); - return; - } - - if (!checkLifecycle(this._frame, this._expectedLifecycle)) - return; - this._lifecycleCallback(); - if (this._hasSameDocumentNavigation) - this._sameDocumentNavigationCallback(); - if (this._frameManager._frameData(this._frame).loaderId !== this._initialLoaderId || - this._initialSession !== this._frameManager._session) - this._newDocumentNavigationCallback(); - } - - _onFrameDetached(frame: frames.Frame) { - if (this._frame === frame) { - this._frameDetachCallback.call(null, new Error('Navigating frame was detached')); - return; - } - this._checkLifecycle(); - } - - _onRequest(request: network.Request) { - if (request.frame() !== this._frame || !request.isNavigationRequest()) - return; - this._navigationRequest = request; - } - - navigationResponse(): network.Response | null { - return this._navigationRequest ? this._navigationRequest.response() : null; - } - - dispose() { - // TODO: handle exceptions - helper.removeEventListeners(this._eventListeners); - clearTimeout(this._timeoutId); - } -} diff --git a/test/mouse.spec.js b/test/mouse.spec.js index 6ce77aef80..5945d19148 100644 --- a/test/mouse.spec.js +++ b/test/mouse.spec.js @@ -26,7 +26,7 @@ function dimensions() { }; } -module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT}) { +module.exports.addTests = function({testRunner, expect, FFOX, CHROME, WEBKIT, MAC}) { const {describe, xdescribe, fdescribe} = testRunner; const {it, fit, xit} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; diff --git a/test/navigation.spec.js b/test/navigation.spec.js index b37387e6fd..0429f8f0a4 100644 --- a/test/navigation.spec.js +++ b/test/navigation.spec.js @@ -141,7 +141,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME server.setRoute('/empty.html', (req, res) => { }); let error = null; await page.goto(server.PREFIX + '/empty.html', {timeout: 1}).catch(e => error = e); - const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + const message = 'Navigation timeout of 1 ms exceeded'; expect(error.message).toContain(message); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); @@ -151,7 +151,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME let error = null; page.setDefaultNavigationTimeout(1); await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); - const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + const message = 'Navigation timeout of 1 ms exceeded'; expect(error.message).toContain(message); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); @@ -161,7 +161,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME let error = null; page.setDefaultTimeout(1); await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); - const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + const message = 'Navigation timeout of 1 ms exceeded'; expect(error.message).toContain(message); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); @@ -172,7 +172,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME page.setDefaultTimeout(0); page.setDefaultNavigationTimeout(1); await page.goto(server.PREFIX + '/empty.html').catch(e => error = e); - const message = WEBKIT ? 'Navigation Timeout Exceeded: 1ms' : 'Navigation timeout of 1 ms exceeded'; + const message = 'Navigation timeout of 1 ms exceeded'; expect(error.message).toContain(message); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); diff --git a/test/page.spec.js b/test/page.spec.js index 9c1d09df35..99fcf792be 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -118,7 +118,7 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF }); }); - describe('Page.Events.Popup', function() { + describe.skip(WEBKIT)('Page.Events.Popup', function() { it('should work', async({page}) => { const [popup] = await Promise.all([ new Promise(x => page.once('popup', x)),