From db12ddebb31c7229b9e4680cc8ab54930ebdff2b Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 26 Jun 2020 21:22:03 -0700 Subject: [PATCH] chore(rpc): clear the page test spec (#2736) --- src/rpc/channels.ts | 3 ++ src/rpc/client/browserContext.ts | 24 +++++++-- src/rpc/client/page.ts | 63 ++++++++++++++++++---- src/rpc/server/browserContextDispatcher.ts | 2 +- src/rpc/server/pageDispatcher.ts | 3 ++ test/test.js | 26 ++++----- 6 files changed, 92 insertions(+), 29 deletions(-) diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts index e576346ec5..587120456e 100644 --- a/src/rpc/channels.ts +++ b/src/rpc/channels.ts @@ -70,12 +70,15 @@ export interface PageChannel extends Channel { on(event: 'bindingCall', callback: (params: BindingCallChannel) => void): this; on(event: 'close', callback: () => void): this; on(event: 'console', callback: (params: ConsoleMessageChannel) => void): this; + on(event: 'crash', callback: () => void): this; on(event: 'dialog', callback: (params: DialogChannel) => void): this; on(event: 'download', callback: (params: DownloadChannel) => void): this; + on(event: 'domcontentloaded', callback: () => void): this; on(event: 'frameAttached', callback: (params: FrameChannel) => void): this; on(event: 'frameDetached', callback: (params: FrameChannel) => void): this; on(event: 'frameNavigated', callback: (params: { frame: FrameChannel, url: string, name: string }) => void): this; on(event: 'frameNavigated', callback: (params: { frame: FrameChannel, url: string, name: string }) => void): this; + on(event: 'load', callback: () => void): this; on(event: 'pageError', callback: (params: { error: types.Error }) => void): this; on(event: 'popup', callback: (params: PageChannel) => void): this; on(event: 'request', callback: (params: RequestChannel) => void): this; diff --git a/src/rpc/client/browserContext.ts b/src/rpc/client/browserContext.ts index 856841f4b1..1c61202155 100644 --- a/src/rpc/client/browserContext.ts +++ b/src/rpc/client/browserContext.ts @@ -25,12 +25,15 @@ import { helper } from '../../helper'; import { Browser } from './browser'; import { Connection } from '../connection'; import { Events } from '../../events'; +import { TimeoutSettings } from '../../timeoutSettings'; export class BrowserContext extends ChannelOwner { _pages = new Set(); private _routes: { url: types.URLMatch, handler: network.RouteHandler }[] = []; _browser: Browser | undefined; readonly _bindings = new Map(); + private _pendingWaitForEvents = new Map<(error: Error) => void, string>(); + _timeoutSettings = new TimeoutSettings(); static from(context: BrowserContextChannel): BrowserContext { return context._object; @@ -45,13 +48,13 @@ export class BrowserContext extends ChannelOwner { const page = Page.from(p); this._pages.add(page); - page._browserContext = this; + page._setBrowserContext(this); }); channel.on('page', page => this._onPage(Page.from(page))); } private _onPage(page: Page): void { - page._browserContext = this; + page._setBrowserContext(this); this._pages.add(page); this.emit(Events.BrowserContext.Page, page); } @@ -78,6 +81,7 @@ export class BrowserContext extends ChannelOwner { - return waitForEvent(this, event, optionsOrPredicate); + const hasTimeout = optionsOrPredicate && !(optionsOrPredicate instanceof Function); + let reject: () => void; + const result = await Promise.race([ + waitForEvent(this, event, optionsOrPredicate, this._timeoutSettings.timeout(hasTimeout ? optionsOrPredicate as any : {})), + new Promise((f, r) => { reject = r; this._pendingWaitForEvents.set(reject, event); }) + ]); + this._pendingWaitForEvents.delete(reject!); + return result; } async close(): Promise { await this._channel.close(); if (this._browser) this._browser._contexts.delete(this); + + for (const [listener, event] of this._pendingWaitForEvents) { + if (event === Events.Page.Close) + continue; + listener(new Error('Context closed')); + } + this._pendingWaitForEvents.clear(); this.emit(Events.BrowserContext.Close); } } diff --git a/src/rpc/client/page.ts b/src/rpc/client/page.ts index 1a7830ce67..00cd4ce13f 100644 --- a/src/rpc/client/page.ts +++ b/src/rpc/client/page.ts @@ -32,11 +32,13 @@ import { Accessibility } from './accessibility'; import { ConsoleMessage } from './consoleMessage'; import { Dialog } from './dialog'; import { Download } from './download'; +import { TimeoutError } from '../../errors'; +import { TimeoutSettings } from '../../timeoutSettings'; export class Page extends ChannelOwner { readonly pdf: ((options?: types.PDFOptions) => Promise) | undefined; - _browserContext: BrowserContext | undefined; + private _browserContext: BrowserContext | undefined; _ownedContext: BrowserContext | undefined; private _mainFrame: Frame; @@ -50,6 +52,8 @@ export class Page extends ChannelOwner { readonly keyboard: Keyboard; readonly mouse: Mouse; readonly _bindings = new Map(); + private _pendingWaitForEvents = new Map<(error: Error) => void, string>(); + private _timeoutSettings = new TimeoutSettings(); static from(page: PageChannel): Page { return page._object; @@ -73,11 +77,14 @@ export class Page extends ChannelOwner { this._channel.on('bindingCall', bindingCall => this._onBinding(BindingCall.from(bindingCall))); this._channel.on('close', () => this._onClose()); this._channel.on('console', message => this.emit(Events.Page.Console, ConsoleMessage.from(message))); + this._channel.on('crash', () => this._onCrash()); this._channel.on('dialog', dialog => this.emit(Events.Page.Dialog, Dialog.from(dialog))); + this._channel.on('domcontentloaded', () => this.emit(Events.Page.DOMContentLoaded)); this._channel.on('download', download => this.emit(Events.Page.Download, Download.from(download))); this._channel.on('frameAttached', frame => this._onFrameAttached(Frame.from(frame))); this._channel.on('frameDetached', frame => this._onFrameDetached(Frame.from(frame))); this._channel.on('frameNavigated', ({ frame, url, name }) => this._onFrameNavigated(Frame.from(frame), url, name)); + this._channel.on('load', () => this.emit(Events.Page.Load)); this._channel.on('pageError', ({ error }) => this.emit(Events.Page.PageError, parseError(error))); this._channel.on('popup', popup => this.emit(Events.Page.Popup, Page.from(popup))); this._channel.on('request', request => this.emit(Events.Page.Request, Request.from(request))); @@ -87,6 +94,11 @@ export class Page extends ChannelOwner { this._channel.on('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request))); } + _setBrowserContext(context: BrowserContext) { + this._browserContext = context; + this._timeoutSettings = new TimeoutSettings(context._timeoutSettings); + } + private _onRequestFailed(request: Request, failureText: string | null) { request._failureText = failureText; this.emit(Events.Page.RequestFailed, request); @@ -132,10 +144,28 @@ export class Page extends ChannelOwner { } private _onClose() { + this._closed = true; this._browserContext!._pages.delete(this); + this._rejectPendingOperations(false); this.emit(Events.Page.Close); } + private _onCrash() { + this._rejectPendingOperations(true); + this.emit(Events.Page.Crash); + } + + private _rejectPendingOperations(isCrash: boolean) { + for (const [listener, event] of this._pendingWaitForEvents) { + if (event === Events.Page.Close && !isCrash) + continue; + if (event === Events.Page.Crash && isCrash) + continue; + listener(new Error(isCrash ? 'Page crashed' : 'Page closed')); + } + this._pendingWaitForEvents.clear(); + } + context(): BrowserContext { return this._browserContext!; } @@ -168,6 +198,7 @@ export class Page extends ChannelOwner { } setDefaultTimeout(timeout: number) { + this._timeoutSettings.setDefaultTimeout(timeout); this._channel.setDefaultTimeoutNoReply({ timeout }); } @@ -280,7 +311,13 @@ export class Page extends ChannelOwner { } async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise { - return waitForEvent(this, event, optionsOrPredicate); + let reject: () => void; + const result = await Promise.race([ + waitForEvent(this, event, optionsOrPredicate, this._timeoutSettings.timeout(optionsOrPredicate instanceof Function ? {} : optionsOrPredicate)), + new Promise((f, r) => { reject = r; this._pendingWaitForEvents.set(reject, event); }) + ]); + this._pendingWaitForEvents.delete(reject!); + return result; } async goBack(options?: types.NavigateOptions): Promise { @@ -477,7 +514,7 @@ export class BindingCall extends ChannelOwner { - // TODO: support timeout +export async function waitForEvent(emitter: EventEmitter, event: string, optionsOrPredicate: types.WaitForEventOptions = {}, defaultTimeout: number): Promise { let predicate: Function | undefined; - if (typeof optionsOrPredicate === 'function') + let timeout = defaultTimeout; + if (typeof optionsOrPredicate === 'function') { predicate = optionsOrPredicate; - else if (optionsOrPredicate.predicate) + } else if (optionsOrPredicate.predicate) { + if (optionsOrPredicate.timeout !== undefined) + timeout = optionsOrPredicate.timeout; predicate = optionsOrPredicate.predicate; + } let callback: (a: any) => void; const result = new Promise(f => callback = f); const listener = helper.addEventListener(emitter, event, param => { // TODO: do not detect channel by guid. - const object = param._guid ? (param as Channel)._object : param; + const object = param && param._guid ? (param as Channel)._object : param; if (predicate && !predicate(object)) return; callback(object); helper.removeEventListeners([listener]); }); - return result; + if (timeout === 0) + return result; + return Promise.race([ + result, + new Promise((f, r) => setTimeout(() => r(new TimeoutError('Timeout while waiting for event')), timeout)) + ]); } diff --git a/src/rpc/server/browserContextDispatcher.ts b/src/rpc/server/browserContextDispatcher.ts index a1587e1ebb..d57a886e4d 100644 --- a/src/rpc/server/browserContextDispatcher.ts +++ b/src/rpc/server/browserContextDispatcher.ts @@ -120,6 +120,6 @@ export class BrowserContextDispatcher extends Dispatcher { - this._context.close(); + await this._context.close(); } } diff --git a/src/rpc/server/pageDispatcher.ts b/src/rpc/server/pageDispatcher.ts index 629c50c2e8..a2838b66ea 100644 --- a/src/rpc/server/pageDispatcher.ts +++ b/src/rpc/server/pageDispatcher.ts @@ -52,11 +52,14 @@ export class PageDispatcher extends Dispatcher implements this._page = page; page.on(Events.Page.Close, () => this._dispatchEvent('close')); page.on(Events.Page.Console, message => this._dispatchEvent('console', ConsoleMessageDispatcher.from(this._scope, message))); + page.on(Events.Page.Crash, () => this._dispatchEvent('crash')); + page.on(Events.Page.DOMContentLoaded, () => this._dispatchEvent('domcontentloaded')); page.on(Events.Page.Dialog, dialog => this._dispatchEvent('dialog', DialogDispatcher.from(this._scope, dialog))); page.on(Events.Page.Download, dialog => this._dispatchEvent('download', DownloadDispatcher.from(this._scope, dialog))); page.on(Events.Page.FrameAttached, frame => this._onFrameAttached(frame)); page.on(Events.Page.FrameDetached, frame => this._onFrameDetached(frame)); page.on(Events.Page.FrameNavigated, frame => this._onFrameNavigated(frame)); + page.on(Events.Page.Load, () => this._dispatchEvent('load')); page.on(Events.Page.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) })); page.on(Events.Page.Popup, page => this._dispatchEvent('popup', PageDispatcher.from(this._scope, page))); page.on(Events.Page.Request, request => this._dispatchEvent('request', RequestDispatcher.from(this._scope, request))); diff --git a/test/test.js b/test/test.js index 74138a3e04..e0006a0509 100644 --- a/test/test.js +++ b/test/test.js @@ -95,22 +95,6 @@ function collect(browserNames) { global.playwright = playwright; - // Channel substitute - let connection; - let dispatcherScope; - if (process.env.PWCHANNEL) { - dispatcherScope = new DispatcherScope(); - connection = new Connection(); - dispatcherScope.sendMessageToClientTransport = async message => { - setImmediate(() => connection.dispatchMessageFromServer(message)); - }; - connection.sendMessageToServerTransport = async message => { - const result = await dispatcherScope.dispatchMessageFromClient(message); - await new Promise(f => setImmediate(f)); - return result; - }; - } - for (const browserName of browserNames) { const browserType = playwright[browserName]; @@ -119,6 +103,16 @@ function collect(browserNames) { // Channel substitute let overridenBrowserType = browserType; if (process.env.PWCHANNEL) { + const dispatcherScope = new DispatcherScope(); + const connection = new Connection(); + dispatcherScope.sendMessageToClientTransport = async message => { + setImmediate(() => connection.dispatchMessageFromServer(message)); + }; + connection.sendMessageToServerTransport = async message => { + const result = await dispatcherScope.dispatchMessageFromClient(message); + await new Promise(f => setImmediate(f)); + return result; + }; BrowserTypeDispatcher.from(dispatcherScope, browserType); overridenBrowserType = await connection.waitForObjectWithKnownName(browserType.name()); }