feature(navigation): implement networkilde0 and networkidle2 (#263)
This commit is contained in:
parent
6d404b0827
commit
f9f7d5c55a
|
|
@ -23,7 +23,7 @@ import * as js from '../javascript';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import { CDPSession } from './Connection';
|
import { CDPSession } from './Connection';
|
||||||
import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate } from './ExecutionContext';
|
import { EVALUATION_SCRIPT_URL, ExecutionContextDelegate } from './ExecutionContext';
|
||||||
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
|
import { NetworkManager } from './NetworkManager';
|
||||||
import { Page } from '../page';
|
import { Page } from '../page';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
|
|
@ -68,11 +68,6 @@ export class FrameManager extends EventEmitter implements PageDelegate {
|
||||||
(this._page as any).overrides = new Overrides(client);
|
(this._page as any).overrides = new Overrides(client);
|
||||||
(this._page as any).interception = new Interception(this._networkManager);
|
(this._page as any).interception = new Interception(this._networkManager);
|
||||||
|
|
||||||
this._networkManager.on(NetworkManagerEvents.Request, event => this._page.emit(Events.Page.Request, event));
|
|
||||||
this._networkManager.on(NetworkManagerEvents.Response, event => this._page.emit(Events.Page.Response, event));
|
|
||||||
this._networkManager.on(NetworkManagerEvents.RequestFailed, event => this._page.emit(Events.Page.RequestFailed, event));
|
|
||||||
this._networkManager.on(NetworkManagerEvents.RequestFinished, event => this._page.emit(Events.Page.RequestFinished, event));
|
|
||||||
|
|
||||||
this._client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
|
this._client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
|
||||||
this._client.on('Log.entryAdded', event => this._onLogEntryAdded(event));
|
this._client.on('Log.entryAdded', event => this._onLogEntryAdded(event));
|
||||||
this._client.on('Page.fileChooserOpened', event => this._onFileChooserOpened(event));
|
this._client.on('Page.fileChooserOpened', event => this._onFileChooserOpened(event));
|
||||||
|
|
@ -115,7 +110,6 @@ export class FrameManager extends EventEmitter implements PageDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}): Promise<network.Response | null> {
|
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}): Promise<network.Response | null> {
|
||||||
assertNoLegacyNavigationOptions(options);
|
|
||||||
const {
|
const {
|
||||||
referer = this._networkManager.extraHTTPHeaders()['referer'],
|
referer = this._networkManager.extraHTTPHeaders()['referer'],
|
||||||
waitUntil = (['load'] as frames.LifecycleEvent[]),
|
waitUntil = (['load'] as frames.LifecycleEvent[]),
|
||||||
|
|
@ -151,7 +145,6 @@ export class FrameManager extends EventEmitter implements PageDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}): Promise<network.Response | null> {
|
async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}): Promise<network.Response | null> {
|
||||||
assertNoLegacyNavigationOptions(options);
|
|
||||||
const {
|
const {
|
||||||
waitUntil = (['load'] as frames.LifecycleEvent[]),
|
waitUntil = (['load'] as frames.LifecycleEvent[]),
|
||||||
timeout = this._page._timeoutSettings.navigationTimeout(),
|
timeout = this._page._timeoutSettings.navigationTimeout(),
|
||||||
|
|
@ -531,12 +524,6 @@ export class FrameManager extends EventEmitter implements PageDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertNoLegacyNavigationOptions(options: frames.NavigateOptions) {
|
|
||||||
assert((options as any)['networkIdleTimeout'] === undefined, 'ERROR: networkIdleTimeout option is no longer supported.');
|
|
||||||
assert((options as any)['networkIdleInflight'] === undefined, 'ERROR: networkIdleInflight option is no longer supported.');
|
|
||||||
assert((options as any).waitUntil !== 'networkidle', 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead');
|
|
||||||
}
|
|
||||||
|
|
||||||
function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {
|
function toRemoteObject(handle: dom.ElementHandle): Protocol.Runtime.RemoteObject {
|
||||||
return handle._remoteObject as Protocol.Runtime.RemoteObject;
|
return handle._remoteObject as Protocol.Runtime.RemoteObject;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { CDPSession } from './Connection';
|
import { CDPSession } from './Connection';
|
||||||
import { Page } from '../page';
|
import { Page } from '../page';
|
||||||
import { assert, debugError, helper } from '../helper';
|
import { assert, debugError, helper } from '../helper';
|
||||||
|
|
@ -23,14 +22,7 @@ import { Protocol } from './protocol';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
|
|
||||||
export const NetworkManagerEvents = {
|
export class NetworkManager {
|
||||||
Request: Symbol('Events.NetworkManager.Request'),
|
|
||||||
Response: Symbol('Events.NetworkManager.Response'),
|
|
||||||
RequestFailed: Symbol('Events.NetworkManager.RequestFailed'),
|
|
||||||
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export class NetworkManager extends EventEmitter {
|
|
||||||
private _client: CDPSession;
|
private _client: CDPSession;
|
||||||
private _ignoreHTTPSErrors: boolean;
|
private _ignoreHTTPSErrors: boolean;
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
|
|
@ -46,7 +38,6 @@ export class NetworkManager extends EventEmitter {
|
||||||
private _requestIdToInterceptionId = new Map<string, string>();
|
private _requestIdToInterceptionId = new Map<string, string>();
|
||||||
|
|
||||||
constructor(client: CDPSession, ignoreHTTPSErrors: boolean, page: Page) {
|
constructor(client: CDPSession, ignoreHTTPSErrors: boolean, page: Page) {
|
||||||
super();
|
|
||||||
this._client = client;
|
this._client = client;
|
||||||
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
this._ignoreHTTPSErrors = ignoreHTTPSErrors;
|
||||||
this._page = page;
|
this._page = page;
|
||||||
|
|
@ -203,7 +194,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
const documentId = isNavigationRequest ? event.loaderId : undefined;
|
const documentId = isNavigationRequest ? event.loaderId : undefined;
|
||||||
const request = new InterceptableRequest(this._client, frame, interceptionId, documentId, this._userRequestInterceptionEnabled, event, redirectChain);
|
const request = new InterceptableRequest(this._client, frame, interceptionId, documentId, this._userRequestInterceptionEnabled, event, redirectChain);
|
||||||
this._requestIdToRequest.set(event.requestId, request);
|
this._requestIdToRequest.set(event.requestId, request);
|
||||||
this.emit(NetworkManagerEvents.Request, request.request);
|
this._page._frameManager.requestStarted(request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response {
|
_createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response {
|
||||||
|
|
@ -221,8 +212,8 @@ export class NetworkManager extends EventEmitter {
|
||||||
response._requestFinished(new Error('Response body is unavailable for redirect responses'));
|
response._requestFinished(new Error('Response body is unavailable for redirect responses'));
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this.emit(NetworkManagerEvents.Response, response);
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
this.emit(NetworkManagerEvents.RequestFinished, request.request);
|
this._page._frameManager.requestFinished(request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
||||||
|
|
@ -231,7 +222,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
const response = this._createResponse(request, event.response);
|
const response = this._createResponse(request, event.response);
|
||||||
this.emit(NetworkManagerEvents.Response, response);
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
|
_onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
|
||||||
|
|
@ -247,7 +238,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
request.request.response()._requestFinished();
|
request.request.response()._requestFinished();
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this.emit(NetworkManagerEvents.RequestFinished, request.request);
|
this._page._frameManager.requestFinished(request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
||||||
|
|
@ -261,8 +252,8 @@ export class NetworkManager extends EventEmitter {
|
||||||
response._requestFinished();
|
response._requestFinished();
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
request.request._setFailureText(event.errorText, event.canceled);
|
request.request._setFailureText(event.errorText);
|
||||||
this.emit(NetworkManagerEvents.RequestFailed, request.request);
|
this._page._frameManager.requestFailed(request.request, event.canceled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import * as dom from '../dom';
|
||||||
import { JugglerSession } from './Connection';
|
import { JugglerSession } from './Connection';
|
||||||
import { ExecutionContextDelegate } from './ExecutionContext';
|
import { ExecutionContextDelegate } from './ExecutionContext';
|
||||||
import { Page, PageDelegate } from '../page';
|
import { Page, PageDelegate } from '../page';
|
||||||
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
|
import { NetworkManager } from './NetworkManager';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
import * as dialog from '../dialog';
|
import * as dialog from '../dialog';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
|
|
@ -67,10 +67,6 @@ export class FrameManager extends EventEmitter implements PageDelegate {
|
||||||
helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)),
|
helper.addEventListener(this._session, 'Page.dialogOpened', this._onDialogOpened.bind(this)),
|
||||||
helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
|
helper.addEventListener(this._session, 'Page.bindingCalled', this._onBindingCalled.bind(this)),
|
||||||
helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)),
|
helper.addEventListener(this._session, 'Page.fileChooserOpened', this._onFileChooserOpened.bind(this)),
|
||||||
helper.addEventListener(this._networkManager, NetworkManagerEvents.Request, request => this._page.emit(Events.Page.Request, request)),
|
|
||||||
helper.addEventListener(this._networkManager, NetworkManagerEvents.Response, response => this._page.emit(Events.Page.Response, response)),
|
|
||||||
helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFinished, request => this._page.emit(Events.Page.RequestFinished, request)),
|
|
||||||
helper.addEventListener(this._networkManager, NetworkManagerEvents.RequestFailed, request => this._page.emit(Events.Page.RequestFailed, request)),
|
|
||||||
];
|
];
|
||||||
(this._page as any).interception = new Interception(this._networkManager);
|
(this._page as any).interception = new Interception(this._networkManager);
|
||||||
(this._page as any).accessibility = new Accessibility(session);
|
(this._page as any).accessibility = new Accessibility(session);
|
||||||
|
|
|
||||||
|
|
@ -15,28 +15,19 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
import { assert, debugError, helper, RegisteredListener } from '../helper';
|
||||||
import { JugglerSession } from './Connection';
|
import { JugglerSession } from './Connection';
|
||||||
import { Page } from '../page';
|
import { Page } from '../page';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
|
|
||||||
export const NetworkManagerEvents = {
|
export class NetworkManager {
|
||||||
RequestFailed: Symbol('NetworkManagerEvents.RequestFailed'),
|
|
||||||
RequestFinished: Symbol('NetworkManagerEvents.RequestFinished'),
|
|
||||||
Request: Symbol('NetworkManagerEvents.Request'),
|
|
||||||
Response: Symbol('NetworkManagerEvents.Response'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export class NetworkManager extends EventEmitter {
|
|
||||||
private _session: JugglerSession;
|
private _session: JugglerSession;
|
||||||
private _requests: Map<string, InterceptableRequest>;
|
private _requests: Map<string, InterceptableRequest>;
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
private _eventListeners: RegisteredListener[];
|
private _eventListeners: RegisteredListener[];
|
||||||
|
|
||||||
constructor(session: JugglerSession, page: Page) {
|
constructor(session: JugglerSession, page: Page) {
|
||||||
super();
|
|
||||||
this._session = session;
|
this._session = session;
|
||||||
|
|
||||||
this._requests = new Map();
|
this._requests = new Map();
|
||||||
|
|
@ -80,7 +71,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
const request = new InterceptableRequest(this._session, frame, redirectChain, event);
|
const request = new InterceptableRequest(this._session, frame, redirectChain, event);
|
||||||
this._requests.set(request._id, request);
|
this._requests.set(request._id, request);
|
||||||
this.emit(NetworkManagerEvents.Request, request.request);
|
this._page._frameManager.requestStarted(request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onResponseReceived(event) {
|
_onResponseReceived(event) {
|
||||||
|
|
@ -100,7 +91,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
for (const {name, value} of event.headers)
|
for (const {name, value} of event.headers)
|
||||||
headers[name.toLowerCase()] = value;
|
headers[name.toLowerCase()] = value;
|
||||||
const response = new network.Response(request.request, event.status, event.statusText, headers, remoteAddress, getResponseBody);
|
const response = new network.Response(request.request, event.status, event.statusText, headers, remoteAddress, getResponseBody);
|
||||||
this.emit(NetworkManagerEvents.Response, response);
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequestFinished(event) {
|
_onRequestFinished(event) {
|
||||||
|
|
@ -116,7 +107,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
this._requests.delete(request._id);
|
this._requests.delete(request._id);
|
||||||
response._requestFinished();
|
response._requestFinished();
|
||||||
}
|
}
|
||||||
this.emit(NetworkManagerEvents.RequestFinished, request.request);
|
this._page._frameManager.requestFinished(request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequestFailed(event) {
|
_onRequestFailed(event) {
|
||||||
|
|
@ -126,8 +117,8 @@ export class NetworkManager extends EventEmitter {
|
||||||
this._requests.delete(request._id);
|
this._requests.delete(request._id);
|
||||||
if (request.request.response())
|
if (request.request.response())
|
||||||
request.request.response()._requestFinished();
|
request.request.response()._requestFinished();
|
||||||
request.request._setFailureText(event.errorCode, event.errorCode === 'NS_BINDING_ABORTED');
|
request.request._setFailureText(event.errorCode);
|
||||||
this.emit(NetworkManagerEvents.RequestFailed, request.request);
|
this._page._frameManager.requestFailed(request.request, event.errorCode === 'NS_BINDING_ABORTED');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ export type GotoOptions = NavigateOptions & {
|
||||||
referer?: string,
|
referer?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LifecycleEvent = 'load' | 'domcontentloaded';
|
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
|
||||||
|
const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']);
|
||||||
|
|
||||||
export type WaitForOptions = types.TimeoutOptions & { waitFor?: boolean };
|
export type WaitForOptions = types.TimeoutOptions & { waitFor?: boolean };
|
||||||
|
|
||||||
|
|
@ -114,6 +115,12 @@ export class FrameManager {
|
||||||
for (const watcher of this._lifecycleWatchers)
|
for (const watcher of this._lifecycleWatchers)
|
||||||
watcher._onCommittedNewDocumentNavigation(frame);
|
watcher._onCommittedNewDocumentNavigation(frame);
|
||||||
}
|
}
|
||||||
|
this._stopNetworkIdleTimer(frame, 'networkidle0');
|
||||||
|
if (frame._inflightRequests === 0)
|
||||||
|
this._startNetworkIdleTimer(frame, 'networkidle0');
|
||||||
|
this._stopNetworkIdleTimer(frame, 'networkidle2');
|
||||||
|
if (frame._inflightRequests <= 2)
|
||||||
|
this._startNetworkIdleTimer(frame, 'networkidle2');
|
||||||
this._page.emit(Events.Page.FrameNavigated, frame);
|
this._page.emit(Events.Page.FrameNavigated, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,6 +173,42 @@ export class FrameManager {
|
||||||
this._page.emit(Events.Page.DOMContentLoaded);
|
this._page.emit(Events.Page.DOMContentLoaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestStarted(request: network.Request) {
|
||||||
|
if (request.frame())
|
||||||
|
this._incrementRequestCount(request.frame());
|
||||||
|
if (request._documentId && request.frame() && !request.redirectChain().length) {
|
||||||
|
for (const watcher of this._lifecycleWatchers)
|
||||||
|
watcher._onNavigationRequest(request.frame(), request);
|
||||||
|
}
|
||||||
|
this._page.emit(Events.Page.Request, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestReceivedResponse(response: network.Response) {
|
||||||
|
this._page.emit(Events.Page.Response, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestFinished(request: network.Request) {
|
||||||
|
if (request.frame())
|
||||||
|
this._decrementRequestCount(request.frame());
|
||||||
|
this._page.emit(Events.Page.RequestFinished, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestFailed(request: network.Request, canceled: boolean) {
|
||||||
|
if (request.frame())
|
||||||
|
this._decrementRequestCount(request.frame());
|
||||||
|
if (request._documentId && request.frame()) {
|
||||||
|
const isCurrentDocument = request.frame()._lastDocumentId === request._documentId;
|
||||||
|
if (!isCurrentDocument) {
|
||||||
|
let errorText = request.failure().errorText;
|
||||||
|
if (canceled)
|
||||||
|
errorText += '; maybe frame was detached?';
|
||||||
|
for (const watcher of this._lifecycleWatchers)
|
||||||
|
watcher._onAbortedNewDocumentNavigation(request.frame(), request._documentId, errorText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._page.emit(Events.Page.RequestFailed, request);
|
||||||
|
}
|
||||||
|
|
||||||
private _removeFramesRecursively(frame: Frame) {
|
private _removeFramesRecursively(frame: Frame) {
|
||||||
for (const child of frame.childFrames())
|
for (const child of frame.childFrames())
|
||||||
this._removeFramesRecursively(child);
|
this._removeFramesRecursively(child);
|
||||||
|
|
@ -175,6 +218,36 @@ export class FrameManager {
|
||||||
watcher._onFrameDetached(frame);
|
watcher._onFrameDetached(frame);
|
||||||
this._page.emit(Events.Page.FrameDetached, frame);
|
this._page.emit(Events.Page.FrameDetached, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _decrementRequestCount(frame: Frame) {
|
||||||
|
frame._inflightRequests--;
|
||||||
|
if (frame._inflightRequests === 0)
|
||||||
|
this._startNetworkIdleTimer(frame, 'networkidle0');
|
||||||
|
if (frame._inflightRequests === 2)
|
||||||
|
this._startNetworkIdleTimer(frame, 'networkidle2');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _incrementRequestCount(frame: Frame) {
|
||||||
|
frame._inflightRequests++;
|
||||||
|
if (frame._inflightRequests === 1)
|
||||||
|
this._stopNetworkIdleTimer(frame, 'networkidle0');
|
||||||
|
if (frame._inflightRequests === 3)
|
||||||
|
this._stopNetworkIdleTimer(frame, 'networkidle2');
|
||||||
|
}
|
||||||
|
|
||||||
|
private _startNetworkIdleTimer(frame: Frame, event: LifecycleEvent) {
|
||||||
|
assert(!frame._networkIdleTimers.has(event));
|
||||||
|
if (frame._firedLifecycleEvents.has(event))
|
||||||
|
return;
|
||||||
|
frame._networkIdleTimers.set(event, setTimeout(() => {
|
||||||
|
this.frameLifecycleEvent(frame._id, event);
|
||||||
|
}, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopNetworkIdleTimer(frame: Frame, event: LifecycleEvent) {
|
||||||
|
clearTimeout(frame._networkIdleTimers.get(event));
|
||||||
|
frame._networkIdleTimers.delete(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Frame {
|
export class Frame {
|
||||||
|
|
@ -188,6 +261,8 @@ export class Frame {
|
||||||
private _contextData = new Map<ContextType, ContextData>();
|
private _contextData = new Map<ContextType, ContextData>();
|
||||||
private _childFrames = new Set<Frame>();
|
private _childFrames = new Set<Frame>();
|
||||||
_name: string;
|
_name: string;
|
||||||
|
_inflightRequests = 0;
|
||||||
|
readonly _networkIdleTimers = new Map<LifecycleEvent, NodeJS.Timer>();
|
||||||
|
|
||||||
constructor(page: Page, id: string, parentFrame: Frame | null) {
|
constructor(page: Page, id: string, parentFrame: Frame | null) {
|
||||||
this._id = id;
|
this._id = id;
|
||||||
|
|
@ -713,7 +788,7 @@ export class LifecycleWatcher {
|
||||||
waitUntil = waitUntil.slice();
|
waitUntil = waitUntil.slice();
|
||||||
else if (typeof waitUntil === 'string')
|
else if (typeof waitUntil === 'string')
|
||||||
waitUntil = [waitUntil];
|
waitUntil = [waitUntil];
|
||||||
if (waitUntil.some(e => e !== 'load' && e !== 'domcontentloaded'))
|
if (waitUntil.some(e => !kLifecycleEvents.has(e)))
|
||||||
throw new Error('Unsupported waitUntil option');
|
throw new Error('Unsupported waitUntil option');
|
||||||
this._expectedLifecycle = waitUntil.slice();
|
this._expectedLifecycle = waitUntil.slice();
|
||||||
this._frame = frame;
|
this._frame = frame;
|
||||||
|
|
|
||||||
|
|
@ -101,24 +101,10 @@ export class Request {
|
||||||
this._headers = headers;
|
this._headers = headers;
|
||||||
this._waitForResponsePromise = new Promise(f => this._waitForResponsePromiseCallback = f);
|
this._waitForResponsePromise = new Promise(f => this._waitForResponsePromiseCallback = f);
|
||||||
this._waitForFinishedPromise = new Promise(f => this._waitForFinishedPromiseCallback = f);
|
this._waitForFinishedPromise = new Promise(f => this._waitForFinishedPromiseCallback = f);
|
||||||
if (documentId && frame) {
|
|
||||||
for (const watcher of frame._page._frameManager._lifecycleWatchers)
|
|
||||||
watcher._onNavigationRequest(frame, this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_setFailureText(failureText: string, canceled: boolean) {
|
_setFailureText(failureText: string) {
|
||||||
this._failureText = failureText;
|
this._failureText = failureText;
|
||||||
if (this._documentId && this._frame) {
|
|
||||||
const isCurrentDocument = this._frame._lastDocumentId === this._documentId;
|
|
||||||
if (!isCurrentDocument) {
|
|
||||||
let errorText = failureText;
|
|
||||||
if (canceled)
|
|
||||||
errorText += '; maybe frame was detached?';
|
|
||||||
for (const watcher of this._frame._page._frameManager._lifecycleWatchers)
|
|
||||||
watcher._onAbortedNewDocumentNavigation(this._frame, this._documentId, errorText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._waitForFinishedPromiseCallback();
|
this._waitForFinishedPromiseCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import * as network from '../network';
|
||||||
import { TargetSession } from './Connection';
|
import { TargetSession } from './Connection';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
import { ExecutionContextDelegate, EVALUATION_SCRIPT_URL } from './ExecutionContext';
|
import { ExecutionContextDelegate, EVALUATION_SCRIPT_URL } from './ExecutionContext';
|
||||||
import { NetworkManager, NetworkManagerEvents } from './NetworkManager';
|
import { NetworkManager } from './NetworkManager';
|
||||||
import { Page, PageDelegate } from '../page';
|
import { Page, PageDelegate } from '../page';
|
||||||
import { Protocol } from './protocol';
|
import { Protocol } from './protocol';
|
||||||
import * as dialog from '../dialog';
|
import * as dialog from '../dialog';
|
||||||
|
|
@ -58,10 +58,6 @@ export class FrameManager extends EventEmitter implements PageDelegate {
|
||||||
this._isolatedWorlds = new Set();
|
this._isolatedWorlds = new Set();
|
||||||
this._page = new Page(this, browserContext);
|
this._page = new Page(this, browserContext);
|
||||||
this._networkManager = new NetworkManager(this._page);
|
this._networkManager = new NetworkManager(this._page);
|
||||||
this._networkManager.on(NetworkManagerEvents.Request, event => this._page.emit(Events.Page.Request, event));
|
|
||||||
this._networkManager.on(NetworkManagerEvents.Response, event => this._page.emit(Events.Page.Response, event));
|
|
||||||
this._networkManager.on(NetworkManagerEvents.RequestFailed, event => this._page.emit(Events.Page.RequestFailed, event));
|
|
||||||
this._networkManager.on(NetworkManagerEvents.RequestFinished, event => this._page.emit(Events.Page.RequestFinished, event));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSession(session: TargetSession) {
|
setSession(session: TargetSession) {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { TargetSession } from './Connection';
|
import { TargetSession } from './Connection';
|
||||||
import { Page } from '../page';
|
import { Page } from '../page';
|
||||||
import { assert, helper, RegisteredListener } from '../helper';
|
import { assert, helper, RegisteredListener } from '../helper';
|
||||||
|
|
@ -23,14 +22,7 @@ import { Protocol } from './protocol';
|
||||||
import * as network from '../network';
|
import * as network from '../network';
|
||||||
import * as frames from '../frames';
|
import * as frames from '../frames';
|
||||||
|
|
||||||
export const NetworkManagerEvents = {
|
export class NetworkManager {
|
||||||
Request: Symbol('Events.NetworkManager.Request'),
|
|
||||||
Response: Symbol('Events.NetworkManager.Response'),
|
|
||||||
RequestFailed: Symbol('Events.NetworkManager.RequestFailed'),
|
|
||||||
RequestFinished: Symbol('Events.NetworkManager.RequestFinished'),
|
|
||||||
};
|
|
||||||
|
|
||||||
export class NetworkManager extends EventEmitter {
|
|
||||||
private _session: TargetSession;
|
private _session: TargetSession;
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
private _requestIdToRequest = new Map<string, InterceptableRequest>();
|
private _requestIdToRequest = new Map<string, InterceptableRequest>();
|
||||||
|
|
@ -40,7 +32,6 @@ export class NetworkManager extends EventEmitter {
|
||||||
private _sessionListeners: RegisteredListener[] = [];
|
private _sessionListeners: RegisteredListener[] = [];
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
super();
|
|
||||||
this._page = page;
|
this._page = page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,7 +94,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
const documentId = isNavigationRequest ? this._session._sessionId + '::' + event.loaderId : undefined;
|
const documentId = isNavigationRequest ? this._session._sessionId + '::' + event.loaderId : undefined;
|
||||||
const request = new InterceptableRequest(frame, undefined, event, redirectChain, documentId);
|
const request = new InterceptableRequest(frame, undefined, event, redirectChain, documentId);
|
||||||
this._requestIdToRequest.set(event.requestId, request);
|
this._requestIdToRequest.set(event.requestId, request);
|
||||||
this.emit(NetworkManagerEvents.Request, request.request);
|
this._page._frameManager.requestStarted(request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response {
|
_createResponse(request: InterceptableRequest, responsePayload: Protocol.Network.Response): network.Response {
|
||||||
|
|
@ -121,8 +112,8 @@ export class NetworkManager extends EventEmitter {
|
||||||
response._requestFinished(new Error('Response body is unavailable for redirect responses'));
|
response._requestFinished(new Error('Response body is unavailable for redirect responses'));
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this.emit(NetworkManagerEvents.Response, response);
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
this.emit(NetworkManagerEvents.RequestFinished, request.request);
|
this._page._frameManager.requestFinished(request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
_onResponseReceived(event: Protocol.Network.responseReceivedPayload) {
|
||||||
|
|
@ -131,7 +122,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
if (!request)
|
if (!request)
|
||||||
return;
|
return;
|
||||||
const response = this._createResponse(request, event.response);
|
const response = this._createResponse(request, event.response);
|
||||||
this.emit(NetworkManagerEvents.Response, response);
|
this._page._frameManager.requestReceivedResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
|
_onLoadingFinished(event: Protocol.Network.loadingFinishedPayload) {
|
||||||
|
|
@ -147,7 +138,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
request.request.response()._requestFinished();
|
request.request.response()._requestFinished();
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
this.emit(NetworkManagerEvents.RequestFinished, request.request);
|
this._page._frameManager.requestFinished(request.request);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
_onLoadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
||||||
|
|
@ -161,8 +152,8 @@ export class NetworkManager extends EventEmitter {
|
||||||
response._requestFinished();
|
response._requestFinished();
|
||||||
this._requestIdToRequest.delete(request._requestId);
|
this._requestIdToRequest.delete(request._requestId);
|
||||||
this._attemptedAuthentications.delete(request._interceptionId);
|
this._attemptedAuthentications.delete(request._interceptionId);
|
||||||
request.request._setFailureText(event.errorText, event.errorText.includes('cancelled'));
|
request.request._setFailureText(event.errorText);
|
||||||
this.emit(NetworkManagerEvents.RequestFailed, request.request);
|
this._page._frameManager.requestFailed(request.request, event.errorText.includes('cancelled'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const utils = require('./utils');
|
const utils = require('./utils');
|
||||||
|
const { performance } = require('perf_hooks');
|
||||||
|
|
||||||
module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) {
|
module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME, WEBKIT}) {
|
||||||
const {describe, xdescribe, fdescribe} = testRunner;
|
const {describe, xdescribe, fdescribe} = testRunner;
|
||||||
|
|
@ -132,11 +133,11 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
|
||||||
const response = await page.goto(server.PREFIX + '/grid.html');
|
const response = await page.goto(server.PREFIX + '/grid.html');
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
});
|
});
|
||||||
xit('should navigate to empty page with networkidle0', async({page, server}) => {
|
it('should navigate to empty page with networkidle0', async({page, server}) => {
|
||||||
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle0'});
|
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle0'});
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
});
|
});
|
||||||
xit('should navigate to empty page with networkidle2', async({page, server}) => {
|
it('should navigate to empty page with networkidle2', async({page, server}) => {
|
||||||
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle2'});
|
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle2'});
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
@ -166,10 +167,10 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
|
||||||
await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(e => error = e);
|
await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(e => error = e);
|
||||||
expectSSLError(error.message);
|
expectSSLError(error.message);
|
||||||
});
|
});
|
||||||
xit('should throw if networkidle is passed as an option', async({page, server}) => {
|
it('should throw if networkidle is passed as an option', async({page, server}) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle'}).catch(err => error = err);
|
await page.goto(server.EMPTY_PAGE, {waitUntil: 'networkidle'}).catch(err => error = err);
|
||||||
expect(error.message).toContain('"networkidle" option is no longer supported');
|
expect(error.message).toContain('Unsupported waitUntil option');
|
||||||
});
|
});
|
||||||
it('should fail when main resources failed to load', async({page, server}) => {
|
it('should fail when main resources failed to load', async({page, server}) => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
@ -250,7 +251,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
|
||||||
expect(response.ok()).toBe(true);
|
expect(response.ok()).toBe(true);
|
||||||
expect(response.url()).toBe(server.EMPTY_PAGE);
|
expect(response.url()).toBe(server.EMPTY_PAGE);
|
||||||
});
|
});
|
||||||
xit('should wait for network idle to succeed navigation', async({page, server}) => {
|
it('should wait for network idle to succeed navigation', async({page, server}) => {
|
||||||
let responses = [];
|
let responses = [];
|
||||||
// Hold on to a bunch of requests without answering.
|
// Hold on to a bunch of requests without answering.
|
||||||
server.setRoute('/fetch-request-a.js', (req, res) => responses.push(res));
|
server.setRoute('/fetch-request-a.js', (req, res) => responses.push(res));
|
||||||
|
|
@ -303,10 +304,54 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
|
||||||
response.end(`File not found`);
|
response.end(`File not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
const response = await navigationPromise;
|
const response = await navigationPromise;
|
||||||
|
expect(performance.now() - now).not.toBeLessThan(499);
|
||||||
// Expect navigation to succeed.
|
// Expect navigation to succeed.
|
||||||
expect(response.ok()).toBe(true);
|
expect(response.ok()).toBe(true);
|
||||||
});
|
});
|
||||||
|
it('should wait for networkidle2 to succeed navigation', async({page, server}) => {
|
||||||
|
let responses = [];
|
||||||
|
// Hold on to a bunch of requests without answering.
|
||||||
|
server.setRoute('/fetch-request-a.js', (req, res) => responses.push(res));
|
||||||
|
server.setRoute('/fetch-request-b.js', (req, res) => responses.push(res));
|
||||||
|
server.setRoute('/fetch-request-c.js', (req, res) => responses.push(res));
|
||||||
|
server.setRoute('/fetch-request-d.js', (req, res) => responses.push(res));
|
||||||
|
const initialFetchResourcesRequested = Promise.all([
|
||||||
|
server.waitForRequest('/fetch-request-a.js'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Navigate to a page which loads immediately and then does a bunch of
|
||||||
|
// requests via javascript's fetch method.
|
||||||
|
const navigationPromise = page.goto(server.PREFIX + '/networkidle.html', {
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
});
|
||||||
|
// Track when the navigation gets completed.
|
||||||
|
let navigationFinished = false;
|
||||||
|
navigationPromise.then(() => navigationFinished = true);
|
||||||
|
|
||||||
|
// Wait for the page's 'load' event.
|
||||||
|
await new Promise(fulfill => page.once('load', fulfill));
|
||||||
|
expect(navigationFinished).toBe(false);
|
||||||
|
|
||||||
|
// Wait for the initial three resources to be requested.
|
||||||
|
await initialFetchResourcesRequested;
|
||||||
|
|
||||||
|
// Expect navigation still to be not finished.
|
||||||
|
expect(navigationFinished).toBe(false);
|
||||||
|
|
||||||
|
// Respond to initial requests.
|
||||||
|
for (const response of responses) {
|
||||||
|
response.statusCode = 404;
|
||||||
|
response.end(`File not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
|
const response = await navigationPromise;
|
||||||
|
expect(performance.now() - now).not.toBeLessThan(499);
|
||||||
|
// Expect navigation to succeed with two outstanding network requests.
|
||||||
|
expect(response.ok()).toBe(true);
|
||||||
|
});
|
||||||
it('should not leak listeners during navigation', async({page, server}) => {
|
it('should not leak listeners during navigation', async({page, server}) => {
|
||||||
let warning = null;
|
let warning = null;
|
||||||
const warningHandler = w => warning = w;
|
const warningHandler = w => warning = w;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue