chore: refactor goBack/goForward/reload (#3859)

These methods are the only users of waitForNavigation and
waitForLoadState on the server side. This refactor lifts the
Progress wrapper to the top-most goBack/goForward/reload call
and leaves waitForNavigation/waitForLoadState as internal helpers.
This way we get a single Progress for the actual api call.
This commit is contained in:
Dmitry Gozman 2020-09-14 16:43:17 -07:00 committed by GitHub
parent 2a66f8a066
commit 2f0d2029ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 63 additions and 56 deletions

View file

@ -175,7 +175,7 @@ export abstract class BrowserContext extends EventEmitter {
await waitForEvent.promise;
}
const pages = this.pages();
await pages[0].mainFrame().waitForLoadState();
await pages[0].mainFrame()._waitForLoadState(progress, 'load');
if (pages.length !== 1 || pages[0].mainFrame().url() !== 'about:blank')
throw new Error(`Arguments can not specify page to be opened (first url is ${pages[0].mainFrame().url()})`);
if (this._options.isMobile || this._options.locale) {

View file

@ -27,7 +27,7 @@ import * as types from '../types';
import { launchProcess, waitForLine, envArrayToObject } from '../processLauncher';
import { BrowserContext } from '../browserContext';
import type {BrowserWindow} from 'electron';
import { ProgressController } from '../progress';
import { ProgressController, runAbortableTask } from '../progress';
import { EventEmitter } from 'events';
import { helper } from '../helper';
import { BrowserProcess } from '../browser';
@ -88,7 +88,7 @@ export class ElectronApplication extends EventEmitter {
this._windows.delete(page);
});
this._windows.add(page);
await page.mainFrame().waitForLoadState('domcontentloaded').catch(e => {}); // can happen after detach
await runAbortableTask(progress => page.mainFrame()._waitForLoadState(progress, 'domcontentloaded'), page._timeoutSettings.navigationTimeout({})).catch(e => {}); // can happen after detach
this.emit(ElectronApplication.Events.Window, page);
}

View file

