From 18d6140d3eecec8a06f1bc290c10ff399e6b67c9 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 26 Jun 2020 11:51:47 -0700 Subject: [PATCH] chore(rpc): support routes and bindings (#2725) --- src/helper.ts | 15 ++ src/network.ts | 4 +- src/rpc/channels.ts | 163 +++++++++++---------- src/rpc/client/browserContext.ts | 33 ++++- src/rpc/client/elementHandle.ts | 10 +- src/rpc/client/frame.ts | 12 +- src/rpc/client/network.ts | 28 ++-- src/rpc/client/page.ts | 109 ++++++++++++-- src/rpc/connection.ts | 16 +- src/rpc/dispatcher.ts | 9 +- src/rpc/server/browserContextDispatcher.ts | 26 +++- src/rpc/server/elementHandlerDispatcher.ts | 12 +- src/rpc/server/frameDispatcher.ts | 10 +- src/rpc/server/networkDispatchers.ts | 45 ++++-- src/rpc/server/pageDispatcher.ts | 76 ++++++++-- src/types.ts | 6 + test/test.js | 4 +- 17 files changed, 418 insertions(+), 160 deletions(-) diff --git a/src/helper.ts b/src/helper.ts index 9d3d1f871f..8e64f16a31 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -362,6 +362,21 @@ export function logPolitely(toBeLogged: string) { console.log(toBeLogged); // eslint-disable-line no-console } +export function serializeError(e: any): types.Error { + if (e instanceof Error) + return { message: e.message, stack: e.stack }; + return { value: e }; +} + +export function parseError(error: types.Error): any { + if (error.message !== undefined) { + const e = new Error(error.message); + e.stack = error.stack; + return e; + } + return error.value; +} + const escapeGlobChars = new Set(['/', '$', '^', '+', '.', '(', ')', '=', '!', '|']); export const helper = Helper; diff --git a/src/network.ts b/src/network.ts index 5db62a4750..29436552b0 100644 --- a/src/network.ts +++ b/src/network.ts @@ -76,7 +76,7 @@ export class Request { private _redirectedTo: Request | null = null; readonly _documentId?: string; readonly _isFavicon: boolean; - private _failureText: string | null = null; + _failureText: string | null = null; private _url: string; private _resourceType: string; private _method: string; @@ -182,7 +182,7 @@ export class Request { return this._redirectedTo; } - failure(): { errorText: string; } | null { + failure(): { errorText: string } | null { if (this._failureText === null) return null; return { diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts index 84b70a4948..dc779c9a28 100644 --- a/src/rpc/channels.ts +++ b/src/rpc/channels.ts @@ -24,65 +24,68 @@ export interface Channel extends EventEmitter { } export interface BrowserTypeChannel extends Channel { + connect(params: { options: types.ConnectOptions }): Promise; launch(params: { options?: types.LaunchOptions }): Promise; launchPersistentContext(params: { userDataDir: string, options?: types.LaunchOptions & types.BrowserContextOptions }): Promise; - connect(params: { options: types.ConnectOptions }): Promise; } export interface BrowserChannel extends Channel { + close(): Promise; newContext(params: { options?: types.BrowserContextOptions }): Promise; newPage(params: { options?: types.BrowserContextOptions }): Promise; - close(): Promise; } export interface BrowserContextChannel extends Channel { + addCookies(params: { cookies: types.SetNetworkCookieParam[] }): Promise; + addInitScript(params: { source: string }): Promise; + clearCookies(): Promise; + clearPermissions(): Promise; + close(): Promise; + cookies(params: { urls: string[] }): Promise; + exposeBinding(params: { name: string }): Promise; + grantPermissions(params: { permissions: string[]; options?: { origin?: string } }): Promise; + newPage(): Promise; setDefaultNavigationTimeoutNoReply(params: { timeout: number }): void; setDefaultTimeoutNoReply(params: { timeout: number }): void; - exposeBinding(params: { name: string }): Promise; - newPage(): Promise; - cookies(params: { urls: string[] }): Promise; - addCookies(params: { cookies: types.SetNetworkCookieParam[] }): Promise; - clearCookies(): Promise; - grantPermissions(params: { permissions: string[]; options?: { origin?: string } }): Promise; - clearPermissions(): Promise; - setGeolocation(params: { geolocation: types.Geolocation | null }): Promise; setExtraHTTPHeaders(params: { headers: types.Headers }): Promise; - setOffline(params: { offline: boolean }): Promise; + setGeolocation(params: { geolocation: types.Geolocation | null }): Promise; setHTTPCredentials(params: { httpCredentials: types.Credentials | null }): Promise; - addInitScript(params: { source: string }): Promise; setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise; + setOffline(params: { offline: boolean }): Promise; waitForEvent(params: { event: string }): Promise; - close(): Promise; } export interface PageChannel extends Channel { - 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 }) => void): this; - on(event: 'request', callback: (params: RequestChannel) => void): this; - on(event: 'response', callback: (params: ResponseChannel) => void): this; - on(event: 'requestFinished', callback: (params: RequestChannel) => void): this; - on(event: 'requestFailed', callback: (params: RequestChannel) => void): this; + on(event: 'bindingCall', callback: (params: BindingCallChannel) => void): this; on(event: 'close', callback: () => void): this; on(event: 'console', callback: (params: ConsoleMessageChannel) => 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: 'pageError', callback: (params: { error: types.Error }) => void): this; + on(event: 'request', callback: (params: RequestChannel) => void): this; + on(event: 'requestFailed', callback: (params: { request: RequestChannel, failureText: string | null }) => void): this; + on(event: 'requestFinished', callback: (params: RequestChannel) => void): this; + on(event: 'response', callback: (params: ResponseChannel) => void): this; + on(event: 'route', callback: (params: { route: RouteChannel, request: RequestChannel }) => void): this; setDefaultNavigationTimeoutNoReply(params: { timeout: number }): void; setDefaultTimeoutNoReply(params: { timeout: number }): Promise; setFileChooserInterceptedNoReply(params: { intercepted: boolean }): Promise; - opener(): Promise; + addInitScript(params: { source: string }): Promise; + close(params: { options?: { runBeforeUnload?: boolean } }): Promise; + emulateMedia(params: { options: { media?: 'screen' | 'print', colorScheme?: 'dark' | 'light' | 'no-preference' } }): Promise; exposeBinding(params: { name: string }): Promise; - setExtraHTTPHeaders(params: { headers: types.Headers }): Promise; - reload(params: { options?: types.NavigateOptions }): Promise; - waitForEvent(params: { event: string }): Promise; goBack(params: { options?: types.NavigateOptions }): Promise; goForward(params: { options?: types.NavigateOptions }): Promise; - emulateMedia(params: { options: { media?: 'screen' | 'print', colorScheme?: 'dark' | 'light' | 'no-preference' } }): Promise; - setViewportSize(params: { viewportSize: types.Size }): Promise; - addInitScript(params: { source: string }): Promise; - setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise; + opener(): Promise; + reload(params: { options?: types.NavigateOptions }): Promise; screenshot(params: { options?: types.ScreenshotOptions }): Promise; - close(params: { options?: { runBeforeUnload?: boolean } }): Promise; + setExtraHTTPHeaders(params: { headers: types.Headers }): Promise; + setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise; + setViewportSize(params: { viewportSize: types.Size }): Promise; // Input keyboardDown(params: { key: string }): Promise; @@ -100,87 +103,86 @@ export interface PageChannel extends Channel { } export interface FrameChannel extends Channel { - goto(params: { url: string, options: types.GotoOptions }): Promise; - waitForNavigation(params: { options: types.WaitForNavigationOptions }): Promise; - waitForLoadState(params: { state: types.LifecycleEvent, options: types.TimeoutOptions }): Promise; - frameElement(): Promise; - evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise; - evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise; - querySelector(params: { selector: string }): Promise; - waitForSelector(params: { selector: string, options: types.WaitForElementOptions }): Promise; - dispatchEvent(params: { selector: string, type: string, eventInit: Object | undefined, options: types.TimeoutOptions }): Promise; - $eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise; $$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise; - querySelectorAll(params: { selector: string }): Promise; - content(): Promise; - setContent(params: { html: string, options: types.NavigateOptions }): Promise; + $eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise; addScriptTag(params: { options: { url?: string | undefined, path?: string | undefined, content?: string | undefined, type?: string | undefined } }): Promise; addStyleTag(params: { options: { url?: string | undefined, path?: string | undefined, content?: string | undefined } }): Promise; + check(params: { selector: string, options: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; click(params: { selector: string, options: types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; + content(): Promise; dblclick(params: { selector: string, options: types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions & { force?: boolean }}): Promise; + dispatchEvent(params: { selector: string, type: string, eventInit: Object | undefined, options: types.TimeoutOptions }): Promise; + evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise; + evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise; fill(params: { selector: string, value: string, options: types.NavigatingActionWaitOptions }): Promise; focus(params: { selector: string, options: types.TimeoutOptions }): Promise; - textContent(params: { selector: string, options: types.TimeoutOptions }): Promise; - innerText(params: { selector: string, options: types.TimeoutOptions }): Promise; - innerHTML(params: { selector: string, options: types.TimeoutOptions }): Promise; + frameElement(): Promise; getAttribute(params: { selector: string, name: string, options: types.TimeoutOptions }): Promise; + goto(params: { url: string, options: types.GotoOptions }): Promise; hover(params: { selector: string, options: types.PointerActionOptions & types.TimeoutOptions & { force?: boolean } }): Promise; - selectOption(params: { selector: string, values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions }): Promise; - setInputFiles(params: { selector: string, files: string | string[] | types.FilePayload | types.FilePayload[], options: types.NavigatingActionWaitOptions }): Promise; - type(params: { selector: string, text: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise; + innerHTML(params: { selector: string, options: types.TimeoutOptions }): Promise; + innerText(params: { selector: string, options: types.TimeoutOptions }): Promise; press(params: { selector: string, key: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise; - check(params: { selector: string, options: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; + querySelector(params: { selector: string }): Promise; + querySelectorAll(params: { selector: string }): Promise; + selectOption(params: { selector: string, values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions }): Promise; + setContent(params: { html: string, options: types.NavigateOptions }): Promise; + setInputFiles(params: { selector: string, files: string | string[] | types.FilePayload | types.FilePayload[], options: types.NavigatingActionWaitOptions }): Promise; + textContent(params: { selector: string, options: types.TimeoutOptions }): Promise; + title(): Promise; + type(params: { selector: string, text: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise; uncheck(params: { selector: string, options: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; waitForFunction(params: { expression: string, isFunction: boolean, arg: any; options: types.WaitForFunctionOptions }): Promise; - title(): Promise; + waitForLoadState(params: { state: types.LifecycleEvent, options: types.TimeoutOptions }): Promise; + waitForNavigation(params: { options: types.WaitForNavigationOptions }): Promise; + waitForSelector(params: { selector: string, options: types.WaitForElementOptions }): Promise; } export interface JSHandleChannel extends Channel { + dispose(): Promise; evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise; evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise; getPropertyList(): Promise<{ name: string, value: JSHandleChannel}[]>; jsonValue(): Promise; - dispose(): Promise; } export interface ElementHandleChannel extends JSHandleChannel { - ownerFrame(): Promise; - contentFrame(): Promise; - - getAttribute(params: { name: string }): Promise; - textContent(): Promise; - innerText(): Promise; - innerHTML(): Promise; + $$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise; + $eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise; boundingBox(): Promise; - - hover(params: { options?: types.PointerActionOptions & types.TimeoutOptions & { force?: boolean } }): Promise; - click(params: { options?: types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; - dblclick(params: { options?: types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; - selectOption(params: { values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null; options?: types.NavigatingActionWaitOptions }): string[] | Promise; - fill(params: { value: string; options?: types.NavigatingActionWaitOptions }): Promise; - selectText(params: { options?: types.TimeoutOptions }): Promise; - setInputFiles(params: { files: string | string[] | types.FilePayload | types.FilePayload[], options?: types.NavigatingActionWaitOptions }): Promise; - focus(): Promise; - type(params: { text: string; options?: { delay?: number } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise; - press(params: { key: string; options?: { delay?: number } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise; check(params: { options?: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; - uncheck(params: { options?: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; + click(params: { options?: types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; + contentFrame(): Promise; + dblclick(params: { options?: types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; dispatchEvent(params: { type: string, eventInit: any }): Promise; - - scrollIntoViewIfNeeded(params: { options?: types.TimeoutOptions }): Promise; - screenshot(params: { options?: types.ElementScreenshotOptions }): Promise; - + fill(params: { value: string; options?: types.NavigatingActionWaitOptions }): Promise; + focus(): Promise; + getAttribute(params: { name: string }): Promise; + hover(params: { options?: types.PointerActionOptions & types.TimeoutOptions & { force?: boolean } }): Promise; + innerHTML(): Promise; + innerText(): Promise; + ownerFrame(): Promise; + press(params: { key: string; options?: { delay?: number } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise; querySelector(params: { selector: string }): Promise; querySelectorAll(params: { selector: string }): Promise; - $eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise; - $$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise; + screenshot(params: { options?: types.ElementScreenshotOptions }): Promise; + scrollIntoViewIfNeeded(params: { options?: types.TimeoutOptions }): Promise; + selectOption(params: { values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null; options?: types.NavigatingActionWaitOptions }): string[] | Promise; + selectText(params: { options?: types.TimeoutOptions }): Promise; + setInputFiles(params: { files: string | string[] | types.FilePayload | types.FilePayload[], options?: types.NavigatingActionWaitOptions }): Promise; + textContent(): Promise; + type(params: { text: string; options?: { delay?: number } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise; + uncheck(params: { options?: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; } export interface RequestChannel extends Channel { + response(): Promise; +} + +export interface RouteChannel extends Channel { + abort(params: { errorCode: string }): Promise; continue(params: { overrides: { method?: string, headers?: types.Headers, postData?: string } }): Promise; fulfill(params: { response: types.FulfillResponse & { path?: string } }): Promise; - abort(params: { errorCode: string }): Promise; - response(): Promise; } export interface ResponseChannel extends Channel { @@ -190,3 +192,8 @@ export interface ResponseChannel extends Channel { export interface ConsoleMessageChannel extends Channel { } + +export interface BindingCallChannel extends Channel { + reject(params: { error: types.Error }): void; + resolve(params: { result: any }): void; +} diff --git a/src/rpc/client/browserContext.ts b/src/rpc/client/browserContext.ts index d84f4a6e69..51f7c19146 100644 --- a/src/rpc/client/browserContext.ts +++ b/src/rpc/client/browserContext.ts @@ -16,7 +16,7 @@ */ import * as frames from './frame'; -import { Page } from './page'; +import { Page, BindingCall, waitForEvent } from './page'; import * as types from '../../types'; import * as network from './network'; import { BrowserContextChannel } from '../channels'; @@ -29,6 +29,7 @@ export class BrowserContext extends ChannelOwner { _pages = new Set(); private _routes: { url: types.URLMatch, handler: network.RouteHandler }[] = []; _browser: Browser | undefined; + readonly _bindings = new Map(); static from(context: BrowserContextChannel): BrowserContext { return context._object; @@ -42,7 +43,23 @@ export class BrowserContext extends ChannelOwner { super(connection, channel); } - _initialize() { + _initialize() {} + + _onRoute(route: network.Route, request: network.Request) { + for (const {url, handler} of this._routes) { + if (helper.urlMatches(request.url(), url)) { + handler(route, request); + return; + } + } + route.continue(); + } + + async _onBinding(bindingCall: BindingCall) { + const func = this._bindings.get(bindingCall.name); + if (!func) + return; + bindingCall.call(func); } setDefaultNavigationTimeout(timeout: number) { @@ -106,7 +123,15 @@ export class BrowserContext extends ChannelOwner { await this._channel.addInitScript({ source }); } - async exposeBinding(name: string, playwrightBinding: frames.FunctionWithSource): Promise { + async exposeBinding(name: string, binding: frames.FunctionWithSource): Promise { + for (const page of this.pages()) { + if (page._bindings.has(name)) + throw new Error(`Function "${name}" has been already registered in one of the pages`); + } + if (this._bindings.has(name)) + throw new Error(`Function "${name}" has been already registered`); + this._bindings.set(name, binding); + await this._channel.exposeBinding({ name }); } async exposeFunction(name: string, playwrightFunction: Function): Promise { @@ -126,7 +151,7 @@ export class BrowserContext extends ChannelOwner { } async waitForEvent(event: string, optionsOrPredicate?: Function | (types.TimeoutOptions & { predicate?: Function })): Promise { - return await this._channel.waitForEvent({ event }); + return waitForEvent(this, event, optionsOrPredicate); } async close(): Promise { diff --git a/src/rpc/client/elementHandle.ts b/src/rpc/client/elementHandle.ts index 2928e6e656..5f0151ec4e 100644 --- a/src/rpc/client/elementHandle.ts +++ b/src/rpc/client/elementHandle.ts @@ -85,7 +85,7 @@ export class ElementHandle extends JSHandle { } async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise { - return await this._elementChannel.selectOption({ values: values as any, options }); + return await this._elementChannel.selectOption({ values: convertSelectOptionValues(values), options }); } async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise { @@ -148,3 +148,11 @@ export class ElementHandle extends JSHandle { return await this._elementChannel.$$eval({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); } } + +export function convertSelectOptionValues(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null): string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null { + if (values instanceof ElementHandle) + return values._elementChannel; + if (Array.isArray(values) && values.length && values[0] instanceof ElementHandle) + return (values as ElementHandle[]).map((v: ElementHandle) => v._elementChannel); + return values as any; +} diff --git a/src/rpc/client/frame.ts b/src/rpc/client/frame.ts index e7487b8649..b9f7d9ca04 100644 --- a/src/rpc/client/frame.ts +++ b/src/rpc/client/frame.ts @@ -20,7 +20,7 @@ import * as types from '../../types'; import { FrameChannel } from '../channels'; import { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; -import { ElementHandle } from './elementHandle'; +import { ElementHandle, convertSelectOptionValues } from './elementHandle'; import { JSHandle, Func1, FuncOn, SmartHandle, convertArg } from './jsHandle'; import * as network from './network'; import { Response } from './network'; @@ -36,9 +36,9 @@ export type FunctionWithSource = (source: { context: BrowserContext, page: Page, export class Frame extends ChannelOwner { _parentFrame: Frame | null = null; _url = ''; + _name = ''; private _detached = false; _childFrames = new Set(); - private _name = ''; static from(frame: FrameChannel): Frame { return frame._object; @@ -52,10 +52,12 @@ export class Frame extends ChannelOwner { super(connection, channel); } - _initialize(payload: { parentFrame: FrameChannel | null }) { - this._parentFrame = payload.parentFrame ? payload.parentFrame._object : null; + _initialize(params: { name: string, url: string, parentFrame: FrameChannel | null }) { + this._parentFrame = params.parentFrame ? params.parentFrame._object : null; if (this._parentFrame) this._parentFrame._childFrames.add(this); + this._name = params.name; + this._url = params.url; } async goto(url: string, options: GotoOptions = {}): Promise { @@ -192,7 +194,7 @@ export class Frame extends ChannelOwner { } async selectOption(selector: string, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise { - return await this._channel.selectOption({ selector, values: values as any, options }); + return await this._channel.selectOption({ selector, values: convertSelectOptionValues(values), options }); } async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise { diff --git a/src/rpc/client/network.ts b/src/rpc/client/network.ts index b22bfae3b8..de329d6108 100644 --- a/src/rpc/client/network.ts +++ b/src/rpc/client/network.ts @@ -16,7 +16,7 @@ import { URLSearchParams } from 'url'; import * as types from '../../types'; -import { RequestChannel, ResponseChannel, FrameChannel } from '../channels'; +import { RequestChannel, ResponseChannel, FrameChannel, RouteChannel } from '../channels'; import { ChannelOwner } from './channelOwner'; import { Frame } from './frame'; import { Connection } from '../connection'; @@ -48,7 +48,7 @@ export class Request extends ChannelOwner { private _redirectedFrom: Request | null = null; private _redirectedTo: Request | null = null; private _isNavigationRequest = false; - private _failureText: string | null = null; + _failureText: string | null = null; private _url: string = ''; private _resourceType = ''; private _method = ''; @@ -150,27 +150,35 @@ export class Request extends ChannelOwner { } } -export class Route { - private _request: Request; +export class Route extends ChannelOwner { + private _request: Request | undefined; - constructor(request: Request) { - this._request = request; + static from(route: RouteChannel): Route { + return route._object; + } + + constructor(connection: Connection, channel: RouteChannel) { + super(connection, channel); + } + + _initialize(params: { request: RequestChannel }) { + this._request = Request.from(params.request); } request(): Request { - return this._request; + return this._request!; } async abort(errorCode: string = 'failed') { - await this._request._channel.abort({ errorCode }); + await this._channel.abort({ errorCode }); } async fulfill(response: types.FulfillResponse & { path?: string }) { - await this._request._channel.fulfill({ response }); + await this._channel.fulfill({ response }); } async continue(overrides: { method?: string; headers?: types.Headers; postData?: string } = {}) { - await this._request._channel.continue({ overrides }); + await this._channel.continue({ overrides }); } } diff --git a/src/rpc/client/page.ts b/src/rpc/client/page.ts index c28772f149..7c67647b44 100644 --- a/src/rpc/client/page.ts +++ b/src/rpc/client/page.ts @@ -17,15 +17,15 @@ import { EventEmitter } from 'events'; import { Events } from '../../events'; -import { assert, assertMaxArguments, helper, Listener } from '../../helper'; +import { assert, assertMaxArguments, helper, Listener, serializeError, parseError } from '../../helper'; import * as types from '../../types'; -import { BrowserContextChannel, FrameChannel, PageChannel } from '../channels'; +import { BrowserContextChannel, FrameChannel, PageChannel, BindingCallChannel, Channel } from '../channels'; import { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; import { ElementHandle } from './elementHandle'; import { Frame, FunctionWithSource, GotoOptions } from './frame'; import { Func1, FuncOn, SmartHandle } from './jsHandle'; -import { Request, Response, RouteHandler } from './network'; +import { Request, Response, RouteHandler, Route } from './network'; import { Connection } from '../connection'; import { Keyboard, Mouse } from './input'; import { Accessibility } from './accessibility'; @@ -45,6 +45,7 @@ export class Page extends ChannelOwner { readonly accessibility: Accessibility; readonly keyboard: Keyboard; readonly mouse: Mouse; + readonly _bindings = new Map(); static from(page: PageChannel): Page { return page._object; @@ -67,15 +68,23 @@ export class Page extends ChannelOwner { this._frames.add(this._mainFrame); this._viewportSize = payload.viewportSize; + 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('frameAttached', frame => this._onFrameAttached(Frame.from(frame))); this._channel.on('frameDetached', frame => this._onFrameDetached(Frame.from(frame))); - this._channel.on('frameNavigated', ({ frame, url }) => this._onFrameNavigated(Frame.from(frame), url)); + this._channel.on('frameNavigated', ({ frame, url, name }) => this._onFrameNavigated(Frame.from(frame), url, name)); + this._channel.on('pageError', ({ error }) => this.emit(Events.Page.PageError, parseError(error))); this._channel.on('request', request => this.emit(Events.Page.Request, Request.from(request))); + this._channel.on('requestFailed', ({ request, failureText }) => this._onRequestFailed(Request.from(request), failureText)); + this._channel.on('requestFinished', request => this.emit(Events.Page.RequestFinished, Request.from(request))); this._channel.on('response', response => this.emit(Events.Page.Response, Response.from(response))); - this._channel.on('requestFinished', request => this.emit(Events.Page.Request, Request.from(request))); - this._channel.on('requestFailed', request => this.emit(Events.Page.Request, Request.from(request))); - this._channel.on('console', message => this.emit(Events.Page.Console, ConsoleMessage.from(message))); - this._channel.on('close', () => this._onClose()); + this._channel.on('route', ({ route, request }) => this._onRoute(Route.from(route), Request.from(request))); + } + + private _onRequestFailed(request: Request, failureText: string | null) { + request._failureText = failureText; + this.emit(Events.Page.RequestFailed, request); } private _onFrameAttached(frame: Frame) { @@ -92,11 +101,29 @@ export class Page extends ChannelOwner { this.emit(Events.Page.FrameDetached, frame); } - private _onFrameNavigated(frame: Frame, url: string) { + private _onFrameNavigated(frame: Frame, url: string, name: string) { frame._url = url; + frame._name = name; this.emit(Events.Page.FrameNavigated, frame); } + private _onRoute(route: Route, request: Request) { + for (const {url, handler} of this._routes) { + if (helper.urlMatches(request.url(), url)) { + handler(route, request); + return; + } + } + this._browserContext!._onRoute(route, request); + } + + async _onBinding(bindingCall: BindingCall) { + const func = this._bindings.get(bindingCall.name); + if (func) + bindingCall.call(func); + this._browserContext!._onBinding(bindingCall); + } + private _onClose() { this._browserContext!._pages.delete(this); this.emit(Events.Page.Close); @@ -111,7 +138,7 @@ export class Page extends ChannelOwner { } mainFrame(): Frame { - return this._mainFrame!!; + return this._mainFrame!; } frame(options: string | { name?: string, url?: types.URLMatch }): Frame | null { @@ -186,7 +213,12 @@ export class Page extends ChannelOwner { await this.exposeBinding(name, (options, ...args: any) => playwrightFunction(...args)); } - async exposeBinding(name: string, playwrightBinding: FunctionWithSource) { + async exposeBinding(name: string, binding: FunctionWithSource) { + if (this._bindings.has(name)) + throw new Error(`Function "${name}" has been already registered`); + if (this._browserContext!._bindings.has(name)) + throw new Error(`Function "${name}" has been already registered in the browser context`); + this._bindings.set(name, binding); await this._channel.exposeBinding({ name }); } @@ -241,9 +273,7 @@ export class Page extends ChannelOwner { } async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise { - const result = await this._channel.waitForEvent({ event }); - if (result._object) - return result._object; + return waitForEvent(this, event, optionsOrPredicate); } async goBack(options?: types.NavigateOptions): Promise { @@ -424,3 +454,54 @@ export class Worker extends EventEmitter { return await this._channel.evaluateHandle({ pageFunction, arg }); } } + +export class BindingCall extends ChannelOwner { + name: string = ''; + source: { context: BrowserContext, page: Page, frame: Frame } | undefined; + args: any[] = []; + static from(channel: BindingCallChannel): BindingCall { + return channel._object; + } + + constructor(connection: Connection, channel: BindingCallChannel) { + super(connection, channel); + } + + _initialize(params: { name: string, context: BrowserContextChannel, page: PageChannel, frame: FrameChannel, args: any[] }) { + this.name = params.name; + this.source = { + context: BrowserContext.from(params.context), + page: Page.from(params.page), + frame: Frame.from(params.frame) + }; + this.args = params.args; + } + + async call(func: FunctionWithSource) { + try { + this._channel.resolve({ result: await func(this.source!, ...this.args) }); + } catch (e) { + this._channel.reject({ error: serializeError(e) }); + } + } +} + +export async function waitForEvent(emitter: EventEmitter, event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise { + // TODO: support timeout + let predicate: Function | undefined; + if (typeof optionsOrPredicate === 'function') + predicate = optionsOrPredicate; + else if (optionsOrPredicate.predicate) + 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; + if (predicate && !predicate(object)) + return; + callback(object); + helper.removeEventListeners([listener]); + }); + return result; +} diff --git a/src/rpc/connection.ts b/src/rpc/connection.ts index f7ccca6301..ba6dac4d7e 100644 --- a/src/rpc/connection.ts +++ b/src/rpc/connection.ts @@ -22,8 +22,8 @@ import { ChannelOwner } from './client/channelOwner'; import { ElementHandle } from './client/elementHandle'; import { Frame } from './client/frame'; import { JSHandle } from './client/jsHandle'; -import { Request, Response } from './client/network'; -import { Page } from './client/page'; +import { Request, Response, Route } from './client/network'; +import { Page, BindingCall } from './client/page'; import debug = require('debug'); import { Channel } from './channels'; import { ConsoleMessage } from './client/console'; @@ -60,6 +60,12 @@ export class Connection { case 'response': result = new Response(this, channel); break; + case 'route': + result = new Route(this, channel); + break; + case 'bindingCall': + result = new BindingCall(this, channel); + break; case 'jsHandle': result = new JSHandle(this, channel); break; @@ -132,6 +138,9 @@ export class Connection { return payload.map(p => this._replaceChannelsWithGuids(p)); if (payload._guid) return { guid: payload._guid }; + // TODO: send base64 + if (payload instanceof Buffer) + return payload; if (typeof payload === 'object') return Object.fromEntries([...Object.entries(payload)].map(([n,v]) => [n, this._replaceChannelsWithGuids(v)])); return payload; @@ -144,6 +153,9 @@ export class Connection { return payload.map(p => this._replaceGuidsWithChannels(p)); if (payload.guid && this._channels.has(payload.guid)) return this._channels.get(payload.guid); + // TODO: send base64 + if (payload instanceof Buffer) + return payload; if (typeof payload === 'object') return Object.fromEntries([...Object.entries(payload)].map(([n,v]) => [n, this._replaceGuidsWithChannels(v)])); return payload; diff --git a/src/rpc/dispatcher.ts b/src/rpc/dispatcher.ts index c731caa2fe..c414893e64 100644 --- a/src/rpc/dispatcher.ts +++ b/src/rpc/dispatcher.ts @@ -56,8 +56,7 @@ export class DispatcherScope { async dispatchMessageFromClient(message: any): Promise { const dispatcher = this.dispatchers.get(message.guid)!; const value = await (dispatcher as any)[message.method](this._replaceGuidsWithDispatchers(message.params)); - const result = this._replaceDispatchersWithGuids(value); - return result; + return this._replaceDispatchersWithGuids(value); } private _replaceDispatchersWithGuids(payload: any): any { @@ -67,6 +66,9 @@ export class DispatcherScope { return { guid: payload._guid }; if (Array.isArray(payload)) return payload.map(p => this._replaceDispatchersWithGuids(p)); + // TODO: send base64 + if (payload instanceof Buffer) + return payload; if (typeof payload === 'object') return Object.fromEntries([...Object.entries(payload)].map(([n,v]) => [n, this._replaceDispatchersWithGuids(v)])); return payload; @@ -79,6 +81,9 @@ export class DispatcherScope { return payload.map(p => this._replaceGuidsWithDispatchers(p)); if (payload.guid && this.dispatchers.has(payload.guid)) return this.dispatchers.get(payload.guid); + // TODO: send base64 + if (payload instanceof Buffer) + return payload; if (typeof payload === 'object') return Object.fromEntries([...Object.entries(payload)].map(([n,v]) => [n, this._replaceGuidsWithDispatchers(v)])); return payload; diff --git a/src/rpc/server/browserContextDispatcher.ts b/src/rpc/server/browserContextDispatcher.ts index 4984a885cd..4694cf3194 100644 --- a/src/rpc/server/browserContextDispatcher.ts +++ b/src/rpc/server/browserContextDispatcher.ts @@ -15,20 +15,22 @@ */ import * as types from '../../types'; -import { BrowserContextBase } from '../../browserContext'; +import { BrowserContextBase, BrowserContext } from '../../browserContext'; import { Events } from '../../events'; import { BrowserDispatcher } from './browserDispatcher'; import { Dispatcher, DispatcherScope } from '../dispatcher'; -import { PageDispatcher } from './pageDispatcher'; +import { PageDispatcher, BindingCallDispatcher } from './pageDispatcher'; import { PageChannel, BrowserContextChannel } from '../channels'; +import { RouteDispatcher, RequestDispatcher } from './networkDispatchers'; +import { Page } from '../../page'; export class BrowserContextDispatcher extends Dispatcher implements BrowserContextChannel { private _context: BrowserContextBase; - static from(scope: DispatcherScope, browserContext: BrowserContextBase): BrowserContextDispatcher { + static from(scope: DispatcherScope, browserContext: BrowserContext): BrowserContextDispatcher { if ((browserContext as any)[scope.dispatcherSymbol]) return (browserContext as any)[scope.dispatcherSymbol]; - return new BrowserContextDispatcher(scope, browserContext); + return new BrowserContextDispatcher(scope, browserContext as BrowserContextBase); } constructor(scope: DispatcherScope, context: BrowserContextBase) { @@ -52,6 +54,11 @@ export class BrowserContextDispatcher extends Dispatcher implements BrowserConte } async exposeBinding(params: { name: string }): Promise { + this._context.exposeBinding(params.name, (source, ...args) => { + const bindingCall = new BindingCallDispatcher(this._scope, params.name, source, args); + this._dispatchEvent('bindingCall', bindingCall); + return bindingCall.promise(); + }); } async newPage(): Promise { @@ -99,9 +106,20 @@ export class BrowserContextDispatcher extends Dispatcher implements BrowserConte } async setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise { + if (!params.enabled) { + await this._context.unroute('**/*'); + return; + } + this._context.route('**/*', (route, request) => { + this._dispatchEvent('route', { route: RouteDispatcher.from(this._scope, route), request: RequestDispatcher.from(this._scope, request) }); + }); } async waitForEvent(params: { event: string }): Promise { + const result = await this._context.waitForEvent(params.event); + if (result instanceof Page) + return PageDispatcher.from(this._scope, result); + return result; } async close(): Promise { diff --git a/src/rpc/server/elementHandlerDispatcher.ts b/src/rpc/server/elementHandlerDispatcher.ts index 6c63dbae31..19b1c91012 100644 --- a/src/rpc/server/elementHandlerDispatcher.ts +++ b/src/rpc/server/elementHandlerDispatcher.ts @@ -23,7 +23,7 @@ import { convertArg, FrameDispatcher } from './frameDispatcher'; import { JSHandleDispatcher } from './jsHandleDispatcher'; export class ElementHandleDispatcher extends JSHandleDispatcher implements ElementHandleChannel { - private _elementHandle: ElementHandle; + readonly _elementHandle: ElementHandle; static from(scope: DispatcherScope, handle: js.JSHandle): JSHandleDispatcher { if ((handle as any)[scope.dispatcherSymbol]) @@ -102,7 +102,7 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme } async selectOption(params: { values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions }): Promise { - return this._elementHandle.selectOption(params.values as any, params.options); + return this._elementHandle.selectOption(convertSelectOptionValues(params.values), params.options); } async fill(params: { value: string, options: types.NavigatingActionWaitOptions }) { @@ -162,3 +162,11 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements Eleme return this._elementHandle._$$evalExpression(params.selector, params.expression, params.isFunction, convertArg(this._scope, params.arg)); } } + +export function convertSelectOptionValues(values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null): string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null { + if (values instanceof ElementHandleDispatcher) + return values._elementHandle; + if (Array.isArray(values) && values.length && values[0] instanceof ElementHandle) + return (values as ElementHandleDispatcher[]).map((v: ElementHandleDispatcher) => v._elementHandle); + return values as any; +} diff --git a/src/rpc/server/frameDispatcher.ts b/src/rpc/server/frameDispatcher.ts index 105bf7dc23..ef1f3e36b6 100644 --- a/src/rpc/server/frameDispatcher.ts +++ b/src/rpc/server/frameDispatcher.ts @@ -18,10 +18,9 @@ import { Frame } from '../../frames'; import * as types from '../../types'; import { ElementHandleChannel, FrameChannel, JSHandleChannel, ResponseChannel } from '../channels'; import { Dispatcher, DispatcherScope } from '../dispatcher'; -import { ElementHandleDispatcher } from './elementHandlerDispatcher'; +import { ElementHandleDispatcher, convertSelectOptionValues } from './elementHandlerDispatcher'; import { JSHandleDispatcher } from './jsHandleDispatcher'; import { ResponseDispatcher } from './networkDispatchers'; -import { PageDispatcher } from './pageDispatcher'; export class FrameDispatcher extends Dispatcher implements FrameChannel { private _frame: Frame; @@ -43,12 +42,9 @@ export class FrameDispatcher extends Dispatcher implements FrameChannel { this._frame = frame; const parentFrame = frame.parentFrame(); this._initialize({ - page: PageDispatcher.from(this._scope, frame._page), url: frame.url(), name: frame.name(), - parentFrame: FrameDispatcher.fromNullable(this._scope, parentFrame), - childFrame: frame.childFrames().map(f => FrameDispatcher.from(this._scope, f)), - isDetached: frame.isDetached() + parentFrame: FrameDispatcher.fromNullable(this._scope, parentFrame) }); } @@ -154,7 +150,7 @@ export class FrameDispatcher extends Dispatcher implements FrameChannel { } async selectOption(params: { selector: string, values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions }): Promise { - return this._frame.selectOption(params.selector, params.values as any, params.options); + return this._frame.selectOption(params.selector, convertSelectOptionValues(params.values), params.options); } async setInputFiles(params: { selector: string, files: string | string[] | types.FilePayload | types.FilePayload[], options: types.NavigatingActionWaitOptions }): Promise { diff --git a/src/rpc/server/networkDispatchers.ts b/src/rpc/server/networkDispatchers.ts index 0b79054b90..9111bdb673 100644 --- a/src/rpc/server/networkDispatchers.ts +++ b/src/rpc/server/networkDispatchers.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { Request, Response } from '../../network'; +import { Request, Response, Route } from '../../network'; import * as types from '../../types'; -import { RequestChannel, ResponseChannel } from '../channels'; +import { RequestChannel, ResponseChannel, RouteChannel } from '../channels'; import { Dispatcher, DispatcherScope } from '../dispatcher'; import { FrameDispatcher } from './frameDispatcher'; @@ -50,15 +50,6 @@ export class RequestDispatcher extends Dispatcher implements RequestChannel { this._request = request; } - async continue(params: { overrides: { method?: string, headers?: types.Headers, postData?: string } }): Promise { - } - - async fulfill(params: { response: types.FulfillResponse & { path?: string } }): Promise { - } - - async abort(params: { errorCode: string }): Promise { - } - async response(): Promise { return ResponseDispatcher.fromNullable(this._scope, await this._request.response()); } @@ -99,3 +90,35 @@ export class ResponseDispatcher extends Dispatcher implements ResponseChannel { return await this._response.body(); } } + +export class RouteDispatcher extends Dispatcher implements RouteChannel { + private _route: Route; + + static from(scope: DispatcherScope, route: Route): RouteDispatcher { + if ((route as any)[scope.dispatcherSymbol]) + return (route as any)[scope.dispatcherSymbol]; + return new RouteDispatcher(scope, route); + } + + static fromNullable(scope: DispatcherScope, route: Route | null): RouteDispatcher | null { + return route ? RouteDispatcher.from(scope, route) : null; + } + + constructor(scope: DispatcherScope, route: Route) { + super(scope, route, 'route'); + this._initialize({ request: RequestDispatcher.from(this._scope, route.request()) }); + this._route = route; + } + + async continue(params: { overrides: { method?: string, headers?: types.Headers, postData?: string } }): Promise { + await this._route.continue(params.overrides); + } + + async fulfill(params: { response: types.FulfillResponse & { path?: string } }): Promise { + await this._route.fulfill(params.response); + } + + async abort(params: { errorCode: string }): Promise { + await this._route.abort(params.errorCode); + } +} diff --git a/src/rpc/server/pageDispatcher.ts b/src/rpc/server/pageDispatcher.ts index f3ddc30f54..4d1f7a59b0 100644 --- a/src/rpc/server/pageDispatcher.ts +++ b/src/rpc/server/pageDispatcher.ts @@ -14,17 +14,19 @@ * limitations under the License. */ -import { ConsoleMessage } from '../../console'; import { Events } from '../../events'; +import { Request } from '../../network'; import { Frame } from '../../frames'; import { Page } from '../../page'; import * as types from '../../types'; -import { ElementHandleChannel, PageChannel, ResponseChannel } from '../channels'; +import { ElementHandleChannel, PageChannel, ResponseChannel, BindingCallChannel } from '../channels'; import { Dispatcher, DispatcherScope } from '../dispatcher'; import { BrowserContextDispatcher } from './browserContextDispatcher'; import { FrameDispatcher } from './frameDispatcher'; -import { RequestDispatcher, ResponseDispatcher } from './networkDispatchers'; +import { RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers'; import { ConsoleMessageDispatcher } from './consoleMessageDispatcher'; +import { BrowserContext } from '../../browserContext'; +import { serializeError, parseError } from '../../helper'; export class PageDispatcher extends Dispatcher implements PageChannel { private _page: Page; @@ -49,17 +51,19 @@ export class PageDispatcher extends Dispatcher implements PageChannel { frames: page.frames().map(f => FrameDispatcher.from(this._scope, f)), }); 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.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.Close, () => { - this._dispatchEvent('close'); - }); + page.on(Events.Page.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) })); page.on(Events.Page.Request, request => this._dispatchEvent('request', RequestDispatcher.from(this._scope, request))); + page.on(Events.Page.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', { + request: RequestDispatcher.from(this._scope, request), + failureText: request._failureText + })); + page.on(Events.Page.RequestFinished, request => this._dispatchEvent('requestFinished', RequestDispatcher.from(this._scope, request))); page.on(Events.Page.Response, response => this._dispatchEvent('response', ResponseDispatcher.from(this._scope, response))); - page.on(Events.Page.RequestFinished, request => this._dispatchEvent('requestFinished', ResponseDispatcher.from(this._scope, request))); - page.on(Events.Page.RequestFailed, request => this._dispatchEvent('requestFailed', ResponseDispatcher.from(this._scope, request))); - page.on(Events.Page.Console, message => this._dispatchEvent('console', ConsoleMessageDispatcher.from(this._scope, message))); } async setDefaultNavigationTimeoutNoReply(params: { timeout: number }) { @@ -75,6 +79,11 @@ export class PageDispatcher extends Dispatcher implements PageChannel { } async exposeBinding(params: { name: string }): Promise { + this._page.exposeBinding(params.name, (source, ...args) => { + const bindingCall = new BindingCallDispatcher(this._scope, params.name, source, args); + this._dispatchEvent('bindingCall', bindingCall); + return bindingCall.promise(); + }); } async setExtraHTTPHeaders(params: { headers: types.Headers }): Promise { @@ -85,12 +94,6 @@ export class PageDispatcher extends Dispatcher implements PageChannel { return ResponseDispatcher.fromNullable(this._scope, await this._page.reload(params.options)); } - async waitForEvent(params: { event: string }): Promise { - const result = await this._page.waitForEvent(params.event); - if (result instanceof ConsoleMessage) - return ConsoleMessageDispatcher.from(this._scope, result); - } - async goBack(params: { options?: types.NavigateOptions }): Promise { return ResponseDispatcher.fromNullable(this._scope, await this._page.goBack(params.options)); } @@ -112,6 +115,13 @@ export class PageDispatcher extends Dispatcher implements PageChannel { } async setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise { + if (!params.enabled) { + await this._page.unroute('**/*'); + return; + } + this._page.route('**/*', (route, request) => { + this._dispatchEvent('route', { route: RouteDispatcher.from(this._scope, route), request: RequestDispatcher.from(this._scope, request) }); + }); } async screenshot(params: { options?: types.ScreenshotOptions }): Promise { @@ -177,10 +187,44 @@ export class PageDispatcher extends Dispatcher implements PageChannel { } _onFrameNavigated(frame: Frame) { - this._dispatchEvent('frameNavigated', { frame: FrameDispatcher.from(this._scope, frame), url: frame.url() }); + this._dispatchEvent('frameNavigated', { frame: FrameDispatcher.from(this._scope, frame), url: frame.url(), name: frame.name() }); } _onFrameDetached(frame: Frame) { this._dispatchEvent('frameDetached', FrameDispatcher.from(this._scope, frame)); } } + + +export class BindingCallDispatcher extends Dispatcher implements BindingCallChannel { + private _resolve: ((arg: any) => void) | undefined; + private _reject: ((error: any) => void) | undefined; + private _promise: Promise; + + constructor(scope: DispatcherScope, name: string, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) { + super(scope, {}, 'bindingCall'); + this._initialize({ + name, + context: BrowserContextDispatcher.from(scope, source.context), + page: PageDispatcher.from(scope, source.page), + frame: FrameDispatcher.from(scope, source.frame), + args + }); + this._promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + } + + promise() { + return this._promise; + } + + resolve(params: { result: any }) { + this._resolve!(params.result); + } + + reject(params: { error: types.Error }) { + this._reject!(parseError(params.error)); + } +} diff --git a/src/types.ts b/src/types.ts index 30ade8652b..97b2e558b5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -305,3 +305,9 @@ export type ConsoleMessageLocation = { lineNumber?: number, columnNumber?: number, }; + +export type Error = { + message?: string, + stack?: string, + value?: any +}; diff --git a/test/test.js b/test/test.js index c9565cddc3..fe85ade175 100644 --- a/test/test.js +++ b/test/test.js @@ -115,8 +115,8 @@ function collect(browserNames) { const browserType = playwright[browserName]; let overridenBrowserType = browserType; - // Channel substitute - if (process.env.PWCHANNEL) { + // Channel substitute + if (process.env.PWCHANNEL) { BrowserTypeDispatcher.from(dispatcherScope, browserType); overridenBrowserType = connection.createRemoteObject('browserType', browserType.name()); }