chore(rpc): support routes and bindings (#2725)

This commit is contained in:
Pavel Feldman 2020-06-26 11:51:47 -07:00 committed by GitHub
parent 064a0a1154
commit 18d6140d3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 418 additions and 160 deletions

View file

@ -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;

View file

@ -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 {

View file

@ -24,65 +24,68 @@ export interface Channel extends EventEmitter {
}
export interface BrowserTypeChannel extends Channel {
connect(params: { options: types.ConnectOptions }): Promise<BrowserChannel>;
launch(params: { options?: types.LaunchOptions }): Promise<BrowserChannel>;
launchPersistentContext(params: { userDataDir: string, options?: types.LaunchOptions & types.BrowserContextOptions }): Promise<BrowserContextChannel>;
connect(params: { options: types.ConnectOptions }): Promise<BrowserChannel>;
}
export interface BrowserChannel extends Channel {
close(): Promise<void>;
newContext(params: { options?: types.BrowserContextOptions }): Promise<BrowserContextChannel>;
newPage(params: { options?: types.BrowserContextOptions }): Promise<PageChannel>;
close(): Promise<void>;
}
export interface BrowserContextChannel extends Channel {
addCookies(params: { cookies: types.SetNetworkCookieParam[] }): Promise<void>;
addInitScript(params: { source: string }): Promise<void>;
clearCookies(): Promise<void>;
clearPermissions(): Promise<void>;
close(): Promise<void>;
cookies(params: { urls: string[] }): Promise<types.NetworkCookie[]>;
exposeBinding(params: { name: string }): Promise<void>;
grantPermissions(params: { permissions: string[]; options?: { origin?: string } }): Promise<void>;
newPage(): Promise<PageChannel>;
setDefaultNavigationTimeoutNoReply(params: { timeout: number }): void;
setDefaultTimeoutNoReply(params: { timeout: number }): void;
exposeBinding(params: { name: string }): Promise<void>;
newPage(): Promise<PageChannel>;
cookies(params: { urls: string[] }): Promise<types.NetworkCookie[]>;
addCookies(params: { cookies: types.SetNetworkCookieParam[] }): Promise<void>;
clearCookies(): Promise<void>;
grantPermissions(params: { permissions: string[]; options?: { origin?: string } }): Promise<void>;
clearPermissions(): Promise<void>;
setGeolocation(params: { geolocation: types.Geolocation | null }): Promise<void>;
setExtraHTTPHeaders(params: { headers: types.Headers }): Promise<void>;
setOffline(params: { offline: boolean }): Promise<void>;
setGeolocation(params: { geolocation: types.Geolocation | null }): Promise<void>;
setHTTPCredentials(params: { httpCredentials: types.Credentials | null }): Promise<void>;
addInitScript(params: { source: string }): Promise<void>;
setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise<void>;
setOffline(params: { offline: boolean }): Promise<void>;
waitForEvent(params: { event: string }): Promise<any>;
close(): Promise<void>;
}
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<void>;
setFileChooserInterceptedNoReply(params: { intercepted: boolean }): Promise<void>;
opener(): Promise<PageChannel | null>;
addInitScript(params: { source: string }): Promise<void>;
close(params: { options?: { runBeforeUnload?: boolean } }): Promise<void>;
emulateMedia(params: { options: { media?: 'screen' | 'print', colorScheme?: 'dark' | 'light' | 'no-preference' } }): Promise<void>;
exposeBinding(params: { name: string }): Promise<void>;
setExtraHTTPHeaders(params: { headers: types.Headers }): Promise<void>;
reload(params: { options?: types.NavigateOptions }): Promise<ResponseChannel | null>;
waitForEvent(params: { event: string }): Promise<any>;
goBack(params: { options?: types.NavigateOptions }): Promise<ResponseChannel | null>;
goForward(params: { options?: types.NavigateOptions }): Promise<ResponseChannel | null>;
emulateMedia(params: { options: { media?: 'screen' | 'print', colorScheme?: 'dark' | 'light' | 'no-preference' } }): Promise<void>;
setViewportSize(params: { viewportSize: types.Size }): Promise<void>;
addInitScript(params: { source: string }): Promise<void>;
setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise<void>;
opener(): Promise<PageChannel | null>;
reload(params: { options?: types.NavigateOptions }): Promise<ResponseChannel | null>;
screenshot(params: { options?: types.ScreenshotOptions }): Promise<Buffer>;
close(params: { options?: { runBeforeUnload?: boolean } }): Promise<void>;
setExtraHTTPHeaders(params: { headers: types.Headers }): Promise<void>;
setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise<void>;
setViewportSize(params: { viewportSize: types.Size }): Promise<void>;
// Input
keyboardDown(params: { key: string }): Promise<void>;
@ -100,87 +103,86 @@ export interface PageChannel extends Channel {
}
export interface FrameChannel extends Channel {
goto(params: { url: string, options: types.GotoOptions }): Promise<ResponseChannel | null>;
waitForNavigation(params: { options: types.WaitForNavigationOptions }): Promise<ResponseChannel | null>;
waitForLoadState(params: { state: types.LifecycleEvent, options: types.TimeoutOptions }): Promise<void>;
frameElement(): Promise<ElementHandleChannel>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<any>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<JSHandleChannel>;
querySelector(params: { selector: string }): Promise<ElementHandleChannel | null>;
waitForSelector(params: { selector: string, options: types.WaitForElementOptions }): Promise<ElementHandleChannel | null>;
dispatchEvent(params: { selector: string, type: string, eventInit: Object | undefined, options: types.TimeoutOptions }): Promise<void>;
$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<any>;
$$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<any>;
querySelectorAll(params: { selector: string }): Promise<ElementHandleChannel[]>;
content(): Promise<string>;
setContent(params: { html: string, options: types.NavigateOptions }): Promise<void>;
$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<any>;
addScriptTag(params: { options: { url?: string | undefined, path?: string | undefined, content?: string | undefined, type?: string | undefined } }): Promise<ElementHandleChannel>;
addStyleTag(params: { options: { url?: string | undefined, path?: string | undefined, content?: string | undefined } }): Promise<ElementHandleChannel>;
check(params: { selector: string, options: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
click(params: { selector: string, options: types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
content(): Promise<string>;
dblclick(params: { selector: string, options: types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions & { force?: boolean }}): Promise<void>;
dispatchEvent(params: { selector: string, type: string, eventInit: Object | undefined, options: types.TimeoutOptions }): Promise<void>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<any>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<JSHandleChannel>;
fill(params: { selector: string, value: string, options: types.NavigatingActionWaitOptions }): Promise<void>;
focus(params: { selector: string, options: types.TimeoutOptions }): Promise<void>;
textContent(params: { selector: string, options: types.TimeoutOptions }): Promise<string | null>;
innerText(params: { selector: string, options: types.TimeoutOptions }): Promise<string>;
innerHTML(params: { selector: string, options: types.TimeoutOptions }): Promise<string>;
frameElement(): Promise<ElementHandleChannel>;
getAttribute(params: { selector: string, name: string, options: types.TimeoutOptions }): Promise<string | null>;
goto(params: { url: string, options: types.GotoOptions }): Promise<ResponseChannel | null>;
hover(params: { selector: string, options: types.PointerActionOptions & types.TimeoutOptions & { force?: boolean } }): Promise<void>;
selectOption(params: { selector: string, values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions }): Promise<string[]>;
setInputFiles(params: { selector: string, files: string | string[] | types.FilePayload | types.FilePayload[], options: types.NavigatingActionWaitOptions }): Promise<void>;
type(params: { selector: string, text: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void>;
innerHTML(params: { selector: string, options: types.TimeoutOptions }): Promise<string>;
innerText(params: { selector: string, options: types.TimeoutOptions }): Promise<string>;
press(params: { selector: string, key: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void>;
check(params: { selector: string, options: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
querySelector(params: { selector: string }): Promise<ElementHandleChannel | null>;
querySelectorAll(params: { selector: string }): Promise<ElementHandleChannel[]>;
selectOption(params: { selector: string, values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions }): Promise<string[]>;
setContent(params: { html: string, options: types.NavigateOptions }): Promise<void>;
setInputFiles(params: { selector: string, files: string | string[] | types.FilePayload | types.FilePayload[], options: types.NavigatingActionWaitOptions }): Promise<void>;
textContent(params: { selector: string, options: types.TimeoutOptions }): Promise<string | null>;
title(): Promise<string>;
type(params: { selector: string, text: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void>;
uncheck(params: { selector: string, options: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
waitForFunction(params: { expression: string, isFunction: boolean, arg: any; options: types.WaitForFunctionOptions }): Promise<JSHandleChannel>;
title(): Promise<string>;
waitForLoadState(params: { state: types.LifecycleEvent, options: types.TimeoutOptions }): Promise<void>;
waitForNavigation(params: { options: types.WaitForNavigationOptions }): Promise<ResponseChannel | null>;
waitForSelector(params: { selector: string, options: types.WaitForElementOptions }): Promise<ElementHandleChannel | null>;
}
export interface JSHandleChannel extends Channel {
dispose(): Promise<void>;
evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise<any>;
evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise<JSHandleChannel>;
getPropertyList(): Promise<{ name: string, value: JSHandleChannel}[]>;
jsonValue(): Promise<any>;
dispose(): Promise<void>;
}
export interface ElementHandleChannel extends JSHandleChannel {
ownerFrame(): Promise<FrameChannel | null>;
contentFrame(): Promise<FrameChannel | null>;
getAttribute(params: { name: string }): Promise<string | null>;
textContent(): Promise<string | null>;
innerText(): Promise<string>;
innerHTML(): Promise<string>;
$$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<any>;
$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<any>;
boundingBox(): Promise<types.Rect | null>;
hover(params: { options?: types.PointerActionOptions & types.TimeoutOptions & { force?: boolean } }): Promise<void>;
click(params: { options?: types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
dblclick(params: { options?: types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
selectOption(params: { values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null; options?: types.NavigatingActionWaitOptions }): string[] | Promise<string[]>;
fill(params: { value: string; options?: types.NavigatingActionWaitOptions }): Promise<void>;
selectText(params: { options?: types.TimeoutOptions }): Promise<void>;
setInputFiles(params: { files: string | string[] | types.FilePayload | types.FilePayload[], options?: types.NavigatingActionWaitOptions }): Promise<void>;
focus(): Promise<void>;
type(params: { text: string; options?: { delay?: number } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void>;
press(params: { key: string; options?: { delay?: number } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void>;
check(params: { options?: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
uncheck(params: { options?: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
click(params: { options?: types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
contentFrame(): Promise<FrameChannel | null>;
dblclick(params: { options?: types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
dispatchEvent(params: { type: string, eventInit: any }): Promise<void>;
scrollIntoViewIfNeeded(params: { options?: types.TimeoutOptions }): Promise<void>;
screenshot(params: { options?: types.ElementScreenshotOptions }): Promise<Buffer>;
fill(params: { value: string; options?: types.NavigatingActionWaitOptions }): Promise<void>;
focus(): Promise<void>;
getAttribute(params: { name: string }): Promise<string | null>;
hover(params: { options?: types.PointerActionOptions & types.TimeoutOptions & { force?: boolean } }): Promise<void>;
innerHTML(): Promise<string>;
innerText(): Promise<string>;
ownerFrame(): Promise<FrameChannel | null>;
press(params: { key: string; options?: { delay?: number } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void>;
querySelector(params: { selector: string }): Promise<ElementHandleChannel | null>;
querySelectorAll(params: { selector: string }): Promise<ElementHandleChannel[]>;
$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<any>;
$$eval(params: { selector: string; expression: string, isFunction: boolean, arg: any }): Promise<any>;
screenshot(params: { options?: types.ElementScreenshotOptions }): Promise<Buffer>;
scrollIntoViewIfNeeded(params: { options?: types.TimeoutOptions }): Promise<void>;
selectOption(params: { values: string | ElementHandleChannel | types.SelectOption | string[] | ElementHandleChannel[] | types.SelectOption[] | null; options?: types.NavigatingActionWaitOptions }): string[] | Promise<string[]>;
selectText(params: { options?: types.TimeoutOptions }): Promise<void>;
setInputFiles(params: { files: string | string[] | types.FilePayload | types.FilePayload[], options?: types.NavigatingActionWaitOptions }): Promise<void>;
textContent(): Promise<string | null>;
type(params: { text: string; options?: { delay?: number } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise<void>;
uncheck(params: { options?: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise<void>;
}
export interface RequestChannel extends Channel {
response(): Promise<ResponseChannel | null>;
}
export interface RouteChannel extends Channel {
abort(params: { errorCode: string }): Promise<void>;
continue(params: { overrides: { method?: string, headers?: types.Headers, postData?: string } }): Promise<void>;
fulfill(params: { response: types.FulfillResponse & { path?: string } }): Promise<void>;
abort(params: { errorCode: string }): Promise<void>;
response(): Promise<ResponseChannel | null>;
}
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;
}

View file

@ -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<BrowserContextChannel> {
_pages = new Set<Page>();
private _routes: { url: types.URLMatch, handler: network.RouteHandler }[] = [];
_browser: Browser | undefined;
readonly _bindings = new Map<string, frames.FunctionWithSource>();
static from(context: BrowserContextChannel): BrowserContext {
return context._object;
@ -42,7 +43,23 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel> {
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<BrowserContextChannel> {
await this._channel.addInitScript({ source });
}
async exposeBinding(name: string, playwrightBinding: frames.FunctionWithSource): Promise<void> {
async exposeBinding(name: string, binding: frames.FunctionWithSource): Promise<void> {
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<void> {
@ -126,7 +151,7 @@ export class BrowserContext extends ChannelOwner<BrowserContextChannel> {
}
async waitForEvent(event: string, optionsOrPredicate?: Function | (types.TimeoutOptions & { predicate?: Function })): Promise<any> {
return await this._channel.waitForEvent({ event });
return waitForEvent(this, event, optionsOrPredicate);
}
async close(): Promise<void> {

View file

@ -85,7 +85,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
}
async selectOption(values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
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<void> {
@ -148,3 +148,11 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
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;
}

View file

@ -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<FrameChannel> {
_parentFrame: Frame | null = null;
_url = '';
_name = '';
private _detached = false;
_childFrames = new Set<Frame>();
private _name = '';
static from(frame: FrameChannel): Frame {
return frame._object;
@ -52,10 +52,12 @@ export class Frame extends ChannelOwner<FrameChannel> {
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<network.Response | null> {
@ -192,7 +194,7 @@ export class Frame extends ChannelOwner<FrameChannel> {
}
async selectOption(selector: string, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options: types.NavigatingActionWaitOptions = {}): Promise<string[]> {
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<void> {

View file

@ -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<RequestChannel> {
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<RequestChannel> {
}
}
export class Route {
private _request: Request;
export class Route extends ChannelOwner<RouteChannel> {
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 });
}
}

View file

@ -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<PageChannel> {
readonly accessibility: Accessibility;
readonly keyboard: Keyboard;
readonly mouse: Mouse;
readonly _bindings = new Map<string, FunctionWithSource>();
static from(page: PageChannel): Page {
return page._object;
@ -67,15 +68,23 @@ export class Page extends ChannelOwner<PageChannel> {
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<PageChannel> {
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<PageChannel> {
}
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<PageChannel> {
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<PageChannel> {
}
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
const result = await this._channel.waitForEvent({ event });
if (result._object)
return result._object;
return waitForEvent(this, event, optionsOrPredicate);
}
async goBack(options?: types.NavigateOptions): Promise<Response | null> {
@ -424,3 +454,54 @@ export class Worker extends EventEmitter {
return await this._channel.evaluateHandle({ pageFunction, arg });
}
}
export class BindingCall extends ChannelOwner<BindingCallChannel> {
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<any> {
// 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;
}

View file

@ -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;

View file

@ -56,8 +56,7 @@ export class DispatcherScope {
async dispatchMessageFromClient(message: any): Promise<any> {
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;

View file

@ -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<void> {
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<PageChannel> {
@ -99,9 +106,20 @@ export class BrowserContextDispatcher extends Dispatcher implements BrowserConte
}
async setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise<void> {
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<any> {
const result = await this._context.waitForEvent(params.event);
if (result instanceof Page)
return PageDispatcher.from(this._scope, result);
return result;
}
async close(): Promise<void> {

View file

@ -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<string[]> {
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;
}

View file

@ -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<string[]> {
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<void> {

View file

@ -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<void> {
}
async fulfill(params: { response: types.FulfillResponse & { path?: string } }): Promise<void> {
}
async abort(params: { errorCode: string }): Promise<void> {
}
async response(): Promise<ResponseChannel | null> {
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<void> {
await this._route.continue(params.overrides);
}
async fulfill(params: { response: types.FulfillResponse & { path?: string } }): Promise<void> {
await this._route.fulfill(params.response);
}
async abort(params: { errorCode: string }): Promise<void> {
await this._route.abort(params.errorCode);
}
}

View file

@ -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<void> {
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<void> {
@ -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<any> {
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<ResponseChannel | null> {
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<void> {
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<Buffer> {
@ -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<any>;
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));
}
}

View file

@ -305,3 +305,9 @@ export type ConsoleMessageLocation = {
lineNumber?: number,
columnNumber?: number,
};
export type Error = {
message?: string,
stack?: string,
value?: any
};

View file

@ -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());
}