chore: reuse navigation methods between browsers (#271)
This commit is contained in:
parent
48be99a56e
commit
5a60a96410
|
|
@ -107,79 +107,17 @@ export class FrameManager implements PageDelegate {
|
||||||
this._page._didClose();
|
this._page._didClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}): Promise<network.Response | null> {
|
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
|
||||||
const {
|
const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id });
|
||||||
referer = this._networkManager.extraHTTPHeaders()['referer'],
|
if (response.errorText)
|
||||||
waitUntil = (['load'] as frames.LifecycleEvent[]),
|
throw new Error(`${response.errorText} at ${url}`);
|
||||||
timeout = this._page._timeoutSettings.navigationTimeout(),
|
return { newDocumentId: response.loaderId, isSameDocument: !response.loaderId };
|
||||||
} = 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 waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}): Promise<network.Response | null> {
|
needsLifecycleResetOnSetContent(): boolean {
|
||||||
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();
|
|
||||||
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
|
// We rely upon the fact that document.open() will reset frame lifecycle with "init"
|
||||||
// lifecycle event. @see https://crrev.com/608658
|
// lifecycle event. @see https://crrev.com/608658
|
||||||
await context.evaluate(html => {
|
return false;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
|
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
|
||||||
|
|
|
||||||
|
|
@ -173,67 +173,13 @@ export class FrameManager implements PageDelegate {
|
||||||
this._page._didClose();
|
this._page._didClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}) {
|
async navigateFrame(frame: frames.Frame, url: string, referer: string | undefined): Promise<frames.GotoResult> {
|
||||||
const {
|
const response = await this._session.send('Page.navigate', { url, referer, frameId: frame._id });
|
||||||
timeout = this._page._timeoutSettings.navigationTimeout(),
|
return { newDocumentId: response.navigationId, isSameDocument: !response.navigationId };
|
||||||
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, options: frames.GotoOptions = {}) {
|
needsLifecycleResetOnSetContent(): boolean {
|
||||||
const {
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void> {
|
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@ export type NavigateOptions = {
|
||||||
export type GotoOptions = NavigateOptions & {
|
export type GotoOptions = NavigateOptions & {
|
||||||
referer?: string,
|
referer?: string,
|
||||||
};
|
};
|
||||||
|
export type GotoResult = {
|
||||||
|
newDocumentId?: string,
|
||||||
|
isSameDocument?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
|
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
|
||||||
const kLifecycleEvents: Set<LifecycleEvent> = new Set(['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);
|
this._parentFrame._childFrames.add(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async goto(url: string, options?: GotoOptions): Promise<network.Response | null> {
|
async goto(url: string, options: GotoOptions = {}): Promise<network.Response | null> {
|
||||||
return this._page._delegate.navigateFrame(this, url, options);
|
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> {
|
async waitForNavigation(options: NavigateOptions = {}): Promise<network.Response | null> {
|
||||||
return this._page._delegate.waitForFrameNavigation(this, options);
|
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> {
|
_mainContext(): Promise<dom.FrameExecutionContext> {
|
||||||
|
|
@ -351,8 +403,27 @@ export class Frame {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async setContent(html: string, options?: NavigateOptions): Promise<void> {
|
async setContent(html: string, options: NavigateOptions = {}): Promise<void> {
|
||||||
return this._page._delegate.setFrameContent(this, html, options);
|
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 {
|
name(): string {
|
||||||
|
|
@ -805,6 +876,13 @@ export class LifecycleWatcher {
|
||||||
this._checkLifecycleComplete();
|
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) {
|
_onFrameDetached(frame: Frame) {
|
||||||
if (this._frame === frame) {
|
if (this._frame === frame) {
|
||||||
this._frameDetachedCallback.call(null, new Error('Navigating frame was detached'));
|
this._frameDetachedCallback.call(null, new Error('Navigating frame was detached'));
|
||||||
|
|
@ -822,7 +900,9 @@ export class LifecycleWatcher {
|
||||||
|
|
||||||
_onNavigationRequest(frame: Frame, request: network.Request) {
|
_onNavigationRequest(frame: Frame, request: network.Request) {
|
||||||
assert(request._documentId);
|
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._navigationRequest = request;
|
||||||
this._expectedDocumentId = request._documentId;
|
this._expectedDocumentId = request._documentId;
|
||||||
this._targetUrl = request.url();
|
this._targetUrl = request.url();
|
||||||
|
|
|
||||||
|
|
@ -41,9 +41,8 @@ export interface PageDelegate {
|
||||||
evaluateOnNewDocument(source: string): Promise<void>;
|
evaluateOnNewDocument(source: string): Promise<void>;
|
||||||
closePage(runBeforeUnload: boolean): Promise<void>;
|
closePage(runBeforeUnload: boolean): Promise<void>;
|
||||||
|
|
||||||
navigateFrame(frame: frames.Frame, url: string, options?: frames.GotoOptions): Promise<network.Response | null>;
|
navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult>;
|
||||||
waitForFrameNavigation(frame: frames.Frame, options?: frames.NavigateOptions): Promise<network.Response | null>;
|
needsLifecycleResetOnSetContent(): boolean;
|
||||||
setFrameContent(frame: frames.Frame, html: string, options?: frames.NavigateOptions): Promise<void>;
|
|
||||||
|
|
||||||
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>;
|
setExtraHTTPHeaders(extraHTTPHeaders: network.Headers): Promise<void>;
|
||||||
setUserAgent(userAgent: string): Promise<void>;
|
setUserAgent(userAgent: string): Promise<void>;
|
||||||
|
|
|
||||||
|
|
@ -187,60 +187,13 @@ export class FrameManager implements PageDelegate {
|
||||||
this._contextIdToContext.set(contextPayload.id, context);
|
this._contextIdToContext.set(contextPayload.id, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
async navigateFrame(frame: frames.Frame, url: string, options: frames.GotoOptions = {}): Promise<network.Response | null> {
|
async navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
|
||||||
const {
|
await this._session.send('Page.navigate', { url, frameId: frame._id });
|
||||||
timeout = this._page._timeoutSettings.navigationTimeout(),
|
return {}; // We cannot get loaderId of cross-process navigation in advance.
|
||||||
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 waitForFrameNavigation(frame: frames.Frame, options: frames.NavigateOptions = {}): Promise<network.Response | null> {
|
needsLifecycleResetOnSetContent(): boolean {
|
||||||
const {
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) {
|
async _onConsoleMessage(event: Protocol.Console.messageAddedPayload) {
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
|
||||||
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'domcontentloaded'});
|
const response = await page.goto(server.EMPTY_PAGE, {waitUntil: 'domcontentloaded'});
|
||||||
expect(response.status()).toBe(200);
|
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.goto(server.EMPTY_PAGE);
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.addEventListener('beforeunload', () => history.replaceState(null, 'initial', window.location.href), false);
|
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/');
|
expect(request1.headers['referer']).toBe('http://google.com/');
|
||||||
// Make sure subresources do not inherit referer.
|
// Make sure subresources do not inherit referer.
|
||||||
expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html');
|
expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html');
|
||||||
|
expect(page.url()).toBe(server.PREFIX + '/grid.html');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -530,6 +530,11 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
|
||||||
const result = await page.content();
|
const result = await page.content();
|
||||||
expect(result).toBe(expectedOutput);
|
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}) => {
|
it('should not confuse with previous navigation', async({page, server}) => {
|
||||||
const imgPath = '/img.png';
|
const imgPath = '/img.png';
|
||||||
let imgResponse = null;
|
let imgResponse = null;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue