fix(navigation): waitForNavigation to pick up aborted navigation (#244)

This commit is contained in:
Dmitry Gozman 2019-12-13 16:35:10 -08:00 committed by GitHub
parent 08fc20c78e
commit dd2ce94de9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 58 additions and 39 deletions

View File

@ -201,7 +201,7 @@ export class FrameManager extends EventEmitter implements PageDelegate {
return; return;
if (event.name === 'init') { if (event.name === 'init') {
frame._firedLifecycleEvents.clear(); frame._firedLifecycleEvents.clear();
frame._onExpectedNewDocumentNavigation(event.loaderId); frame._expectNewDocumentNavigation(event.loaderId);
} else if (event.name === 'load') { } else if (event.name === 'load') {
frame._lifecycleEvent('load'); frame._lifecycleEvent('load');
} else if (event.name === 'DOMContentLoaded') { } else if (event.name === 'DOMContentLoaded') {

View File

@ -197,8 +197,13 @@ export class NetworkManager extends EventEmitter {
redirectChain = request.request._redirectChain; redirectChain = request.request._redirectChain;
} }
} }
// TODO: how can frame be null here?
const frame = event.frameId ? this._frameManager.frame(event.frameId) : null; const frame = event.frameId ? this._frameManager.frame(event.frameId) : null;
const request = new InterceptableRequest(this._client, frame, interceptionId, this._userRequestInterceptionEnabled, event, redirectChain); const isNavigationRequest = event.requestId === event.loaderId && event.type === 'Document';
const documentId = isNavigationRequest ? event.loaderId : undefined;
if (documentId && frame)
frame._expectNewDocumentNavigation(documentId, event.request.url);
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.emit(NetworkManagerEvents.Request, request.request);
} }
@ -259,6 +264,14 @@ 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);
if (request._documentId && request.request.frame()) {
const isCurrentDocument = request.request.frame()._lastDocumentId === request._documentId;
let errorText = event.errorText;
if (event.canceled)
errorText += '; maybe frame was detached?';
if (!isCurrentDocument)
request.request.frame()._onAbortedNewDocumentNavigation(request._documentId, errorText);
}
this.emit(NetworkManagerEvents.RequestFailed, request.request); this.emit(NetworkManagerEvents.RequestFailed, request.request);
} }
} }
@ -273,17 +286,19 @@ class InterceptableRequest {
readonly request: network.Request; readonly request: network.Request;
_requestId: string; _requestId: string;
_interceptionId: string; _interceptionId: string;
_documentId: string;
private _client: CDPSession; private _client: CDPSession;
private _allowInterception: boolean; private _allowInterception: boolean;
private _interceptionHandled = false; private _interceptionHandled = false;
constructor(client: CDPSession, frame: frames.Frame | null, interceptionId: string, allowInterception: boolean, event: Protocol.Network.requestWillBeSentPayload, redirectChain: network.Request[]) { constructor(client: CDPSession, frame: frames.Frame | null, interceptionId: string, documentId: string | undefined, allowInterception: boolean, event: Protocol.Network.requestWillBeSentPayload, redirectChain: network.Request[]) {
this._client = client; this._client = client;
this._requestId = event.requestId; this._requestId = event.requestId;
this._interceptionId = interceptionId; this._interceptionId = interceptionId;
this._documentId = documentId;
this._allowInterception = allowInterception; this._allowInterception = allowInterception;
this.request = new network.Request(frame, redirectChain, event.requestId === event.loaderId && event.type === 'Document', this.request = new network.Request(frame, redirectChain, !!documentId,
event.request.url, event.type.toLowerCase(), event.request.method, event.request.postData, headersObject(event.request.headers)); event.request.url, event.type.toLowerCase(), event.request.method, event.request.postData, headersObject(event.request.headers));
(this.request as any)[interceptableRequestSymbol] = this; (this.request as any)[interceptableRequestSymbol] = this;
} }

View File

@ -40,8 +40,8 @@ export class Permissions {
['gyroscope', 'sensors'], ['gyroscope', 'sensors'],
['magnetometer', 'sensors'], ['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'], ['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardRead'], ['clipboard-read', 'clipboardReadWrite'],
['clipboard-write', 'clipboardWrite'], ['clipboard-write', 'clipboardSanitizedWrite'],
['payment-handler', 'paymentHandler'], ['payment-handler', 'paymentHandler'],
// chrome-specific permissions we have. // chrome-specific permissions we have.
['midi-sysex', 'midiSysex'], ['midi-sysex', 'midiSysex'],

View File

@ -138,7 +138,7 @@ export class FrameManager extends EventEmitter implements PageDelegate {
_onNavigationStarted(params) { _onNavigationStarted(params) {
const frame = this._frames.get(params.frameId); const frame = this._frames.get(params.frameId);
frame._onExpectedNewDocumentNavigation(params.navigationId, params.url); frame._expectNewDocumentNavigation(params.navigationId, params.url);
} }
_onNavigationAborted(params) { _onNavigationAborted(params) {

View File

@ -422,7 +422,7 @@ export class Frame {
return context.evaluate(() => document.title); return context.evaluate(() => document.title);
} }
_onExpectedNewDocumentNavigation(documentId: string, url?: string) { _expectNewDocumentNavigation(documentId: string, url?: string) {
for (const watcher of this._page._lifecycleWatchers) for (const watcher of this._page._lifecycleWatchers)
watcher._onExpectedNewDocumentNavigation(this, documentId, url); watcher._onExpectedNewDocumentNavigation(this, documentId, url);
} }

View File

@ -231,7 +231,7 @@ export class FrameManager extends EventEmitter implements PageDelegate {
// Append session id to avoid cross-process loaderId clash. // Append session id to avoid cross-process loaderId clash.
const documentId = this._session._sessionId + '::' + framePayload.loaderId; const documentId = this._session._sessionId + '::' + framePayload.loaderId;
if (!initial) if (!initial)
frame._onExpectedNewDocumentNavigation(documentId); frame._expectNewDocumentNavigation(documentId);
frame._onCommittedNewDocumentNavigation(framePayload.url, framePayload.name, documentId); frame._onCommittedNewDocumentNavigation(framePayload.url, framePayload.name, documentId);
this._page.emit(Events.Page.FrameNavigated, frame); this._page.emit(Events.Page.FrameNavigated, frame);

View File

@ -102,7 +102,7 @@ export class NetworkManager extends EventEmitter {
const isNavigationRequest = event.type === 'Document'; const isNavigationRequest = event.type === 'Document';
const documentId = isNavigationRequest ? this._session._sessionId + '::' + event.loaderId : undefined; const documentId = isNavigationRequest ? this._session._sessionId + '::' + event.loaderId : undefined;
if (documentId) if (documentId)
frame._onExpectedNewDocumentNavigation(documentId, event.request.url); frame._expectNewDocumentNavigation(documentId, event.request.url);
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.emit(NetworkManagerEvents.Request, request.request);
@ -166,11 +166,11 @@ export class NetworkManager extends EventEmitter {
this._attemptedAuthentications.delete(request._interceptionId); this._attemptedAuthentications.delete(request._interceptionId);
if (request._documentId) { if (request._documentId) {
const isCurrentDocument = request.request.frame()._lastDocumentId === request._documentId; const isCurrentDocument = request.request.frame()._lastDocumentId === request._documentId;
// When frame was detached during load, "cancelled" comes before detach. let errorText = event.errorText;
// Ignore it and hope for the best. if (errorText.includes('cancelled'))
const wasCanceled = event.errorText.includes('cancelled'); errorText += '; maybe frame was detached?';
if (!isCurrentDocument && !wasCanceled) if (!isCurrentDocument)
request.request.frame()._onAbortedNewDocumentNavigation(request._documentId, event.errorText); request.request.frame()._onAbortedNewDocumentNavigation(request._documentId, errorText);
} }
this.emit(NetworkManagerEvents.RequestFailed, request.request); this.emit(NetworkManagerEvents.RequestFailed, request.request);
} }

View File

@ -157,32 +157,14 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
page.on('requestfailed', request => expect(request).toBeTruthy()); page.on('requestfailed', request => expect(request).toBeTruthy());
let error = null; let error = null;
await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e); await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e);
if (CHROME) { expectSSLError(error.message);
expect(error.message).toContain('net::ERR_CERT_AUTHORITY_INVALID');
} else if (WEBKIT) {
if (process.platform === 'darwin')
expect(error.message).toContain('The certificate for this server is invalid');
else
expect(error.message).toContain('Unacceptable TLS certificate');
} else {
expect(error.message).toContain('SSL_ERROR_UNKNOWN');
}
}); });
it('should fail when navigating to bad SSL after redirects', async({page, server, httpsServer}) => { it('should fail when navigating to bad SSL after redirects', async({page, server, httpsServer}) => {
server.setRedirect('/redirect/1.html', '/redirect/2.html'); server.setRedirect('/redirect/1.html', '/redirect/2.html');
server.setRedirect('/redirect/2.html', '/empty.html'); server.setRedirect('/redirect/2.html', '/empty.html');
let error = null; let error = null;
await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(e => error = e); await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(e => error = e);
if (CHROME) { expectSSLError(error.message);
expect(error.message).toContain('net::ERR_CERT_AUTHORITY_INVALID');
} else if (WEBKIT) {
if (process.platform === 'darwin')
expect(error.message).toContain('The certificate for this server is invalid');
else
expect(error.message).toContain('Unacceptable TLS certificate');
} else {
expect(error.message).toContain('SSL_ERROR_UNKNOWN');
}
}); });
xit('should throw if networkidle is passed as an option', async({page, server}) => { xit('should throw if networkidle is passed as an option', async({page, server}) => {
let error = null; let error = null;
@ -443,6 +425,15 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
expect(response).toBe(null); expect(response).toBe(null);
expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar'); expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar');
}); });
it('should work with clicking on links which do not commit navigation', async({page, server, httpsServer}) => {
await page.goto(server.EMPTY_PAGE);
await page.setContent(`<a href='${httpsServer.EMPTY_PAGE}'>foobar</a>`);
const [error] = await Promise.all([
page.waitForNavigation().catch(e => e),
page.click('a'),
]);
expectSSLError(error.message);
});
it('should work with history.pushState()', async({page, server}) => { it('should work with history.pushState()', async({page, server}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.setContent(` await page.setContent(`
@ -566,7 +557,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
await page.$eval('iframe', frame => frame.remove()); await page.$eval('iframe', frame => frame.remove());
const error = await navigationPromise; const error = await navigationPromise;
expect(error.message).toBe('Navigating frame was detached'); expect(error.message).toContain('frame was detached');
}); });
it('should return matching responses', async({page, server}) => { it('should return matching responses', async({page, server}) => {
// Disable cache: otherwise, chromium will cache similar requests. // Disable cache: otherwise, chromium will cache similar requests.
@ -623,7 +614,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
]); ]);
await page.$eval('iframe', frame => frame.remove()); await page.$eval('iframe', frame => frame.remove());
await navigationPromise; await navigationPromise;
expect(error.message).toBe('Navigating frame was detached'); expect(error.message).toContain('frame was detached');
}); });
}); });
@ -635,4 +626,17 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
expect(await page.evaluate(() => window._foo)).toBe(undefined); expect(await page.evaluate(() => window._foo)).toBe(undefined);
}); });
}); });
function expectSSLError(errorMessage) {
if (CHROME) {
expect(errorMessage).toContain('net::ERR_CERT_AUTHORITY_INVALID');
} else if (WEBKIT) {
if (process.platform === 'darwin')
expect(errorMessage).toContain('The certificate for this server is invalid');
else
expect(errorMessage).toContain('Unacceptable TLS certificate');
} else {
expect(errorMessage).toContain('SSL_ERROR_UNKNOWN');
}
}
}; };