chore: introduce DocumentInfo (#2765)

It encapsulates documentId and request.
This commit is contained in:
Dmitry Gozman 2020-07-06 15:58:27 -07:00 committed by GitHub
parent 15ddb5d32b
commit db3439d411
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 70 additions and 74 deletions

View File

@ -220,8 +220,6 @@ export class CRNetworkManager {
} }
const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document'; const isNavigationRequest = requestWillBeSentEvent.requestId === requestWillBeSentEvent.loaderId && requestWillBeSentEvent.type === 'Document';
const documentId = isNavigationRequest ? requestWillBeSentEvent.loaderId : undefined; const documentId = isNavigationRequest ? requestWillBeSentEvent.loaderId : undefined;
if (isNavigationRequest)
this._page._frameManager.frameUpdatedDocumentIdForNavigation(requestWillBeSentEvent.frameId!, documentId!);
const request = new InterceptableRequest({ const request = new InterceptableRequest({
client: this._client, client: this._client,
frame, frame,

View File

@ -498,7 +498,7 @@ class FrameSession {
_onFrameRequestedNavigation(payload: Protocol.Page.frameRequestedNavigationPayload) { _onFrameRequestedNavigation(payload: Protocol.Page.frameRequestedNavigationPayload) {
if (payload.disposition === 'currentTab') if (payload.disposition === 'currentTab')
this._page._frameManager.frameRequestedNavigation(payload.frameId, ''); this._page._frameManager.frameRequestedNavigation(payload.frameId);
} }
_onFrameNavigatedWithinDocument(frameId: string, url: string) { _onFrameNavigatedWithinDocument(frameId: string, url: string) {

View File

@ -140,9 +140,7 @@ export class FFPage implements PageDelegate {
} }
_onNavigationAborted(params: Protocol.Page.navigationAbortedPayload) { _onNavigationAborted(params: Protocol.Page.navigationAbortedPayload) {
const frame = this._page._frameManager.frame(params.frameId)!; this._page._frameManager.frameAbortedNavigation(params.frameId, params.errorText, params.navigationId);
for (const task of frame._frameTasks)
task.onNewDocument(params.navigationId, new Error(params.errorText));
} }
_onNavigationCommitted(params: Protocol.Page.navigationCommittedPayload) { _onNavigationCommitted(params: Protocol.Page.navigationCommittedPayload) {

View File

@ -36,6 +36,13 @@ type ContextData = {
rerunnableTasks: Set<RerunnableTask>; rerunnableTasks: Set<RerunnableTask>;
}; };
type DocumentInfo = {
// Unfortunately, we don't have documentId when we find out about
// a pending navigation from things like frameScheduledNavigaiton.
documentId: string | undefined,
request: network.Request | undefined,
};
export type GotoResult = { export type GotoResult = {
newDocumentId?: string, newDocumentId?: string,
}; };
@ -125,20 +132,17 @@ export class FrameManager {
barrier.release(); barrier.release();
} }
frameRequestedNavigation(frameId: string, documentId: string) { frameRequestedNavigation(frameId: string, documentId?: string) {
const frame = this._frames.get(frameId); const frame = this._frames.get(frameId);
if (!frame) if (!frame)
return; return;
for (const barrier of this._signalBarriers) for (const barrier of this._signalBarriers)
barrier.addFrameNavigation(frame); barrier.addFrameNavigation(frame);
frame._pendingDocumentId = documentId; if (frame._pendingDocument && frame._pendingDocument.documentId === documentId) {
} // Do not override request with undefined.
frameUpdatedDocumentIdForNavigation(frameId: string, documentId: string) {
const frame = this._frames.get(frameId);
if (!frame)
return; return;
frame._pendingDocumentId = documentId; }
frame._pendingDocument = { documentId, request: undefined };
} }
frameCommittedNewDocumentNavigation(frameId: string, url: string, name: string, documentId: string, initial: boolean) { frameCommittedNewDocumentNavigation(frameId: string, url: string, name: string, documentId: string, initial: boolean) {
@ -146,11 +150,16 @@ export class FrameManager {
this.removeChildFramesRecursively(frame); this.removeChildFramesRecursively(frame);
frame._url = url; frame._url = url;
frame._name = name; frame._name = name;
debugAssert(!frame._pendingDocumentId || frame._pendingDocumentId === documentId); if (frame._pendingDocument && frame._pendingDocument.documentId === undefined)
frame._lastDocumentId = documentId; frame._pendingDocument.documentId = documentId;
frame._pendingDocumentId = ''; debugAssert(!frame._pendingDocument || frame._pendingDocument.documentId === documentId);
if (frame._pendingDocument && frame._pendingDocument.documentId === documentId)
frame._currentDocument = frame._pendingDocument;
else
frame._currentDocument = { documentId, request: undefined };
frame._pendingDocument = undefined;
for (const task of frame._frameTasks) for (const task of frame._frameTasks)
task.onNewDocument(documentId); task.onNewDocument(frame._currentDocument);
this.clearFrameLifecycle(frame); this.clearFrameLifecycle(frame);
if (!initial) if (!initial)
this._page.emit(Events.Page.FrameNavigated, frame); this._page.emit(Events.Page.FrameNavigated, frame);
@ -166,6 +175,18 @@ export class FrameManager {
this._page.emit(Events.Page.FrameNavigated, frame); this._page.emit(Events.Page.FrameNavigated, frame);
} }
frameAbortedNavigation(frameId: string, errorText: string, documentId?: string) {
const frame = this._frames.get(frameId);
if (!frame || !frame._pendingDocument)
return;
if (documentId !== undefined && frame._pendingDocument.documentId !== documentId)
return;
const pending = frame._pendingDocument;
frame._pendingDocument = undefined;
for (const task of frame._frameTasks)
task.onNewDocument(pending, new Error(errorText));
}
frameDetached(frameId: string) { frameDetached(frameId: string) {
const frame = this._frames.get(frameId); const frame = this._frames.get(frameId);
if (frame) if (frame)
@ -194,16 +215,17 @@ export class FrameManager {
clearFrameLifecycle(frame: Frame) { clearFrameLifecycle(frame: Frame) {
frame._firedLifecycleEvents.clear(); frame._firedLifecycleEvents.clear();
// Keep the current navigation request if any. // Keep the current navigation request if any.
frame._inflightRequests = new Set(Array.from(frame._inflightRequests).filter(request => request._documentId === frame._lastDocumentId)); frame._inflightRequests = new Set(Array.from(frame._inflightRequests).filter(request => request === frame._currentDocument.request));
frame._stopNetworkIdleTimer(); frame._stopNetworkIdleTimer();
if (frame._inflightRequests.size === 0) if (frame._inflightRequests.size === 0)
frame._startNetworkIdleTimer(); frame._startNetworkIdleTimer();
} }
requestStarted(request: network.Request) { requestStarted(request: network.Request) {
const frame = request.frame();
this._inflightRequestStarted(request); this._inflightRequestStarted(request);
for (const task of request.frame()._frameTasks) if (request._documentId)
task.onRequest(request); frame._pendingDocument = { documentId: request._documentId, request };
if (request._isFavicon) { if (request._isFavicon) {
const route = request._route(); const route = request._route();
if (route) if (route)
@ -225,27 +247,18 @@ export class FrameManager {
} }
requestFailed(request: network.Request, canceled: boolean) { requestFailed(request: network.Request, canceled: boolean) {
const frame = request.frame();
this._inflightRequestFinished(request); this._inflightRequestFinished(request);
if (request._documentId) { if (frame._pendingDocument && frame._pendingDocument.request === request) {
const isPendingDocument = request.frame()._pendingDocumentId === request._documentId; let errorText = request.failure()!.errorText;
if (isPendingDocument) { if (canceled)
request.frame()._pendingDocumentId = ''; errorText += '; maybe frame was detached?';
let errorText = request.failure()!.errorText; this.frameAbortedNavigation(frame._id, errorText, frame._pendingDocument.documentId);
if (canceled)
errorText += '; maybe frame was detached?';
for (const task of request.frame()._frameTasks)
task.onNewDocument(request._documentId, new Error(errorText));
}
} }
if (!request._isFavicon) if (!request._isFavicon)
this._page.emit(Events.Page.RequestFailed, request); this._page.emit(Events.Page.RequestFailed, request);
} }
provisionalLoadFailed(frame: Frame, documentId: string, error: string) {
for (const task of frame._frameTasks)
task.onNewDocument(documentId, new Error(error));
}
private _notifyLifecycle(frame: Frame, lifecycleEvent: types.LifecycleEvent) { private _notifyLifecycle(frame: Frame, lifecycleEvent: types.LifecycleEvent) {
for (let parent: Frame | null = frame; parent; parent = parent.parentFrame()) { for (let parent: Frame | null = frame; parent; parent = parent.parentFrame()) {
for (const frameTask of parent._frameTasks) for (const frameTask of parent._frameTasks)
@ -301,8 +314,8 @@ export class FrameManager {
export class Frame { export class Frame {
_id: string; _id: string;
readonly _firedLifecycleEvents: Set<types.LifecycleEvent>; readonly _firedLifecycleEvents: Set<types.LifecycleEvent>;
_lastDocumentId = ''; _currentDocument: DocumentInfo;
_pendingDocumentId = ''; _pendingDocument?: DocumentInfo;
_frameTasks = new Set<FrameTask>(); _frameTasks = new Set<FrameTask>();
readonly _page: Page; readonly _page: Page;
private _parentFrame: Frame | null; private _parentFrame: Frame | null;
@ -322,6 +335,7 @@ export class Frame {
this._firedLifecycleEvents = new Set(); this._firedLifecycleEvents = new Set();
this._page = page; this._page = page;
this._parentFrame = parentFrame; this._parentFrame = parentFrame;
this._currentDocument = { documentId: undefined, request: undefined };
this._detachedPromise = new Promise<void>(x => this._detachedCallback = x); this._detachedPromise = new Promise<void>(x => this._detachedCallback = x);
@ -358,14 +372,14 @@ export class Frame {
sameDocumentPromise.catch(e => {}); sameDocumentPromise.catch(e => {});
throw e; throw e;
}); });
let request: network.Request | undefined;
if (navigateResult.newDocumentId) { if (navigateResult.newDocumentId) {
// Do not leave sameDocumentPromise unhandled. // Do not leave sameDocumentPromise unhandled.
sameDocumentPromise.catch(e => {}); sameDocumentPromise.catch(e => {});
await frameTask.waitForSpecificDocument(navigateResult.newDocumentId); request = await frameTask.waitForSpecificDocument(navigateResult.newDocumentId);
} else { } else {
await sameDocumentPromise; await sameDocumentPromise;
} }
const request = (navigateResult && navigateResult.newDocumentId) ? frameTask.request(navigateResult.newDocumentId) : null;
await frameTask.waitForLifecycle(options.waitUntil === undefined ? 'load' : options.waitUntil); await frameTask.waitForLifecycle(options.waitUntil === undefined ? 'load' : options.waitUntil);
frameTask.done(); frameTask.done();
return request ? request._finalRequest().response() : null; return request ? request._finalRequest().response() : null;
@ -377,12 +391,11 @@ export class Frame {
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : ''; const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
progress.logger.info(`waiting for navigation${toUrl} until "${options.waitUntil || 'load'}"`); progress.logger.info(`waiting for navigation${toUrl} until "${options.waitUntil || 'load'}"`);
const frameTask = new FrameTask(this, progress); const frameTask = new FrameTask(this, progress);
let documentId: string | undefined; let request: network.Request | undefined;
await Promise.race([ await Promise.race([
frameTask.waitForNewDocument(options.url).then(id => documentId = id), frameTask.waitForNewDocument(options.url).then(r => request = r),
frameTask.waitForSameDocumentNavigation(options.url), frameTask.waitForSameDocumentNavigation(options.url),
]); ]);
const request = documentId ? frameTask.request(documentId) : null;
await frameTask.waitForLifecycle(options.waitUntil === undefined ? 'load' : options.waitUntil); await frameTask.waitForLifecycle(options.waitUntil === undefined ? 'load' : options.waitUntil);
frameTask.done(); frameTask.done();
return request ? request._finalRequest().response() : null; return request ? request._finalRequest().response() : null;
@ -1026,11 +1039,10 @@ class SignalBarrier {
class FrameTask { class FrameTask {
private readonly _frame: Frame; private readonly _frame: Frame;
private readonly _requestMap = new Map<string, network.Request>();
private readonly _progress: Progress | null = null; private readonly _progress: Progress | null = null;
private _onSameDocument?: { url?: types.URLMatch, resolve: () => void }; private _onSameDocument?: { url?: types.URLMatch, resolve: () => void };
private _onSpecificDocument?: { expectedDocumentId: string, resolve: () => void, reject: (error: Error) => void }; private _onSpecificDocument?: { expectedDocumentId: string, resolve: (request: network.Request | undefined) => void, reject: (error: Error) => void };
private _onNewDocument?: { url?: types.URLMatch, resolve: (documentId: string) => void, reject: (error: Error) => void }; private _onNewDocument?: { url?: types.URLMatch, resolve: (request: network.Request | undefined) => void, reject: (error: Error) => void };
private _onLifecycle?: { waitUntil: types.LifecycleEvent, resolve: () => void }; private _onLifecycle?: { waitUntil: types.LifecycleEvent, resolve: () => void };
constructor(frame: Frame, progress: Progress | null) { constructor(frame: Frame, progress: Progress | null) {
@ -1041,16 +1053,6 @@ class FrameTask {
progress.cleanupWhenAborted(() => this.done()); progress.cleanupWhenAborted(() => this.done());
} }
onRequest(request: network.Request) {
if (!request._documentId || request.redirectedFrom())
return;
this._requestMap.set(request._documentId, request);
}
request(documentId: string): network.Request | undefined {
return this._requestMap.get(documentId);
}
onSameDocument() { onSameDocument() {
if (this._progress) if (this._progress)
this._progress.logger.info(`navigated to "${this._frame._url}"`); this._progress.logger.info(`navigated to "${this._frame._url}"`);
@ -1058,15 +1060,15 @@ class FrameTask {
this._onSameDocument.resolve(); this._onSameDocument.resolve();
} }
onNewDocument(documentId: string, error?: Error) { onNewDocument(documentInfo: DocumentInfo, error?: Error) {
if (this._progress && !error) if (this._progress && !error)
this._progress.logger.info(`navigated to "${this._frame._url}"`); this._progress.logger.info(`navigated to "${this._frame._url}"`);
if (this._onSpecificDocument) { if (this._onSpecificDocument) {
if (documentId === this._onSpecificDocument.expectedDocumentId) { if (documentInfo.documentId === this._onSpecificDocument.expectedDocumentId) {
if (error) if (error)
this._onSpecificDocument.reject(error); this._onSpecificDocument.reject(error);
else else
this._onSpecificDocument.resolve(); this._onSpecificDocument.resolve(documentInfo.request);
} else if (!error) { } else if (!error) {
this._onSpecificDocument.reject(new Error('Navigation interrupted by another one')); this._onSpecificDocument.reject(new Error('Navigation interrupted by another one'));
} }
@ -1075,7 +1077,7 @@ class FrameTask {
if (error) if (error)
this._onNewDocument.reject(error); this._onNewDocument.reject(error);
else if (helper.urlMatches(this._frame.url(), this._onNewDocument.url)) else if (helper.urlMatches(this._frame.url(), this._onNewDocument.url))
this._onNewDocument.resolve(documentId); this._onNewDocument.resolve(documentInfo.request);
} }
} }
@ -1093,14 +1095,14 @@ class FrameTask {
}); });
} }
waitForSpecificDocument(expectedDocumentId: string): Promise<void> { waitForSpecificDocument(expectedDocumentId: string): Promise<network.Request | undefined> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
assert(!this._onSpecificDocument); assert(!this._onSpecificDocument);
this._onSpecificDocument = { expectedDocumentId, resolve, reject }; this._onSpecificDocument = { expectedDocumentId, resolve, reject };
}); });
} }
waitForNewDocument(url?: types.URLMatch): Promise<string> { waitForNewDocument(url?: types.URLMatch): Promise<network.Request | undefined> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
assert(!this._onNewDocument); assert(!this._onNewDocument);
this._onNewDocument = { url, resolve, reject }; this._onNewDocument = { url, resolve, reject };

View File

@ -89,14 +89,14 @@ export class WKBrowser extends BrowserBase {
const page = this._wkPages.get(payload.pageProxyId); const page = this._wkPages.get(payload.pageProxyId);
if (!page) if (!page)
return; return;
const frameManager = page._page._frameManager; // In some cases, e.g. blob url download, we receive only frameScheduledNavigation
const frame = frameManager.frame(payload.frameId); // but no signals that the navigation was canceled and replaced by download. Fix it
if (frame) { // here by simulating cancelled provisional load which matches downloads from network.
// In some cases, e.g. blob url download, we receive only frameScheduledNavigation //
// but no signals that the navigation was canceled and replaced by download. Fix it // TODO: this is racy, because download might be unrelated any navigation, and we will
// here by simulating cancelled provisional load which matches downloads from network. // abort navgitation that is still running. We should be able to fix this by
frameManager.provisionalLoadFailed(frame, '', 'Download is starting'); // instrumenting policy decision start/proceed/cancel.
} page._page._frameManager.frameAbortedNavigation(payload.frameId, 'Download is starting');
let originPage = page._initializedPage; let originPage = page._initializedPage;
// If it's a new window download, report it on the opener page. // If it's a new window download, report it on the opener page.
if (!originPage) { if (!originPage) {

View File

@ -248,7 +248,7 @@ export class WKPage implements PageDelegate {
let errorText = event.error; let errorText = event.error;
if (errorText.includes('cancelled')) if (errorText.includes('cancelled'))
errorText += '; maybe frame was detached?'; errorText += '; maybe frame was detached?';
this._page._frameManager.provisionalLoadFailed(this._page.mainFrame(), event.loaderId, errorText); this._page._frameManager.frameAbortedNavigation(this._page.mainFrame()._id, errorText, event.loaderId);
} }
handleWindowOpen(event: Protocol.Playwright.windowOpenPayload) { handleWindowOpen(event: Protocol.Playwright.windowOpenPayload) {
@ -366,7 +366,7 @@ export class WKPage implements PageDelegate {
} }
private _onFrameScheduledNavigation(frameId: string) { private _onFrameScheduledNavigation(frameId: string) {
this._page._frameManager.frameRequestedNavigation(frameId, ''); this._page._frameManager.frameRequestedNavigation(frameId);
} }
private _onFrameStoppedLoading(frameId: string) { private _onFrameStoppedLoading(frameId: string) {
@ -835,8 +835,6 @@ export class WKPage implements PageDelegate {
// TODO(einbinder) this will fail if we are an XHR document request // TODO(einbinder) this will fail if we are an XHR document request
const isNavigationRequest = event.type === 'Document'; const isNavigationRequest = event.type === 'Document';
const documentId = isNavigationRequest ? event.loaderId : undefined; const documentId = isNavigationRequest ? event.loaderId : undefined;
if (isNavigationRequest)
this._page._frameManager.frameUpdatedDocumentIdForNavigation(event.frameId, documentId!);
// We do not support intercepting redirects. // We do not support intercepting redirects.
const allowInterception = this._page._needsRequestInterception() && !redirectedFrom; const allowInterception = this._page._needsRequestInterception() && !redirectedFrom;
const request = new WKInterceptableRequest(session, allowInterception, frame, event, redirectedFrom, documentId); const request = new WKInterceptableRequest(session, allowInterception, frame, event, redirectedFrom, documentId);