chore: reuse navigation methods between browsers (#271)

This commit is contained in:
Dmitry Gozman 2019-12-16 22:02:33 -08:00 committed by Pavel Feldman
parent 48be99a56e
commit 5a60a96410
7 changed files with 113 additions and 191 deletions

View File

@ -107,79 +107,17 @@ export class FrameManager implements PageDelegate {
this._page._didClose();
}
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}): Promise<network.Response | null> {
const {
referer = this._networkManager.extraHTTPHeaders()['referer'],
waitUntil = (['load'] as frames.LifecycleEvent[]),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
let ensureNewDocumentNavigation = false;
let error = await Promise.race([
navigate(this._client, url, referer, frame._id),
watcher.timeoutOrTerminationPromise,
]);
if (!error) {
error = await Promise.race([
watcher.timeoutOrTerminationPromise,
ensureNewDocumentNavigation ? watcher.newDocumentNavigationPromise : watcher.sameDocumentNavigationPromise,
]);
}
watcher.dispose();
if (error)
throw error;
return watcher.navigationResponse();
async function navigate(client: CDPSession, url: string, referrer: string, frameId: string): Promise<Error | null> {
try {
const response = await client.send('Page.navigate', {url, referrer, frameId});
ensureNewDocumentNavigation = !!response.loaderId;
return response.errorText ? new Error(`${response.errorText} at ${url}`) : null;
} catch (error) {
return error;
}
}
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id });
if (response.errorText)
throw new Error(`${response.errorText} at ${url}`);
return { newDocumentId: response.loaderId, isSameDocument: !response.loaderId };
}
async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}): Promise<network.Response | null> {
const {
waitUntil = (['load'] as frames.LifecycleEvent[]),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.sameDocumentNavigationPromise,
watcher.newDocumentNavigationPromise,
]);
watcher.dispose();
if (error)
throw error;
return watcher.navigationResponse();
}
async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) {
const {
waitUntil = (['load'] as frames.LifecycleEvent[]),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const context = await frame._utilityContext();
needsLifecycleResetOnSetContent(): boolean {
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
// lifecycle event. @see https://crrev.com/608658
await context.evaluate(html => {
document.open();
document.write(html);
document.close();
}, html);
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.lifecyclePromise,
]);
watcher.dispose();
if (error)
throw error;
return false;
}
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {

View File

@ -173,67 +173,13 @@ export class FrameManager implements PageDelegate {
this._page._didClose();
}
async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}) {
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = (['load'] as frames.LifecycleEvent[]),
} = options;
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.newDocumentNavigationPromise,
watcher.sameDocumentNavigationPromise,
]);
watcher.dispose();
if (error)
throw error;
return watcher.navigationResponse();
async navigateFrame(frame: frames.Frame, url: string, referer: string | undefined): Promise<frames.GotoResult> {
const response = await this._session.send('Page.navigate', { url, referer, frameId: frame._id });
return { newDocumentId: response.navigationId, isSameDocument: !response.navigationId };
}
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}) {
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = (['load'] as frames.LifecycleEvent[]),
referer,
} = options;
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
await this._session.send('Page.navigate', {
frameId: frame._id,
referer,
url,
});
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.newDocumentNavigationPromise,
watcher.sameDocumentNavigationPromise,
]);
watcher.dispose();
if (error)
throw error;
return watcher.navigationResponse();
}
async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) {
const {
waitUntil = (['load'] as frames.LifecycleEvent[]),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const context = await frame._utilityContext();
frame._firedLifecycleEvents.clear();
await context.evaluate(html => {
document.open();
document.write(html);
document.close();
}, html);
const watcher = new frames.LifecycleWatcher(frame, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.lifecyclePromise,
]);
watcher.dispose();
if (error)
throw error;
needsLifecycleResetOnSetContent(): boolean {
return true;
}
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void> {

View File

@ -45,6 +45,10 @@ export type NavigateOptions = {
export type GotoOptions = NavigateOptions & {
referer?: string,
};
export type GotoResult = {
newDocumentId?: string,
isSameDocument?: boolean,
};
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']);
@ -280,12 +284,60 @@ export class Frame {
this._parentFrame._childFrames.add(this);
}
async goto(url: string, options?: GotoOptions): Promise<network.Response | null> {
return this._page._delegate.navigateFrame(this, url, options);
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> {
const {
referer = (this._page._state.extraHTTPHeaders || {})['referer'],
waitUntil = (['load'] as LifecycleEvent[]),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const watcher = new LifecycleWatcher(this, waitUntil, timeout);
let navigateResult: GotoResult;
const navigate = async () => {
try {
navigateResult = await this._page._delegate.navigateFrame(this, url, referer);
} catch (error) {
return error;
}
};
let error = await Promise.race([
navigate(),
watcher.timeoutOrTerminationPromise,
]);
if (!error) {
const promises = [watcher.timeoutOrTerminationPromise];
if (navigateResult.newDocumentId) {
watcher.setExpectedDocumentId(navigateResult.newDocumentId, url);
promises.push(watcher.newDocumentNavigationPromise);
} else if (navigateResult.isSameDocument) {
promises.push(watcher.sameDocumentNavigationPromise);
} else {
promises.push(watcher.sameDocumentNavigationPromise, watcher.newDocumentNavigationPromise);
}
error = await Promise.race(promises);
}
watcher.dispose();
if (error)
throw error;
return watcher.navigationResponse();
}
async waitForNavigation(options?: NavigateOptions): Promise<network.Response | null> {
return this._page._delegate.waitForFrameNavigation(this, options);
async waitForNavigation(options: NavigateOptions = {}): Promise<network.Response | null> {
const {
waitUntil = (['load'] as LifecycleEvent[]),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const watcher = new LifecycleWatcher(this, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.sameDocumentNavigationPromise,
watcher.newDocumentNavigationPromise,
]);
watcher.dispose();
if (error)
throw error;
return watcher.navigationResponse();
}
_mainContext(): Promise<dom.FrameExecutionContext> {
@ -351,8 +403,27 @@ export class Frame {
});
}
async setContent(html: string, options?: NavigateOptions): Promise<void> {
return this._page._delegate.setFrameContent(this, html, options);
async setContent(html: string, options: NavigateOptions = {}): Promise<void> {
const {
waitUntil = (['load'] as LifecycleEvent[]),
timeout = this._page._timeoutSettings.navigationTimeout(),
} = options;
const context = await this._utilityContext();
if (this._page._delegate.needsLifecycleResetOnSetContent())
this._firedLifecycleEvents.clear();
await context.evaluate(html => {
document.open();
document.write(html);
document.close();
}, html);
const watcher = new LifecycleWatcher(this, waitUntil, timeout);
const error = await Promise.race([
watcher.timeoutOrTerminationPromise,
watcher.lifecyclePromise,
]);
watcher.dispose();
if (error)
throw error;
}
name(): string {
@ -805,6 +876,13 @@ export class LifecycleWatcher {
this._checkLifecycleComplete();
}
setExpectedDocumentId(documentId: string, url: string) {
this._expectedDocumentId = documentId;
this._targetUrl = url;
if (this._navigationRequest && this._navigationRequest._documentId !== documentId)
this._navigationRequest = null;
}
_onFrameDetached(frame: Frame) {
if (this._frame === frame) {
this._frameDetachedCallback.call(null, new Error('Navigating frame was detached'));
@ -822,7 +900,9 @@ export class LifecycleWatcher {
_onNavigationRequest(frame: Frame, request: network.Request) {
assert(request._documentId);
if (frame === this._frame && this._expectedDocumentId === undefined) {
if (frame !== this._frame)
return;
if (this._expectedDocumentId === undefined || this._expectedDocumentId === request._documentId) {
this._navigationRequest = request;
this._expectedDocumentId = request._documentId;
this._targetUrl = request.url();

View File

@ -41,9 +41,8 @@ export interface PageDelegate {
evaluateOnNewDocument(source: string): Promise<void>;
closePage(runBeforeUnload: boolean): Promise<void>;
navigateFrame(frame: frames.Frame, url: string, options?: frames.GotoOptions): Promise<network.Response | null>;
waitForFrameNavigation(frame: frames.Frame, options?: frames.NavigateOptions): Promise<network.Response | null>;
setFrameContent(frame: frames.Frame, html: string, options?: frames.NavigateOptions): Promise<void>;
navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult>;
needsLifecycleResetOnSetContent(): boolean;
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>;
setUserAgent(userAgent: string): Promise<void>;

View File

@ -187,60 +187,13 @@ export class FrameManager implements PageDelegate {
this._contextIdToContext.set(contextPayload.id, context);
}
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}): Promise<network.Response | null> {
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = (['load'] as frames.LifecycleEvent[])
} = options;
const watchDog = new frames.LifecycleWatcher(frame, waitUntil, timeout);
await this._session.send('Page.navigate', {url, frameId: frame._id});
const error = await Promise.race([
watchDog.timeoutOrTerminationPromise,
watchDog.newDocumentNavigationPromise,
watchDog.sameDocumentNavigationPromise,
]);
watchDog.dispose();
if (error)
throw error;
return watchDog.navigationResponse();
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
await this._session.send('Page.navigate', { url, frameId: frame._id });
return {}; // We cannot get loaderId of cross-process navigation in advance.
}
async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}): Promise<network.Response | null> {
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = (['load'] as frames.LifecycleEvent[])
} = options;
const watchDog = new frames.LifecycleWatcher(frame, waitUntil, timeout);
const error = await Promise.race([
watchDog.timeoutOrTerminationPromise,
watchDog.newDocumentNavigationPromise,
watchDog.sameDocumentNavigationPromise,
]);
watchDog.dispose();
if (error)
throw error;
return watchDog.navigationResponse();
}
async setFrameContent(frame: frames.Frame, html: string, options: frames.NavigateOptions = {}) {
// We rely upon the fact that document.open() will trigger Page.loadEventFired.
const {
timeout = this._page._timeoutSettings.navigationTimeout(),
waitUntil = (['load'] as frames.LifecycleEvent[])
} = options;
const watchDog = new frames.LifecycleWatcher(frame, waitUntil, timeout);
await frame.evaluate(html => {
document.open();
document.write(html);
document.close();
}, html);
const error = await Promise.race([
watchDog.timeoutOrTerminationPromise,
watchDog.lifecyclePromise,
]);
watchDog.dispose();
if (error)
throw error;
needsLifecycleResetOnSetContent(): boolean {
return true;
}
async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) {

View File

@ -125,7 +125,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'domcontentloaded'});
expect(response.status()).toBe(200);
});
it.skip(WEBKIT)('should work when page calls history API in beforeunload', async({page, server}) => {
it('should work when page calls history API in beforeunload', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
window.addEventListener('beforeunload', () => history.replaceState(null, 'initial', window.location.href), false);
@ -427,6 +427,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
expect(request1.headers['referer']).toBe('http://google.com/');
// Make sure subresources do not inherit referer.
expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html');
expect(page.url()).toBe(server.PREFIX + '/grid.html');
});
});

View File

@ -530,6 +530,11 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
const result = await page.content();
expect(result).toBe(expectedOutput);
});
it('should work with domcontentloaded', async({page, server}) => {
await page.setContent('<div>hello</div>', { waitUntil: 'domcontentloaded' });
const result = await page.content();
expect(result).toBe(expectedOutput);
});
it('should not confuse with previous navigation', async({page, server}) => {
const imgPath = '/img.png';
let imgResponse = null;