diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 8c55acee865e8..9e20d01c41271 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -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) { diff --git a/src/server/electron/electron.ts b/src/server/electron/electron.ts index c721cbc996abe..aea3e55088389 100644 --- a/src/server/electron/electron.ts +++ b/src/server/electron/electron.ts @@ -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); } diff --git a/src/server/frames.ts b/src/server/frames.ts index cf286dcb96e76..1d7beae55f289 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -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 { - 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 { - 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 { + 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; - - if (!this._subtreeLifecycleEvents.has(waitUntil)) - await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise; + progress.log(` navigated to "${this._url}"`); + return true; + }).promise; + if (navigationEvent.error) + throw navigationEvent.error; - const request = navigationEvent.newDocument ? navigationEvent.newDocument.request : undefined; - return request ? request._finalRequest().response() : null; - }); - } + if (!this._subtreeLifecycleEvents.has(waitUntil)) + await helper.waitForEvent(progress, this, Frame.Events.AddLifecycle, (e: types.LifecycleEvent) => e === waitUntil).promise; - async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise { - 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 { @@ -606,7 +608,9 @@ export class Frame extends EventEmitter { } async setContent(html: string, options: types.NavigateOptions = {}): Promise { - 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}--`; @@ -1083,15 +1087,6 @@ class SignalBarrier { } } -async function runNavigationTask(frame: Frame, options: types.TimeoutOptions, task: (progress: Progress) => Promise): Promise { - 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'; diff --git a/src/server/page.ts b/src/server/page.ts index 194a6ceee77fc..92a984e1fc575 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -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 { - const waitPromise = this.mainFrame().waitForNavigation(options); - await this._delegate.reload(); - const response = await waitPromise; + async reload(options: types.NavigateOptions = {}): Promise { + 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 { - 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 { + 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 { - 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 { + 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; }