@ -424,8 +424,16 @@ export class Frame extends EventEmitter {
this._subtreeLifecycleEvents = events;
}
setupNavigationProgressController(controller: ProgressController) {
this._page._disconnectedPromise.then(() => controller.abort(new Error('Navigation failed because page was closed!')));
this._page._crashedPromise.then(() => controller.abort(new Error('Navigation failed because page crashed!')));
this._detachedPromise.then(() => controller.abort(new Error('Navigating frame was detached!')));
}
async goto(url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
return runNavigationTask(this, options, async progress => {
const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(options));
this.setupNavigationProgressController(controller);
return controller.run(async progress => {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.log(`navigating to "${url}", waiting until "${waitUntil}"`);
const headers = this._page._state.extraHTTPHeaders || [];
@ -471,31 +479,25 @@ export class Frame extends EventEmitter {
});
}
async waitForNavigation(options: types.NavigateOptions = {}): Promise<network.Response | null> {
return runNavigationTask(this, options, async progress => {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.log(`waiting for navigation until "${waitUntil}"`);
async _waitForNavigation(progress: Progress, options: types.NavigateOptions): Promise<network.Response | null> {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
progress.log(`waiting for navigation until "${waitUntil}"`);
const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this, Frame.Events.Navigation, (event: NavigationEvent) => {
// Any failed navigation results in a rejection.
if (event.error)
return true;
progress.log(` navigated to "${this._url}"`);
const navigationEvent: NavigationEvent = await helper.waitForEvent(progress, this, Frame.Events.Navigation, (event: NavigationEvent) => {
// Any failed navigation results in a rejection.
if (event.error)
return true;
}).promise;
if (navigationEvent.error)
throw navigationEvent.error;
progress.log(` navigated to "${this._url}"`);
return true;
}).promise;
if (navigationEvent.error)
throw navigationEvent.error;
if (!this._subtreeLifecycleEvents.has(waitUntil))
await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
if (!this._subtreeLifecycleEvents.has(waitUntil))
await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise;
const request = navigationEvent.newDocument ? navigationEvent.newDocument.request : undefined;
return request ? request._finalRequest().response() : null;
});
}
async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise<void> {
return runNavigationTask(this, options, progress => this._waitForLoadState(progress, state));
const request = navigationEvent.newDocument ? navigationEvent.newDocument.request : undefined;
return request ? request._finalRequest().response() : null;
}
async _waitForLoadState(progress: Progress, state: types.LifecycleEvent): Promise<void> {
@ -606,7 +608,9 @@ export class Frame extends EventEmitter {
}
async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> {
return runNavigationTask(this, options, async progress => {
const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(options));
this.setupNavigationProgressController(controller);
return controller.run(async progress => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;
progress.log(`setting frame content, waiting until "${waitUntil}"`);
const tag = `--playwright--set--content--${this._id}--${++this._setContentCounter}--`;
@ -1087,15 +1091,6 @@ class SignalBarrier {
}
}
async function runNavigationTask<T>(frame: Frame, options: types.TimeoutOptions, task: (progress: Progress) => Promise<T>): Promise<T> {
const page = frame._page;
const controller = new ProgressController(page._timeoutSettings.navigationTimeout(options));
page._disconnectedPromise.then(() => controller.abort(new Error('Navigation failed because page was closed!')));
page._crashedPromise.then(() => controller.abort(new Error('Navigation failed because page crashed!')));
frame._detachedPromise.then(() => controller.abort(new Error('Navigating frame was detached!')));
return controller.run(task);
}
function verifyLifecycle(name: string, waitUntil: types.LifecycleEvent): types.LifecycleEvent {
if (waitUntil as unknown === 'networkidle0')
waitUntil = 'networkidle';

View file

@ -28,7 +28,7 @@ import { ConsoleMessage } from './console';
import * as accessibility from './accessibility';
import { EventEmitter } from 'events';
import { FileChooser } from './fileChooser';
import { runAbortableTask } from './progress';
import { ProgressController, runAbortableTask } from './progress';
import { assert, isError } from '../utils/utils';
import { debugLogger } from '../utils/debugLogger';
import { Selectors } from './selectors';
@ -262,34 +262,46 @@ export class Page extends EventEmitter {
this.emit(Page.Events.Console, message);
}
async reload(options?: types.NavigateOptions): Promise<network.Response | null> {
const waitPromise = this.mainFrame().waitForNavigation(options);
await this._delegate.reload();
const response = await waitPromise;
async reload(options: types.NavigateOptions = {}): Promise<network.Response | null> {
const controller = new ProgressController(this._timeoutSettings.navigationTimeout(options));
this.mainFrame().setupNavigationProgressController(controller);
const response = await controller.run(async progress => {
const waitPromise = this.mainFrame()._waitForNavigation(progress, options);
await this._delegate.reload();
return waitPromise;
});
await this._doSlowMo();
return response;
}
async goBack(options?: types.NavigateOptions): Promise<network.Response | null> {
const waitPromise = this.mainFrame().waitForNavigation(options);
const result = await this._delegate.goBack();
if (!result) {
waitPromise.catch(() => {});
return null;
}
const response = await waitPromise;
async goBack(options: types.NavigateOptions = {}): Promise<network.Response | null> {
const controller = new ProgressController(this._timeoutSettings.navigationTimeout(options));
this.mainFrame().setupNavigationProgressController(controller);
const response = await controller.run(async progress => {
const waitPromise = this.mainFrame()._waitForNavigation(progress, options);
const result = await this._delegate.goBack();
if (!result) {
waitPromise.catch(() => {});
return null;
}
return waitPromise;
});
await this._doSlowMo();
return response;
}
async goForward(options?: types.NavigateOptions): Promise<network.Response | null> {
const waitPromise = this.mainFrame().waitForNavigation(options);
const result = await this._delegate.goForward();
if (!result) {
waitPromise.catch(() => {});
return null;
}
const response = await waitPromise;
async goForward(options: types.NavigateOptions = {}): Promise<network.Response | null> {
const controller = new ProgressController(this._timeoutSettings.navigationTimeout(options));
this.mainFrame().setupNavigationProgressController(controller);
const response = await controller.run(async progress => {
const waitPromise = this.mainFrame()._waitForNavigation(progress, options);
const result = await this._delegate.goForward();
if (!result) {
waitPromise.catch(() => {});
return null;
}
return waitPromise;
});
await this._doSlowMo();
return response;
}