diff --git a/src/frames.ts b/src/frames.ts index 1d211c3a9bc736..126b4a27e1dcf3 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -451,7 +451,7 @@ export class Frame { async _evaluateExpression(expression: string, isFunction: boolean, arg: any): Promise { const context = await this._mainContext(); - return context.evaluateExpressionHandleInternal(expression, isFunction, arg); + return context.evaluateExpressionInternal(expression, isFunction, arg); } async $(selector: string): Promise | null> { diff --git a/src/javascript.ts b/src/javascript.ts index e4b7e3184d4462..1c6a9eafef0f5f 100644 --- a/src/javascript.ts +++ b/src/javascript.ts @@ -213,7 +213,7 @@ export async function evaluateExpression(context: ExecutionContext, returnByValu const script = `(utilityScript, ...args) => utilityScript.callFunction(...args)` + sourceMap.generateSourceUrl(); try { - return context._delegate.evaluateWithArguments(script, returnByValue, utilityScript, utilityScriptValues, utilityScriptObjectIds); + return await context._delegate.evaluateWithArguments(script, returnByValue, utilityScript, utilityScriptValues, utilityScriptObjectIds); } finally { toDispose.map(handlePromise => handlePromise.then(handle => handle.dispose())); } diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts new file mode 100644 index 00000000000000..820af62646a718 --- /dev/null +++ b/src/rpc/channels.ts @@ -0,0 +1,175 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventEmitter } from 'events'; +import * as types from '../types'; + +export interface Channel extends EventEmitter { + _type: string; + _guid: string; + _object: any; +} + +export interface BrowserTypeChannel extends Channel { + 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 { + newContext(params: { options?: types.BrowserContextOptions }): Promise; + newPage(params: { options?: types.BrowserContextOptions }): Promise; + close(): Promise; +} + +export interface BrowserContextChannel extends Channel { + 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; + setHTTPCredentials(params: { httpCredentials: types.Credentials | null }): Promise; + addInitScript(params: { source: string }): Promise; + setNetworkInterceptionEnabled(params: { enabled: 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: 'close', callback: () => void): this; + + setDefaultNavigationTimeoutNoReply(params: { timeout: number }): void; + setDefaultTimeoutNoReply(params: { timeout: number }): Promise; + setFileChooserInterceptedNoReply(params: { intercepted: boolean }): Promise; + + opener(): 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; + screenshot(params: { options?: types.ScreenshotOptions }): Promise; + close(params: { options?: { runBeforeUnload?: boolean } }): Promise; +} + +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; + 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; + click(params: { selector: string, options: types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise; + dblclick(params: { selector: string, options: types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions & { force?: boolean }}): 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; + getAttribute(params: { selector: string, name: string, options: types.TimeoutOptions }): 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; + 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; + 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; +} + +export interface JSHandleChannel extends Channel { + 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; + 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; + dispatchEvent(params: { type: string, eventInit: any }): Promise; + + scrollIntoViewIfNeeded(params: { options?: types.TimeoutOptions }): Promise; + screenshot(params: { options?: types.ElementScreenshotOptions }): 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; +} + +export interface RequestChannel extends Channel { + 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 { + body(): Promise; + finished(): Promise; +} + diff --git a/src/rpc/client/browser.ts b/src/rpc/client/browser.ts new file mode 100644 index 00000000000000..41445ae72505f3 --- /dev/null +++ b/src/rpc/client/browser.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as types from '../../types'; +import { BrowserChannel } from '../channels'; +import { BrowserContext } from './browserContext'; +import { Page } from './page'; +import { ChannelOwner } from './channelOwner'; +import { Connection } from '../connection'; + +export class Browser extends ChannelOwner { + readonly _contexts = new Set(); + private _isConnected = true; + + + static from(browser: BrowserChannel): Browser { + return browser._object; + } + + static fromNullable(browser: BrowserChannel | null): Browser | null { + return browser ? Browser.from(browser) : null; + } + + constructor(connection: Connection, channel: BrowserChannel) { + super(connection, channel); + } + + _initialize() {} + + async newContext(options?: types.BrowserContextOptions): Promise { + const context = BrowserContext.from(await this._channel.newContext({ options })); + this._contexts.add(context); + context._browser = this; + return context; + } + + contexts(): BrowserContext[] { + return [...this._contexts]; + } + + async newPage(options?: types.BrowserContextOptions): Promise { + return Page.from(await this._channel.newPage({ options })); + } + + isConnected(): boolean { + return this._isConnected; + } + + async close(): Promise { + await this._channel.close(); + } +} diff --git a/src/rpc/client/browserContext.ts b/src/rpc/client/browserContext.ts new file mode 100644 index 00000000000000..d84f4a6e699ca0 --- /dev/null +++ b/src/rpc/client/browserContext.ts @@ -0,0 +1,136 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as frames from './frame'; +import { Page } from './page'; +import * as types from '../../types'; +import * as network from './network'; +import { BrowserContextChannel } from '../channels'; +import { ChannelOwner } from './channelOwner'; +import { helper } from '../../helper'; +import { Browser } from './browser'; +import { Connection } from '../connection'; + +export class BrowserContext extends ChannelOwner { + _pages = new Set(); + private _routes: { url: types.URLMatch, handler: network.RouteHandler }[] = []; + _browser: Browser | undefined; + + static from(context: BrowserContextChannel): BrowserContext { + return context._object; + } + + static fromNullable(context: BrowserContextChannel | null): BrowserContext | null { + return context ? BrowserContext.from(context) : null; + } + + constructor(connection: Connection, channel: BrowserContextChannel) { + super(connection, channel); + } + + _initialize() { + } + + setDefaultNavigationTimeout(timeout: number) { + this._channel.setDefaultNavigationTimeoutNoReply({ timeout }); + } + + setDefaultTimeout(timeout: number) { + this._channel.setDefaultTimeoutNoReply({ timeout }); + } + + pages(): Page[] { + return [...this._pages]; + } + + async newPage(): Promise { + return Page.from(await this._channel.newPage()); + } + + async cookies(urls?: string | string[]): Promise { + if (!urls) + urls = []; + if (urls && typeof urls === 'string') + urls = [ urls ]; + return this._channel.cookies({ urls: urls as string[] }); + } + + async addCookies(cookies: network.SetNetworkCookieParam[]): Promise { + await this._channel.addCookies({ cookies }); + } + + async clearCookies(): Promise { + await this._channel.clearCookies(); + } + + async grantPermissions(permissions: string[], options?: { origin?: string }): Promise { + await this._channel.grantPermissions({ permissions, options }); + } + + async clearPermissions(): Promise { + await this._channel.clearPermissions(); + } + + async setGeolocation(geolocation: types.Geolocation | null): Promise { + await this._channel.setGeolocation({ geolocation }); + } + + async setExtraHTTPHeaders(headers: types.Headers): Promise { + await this._channel.setExtraHTTPHeaders({ headers }); + } + + async setOffline(offline: boolean): Promise { + await this._channel.setOffline({ offline }); + } + + async setHTTPCredentials(httpCredentials: types.Credentials | null): Promise { + await this._channel.setHTTPCredentials({ httpCredentials }); + } + + async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise { + const source = await helper.evaluationScript(script, arg); + await this._channel.addInitScript({ source }); + } + + async exposeBinding(name: string, playwrightBinding: frames.FunctionWithSource): Promise { + } + + async exposeFunction(name: string, playwrightFunction: Function): Promise { + await this.exposeBinding(name, (source, ...args) => playwrightFunction(...args)); + } + + async route(url: types.URLMatch, handler: network.RouteHandler): Promise { + this._routes.push({ url, handler }); + if (this._routes.length === 1) + await this._channel.setNetworkInterceptionEnabled({ enabled: true }); + } + + async unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise { + this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); + if (this._routes.length === 0) + await this._channel.setNetworkInterceptionEnabled({ enabled: false }); + } + + async waitForEvent(event: string, optionsOrPredicate?: Function | (types.TimeoutOptions & { predicate?: Function })): Promise { + return await this._channel.waitForEvent({ event }); + } + + async close(): Promise { + await this._channel.close(); + this._browser!._contexts.delete(this); + } +} diff --git a/src/rpc/client/browserType.ts b/src/rpc/client/browserType.ts new file mode 100644 index 00000000000000..9f37b31d977eec --- /dev/null +++ b/src/rpc/client/browserType.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as types from '../../types'; +import { BrowserTypeChannel } from '../channels'; +import { Browser } from './browser'; +import { BrowserContext } from './browserContext'; +import { ChannelOwner } from './channelOwner'; +import { Connection } from '../connection'; + +export class BrowserType extends ChannelOwner { + private _executablePath: string = ''; + private _name: string = ''; + + constructor(connection: Connection, channel: BrowserTypeChannel) { + super(connection, channel); + } + + _initialize(payload: { executablePath: string, name: string }) { + this._executablePath = payload.executablePath; + this._name = payload.name; + } + + executablePath(): string { + return this._executablePath; + } + + name(): string { + return this._name; + } + + async launch(options?: types.LaunchOptions): Promise { + return Browser.from(await this._channel.launch({ options })); + } + + async launchPersistentContext(userDataDir: string, options?: types.LaunchOptions & types.BrowserContextOptions): Promise { + return BrowserContext.from(await this._channel.launchPersistentContext({ userDataDir, options })); + } + + async connect(options: types.ConnectOptions): Promise { + return Browser.from(await this._channel.connect({ options })); + } +} diff --git a/src/rpc/client/channelOwner.ts b/src/rpc/client/channelOwner.ts new file mode 100644 index 00000000000000..c41956e2474609 --- /dev/null +++ b/src/rpc/client/channelOwner.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventEmitter } from 'events'; +import { Channel } from '../channels'; +import { Connection } from '../connection'; + +export abstract class ChannelOwner extends EventEmitter { + readonly _channel: T; + readonly _connection: Connection; + static clientSymbol = Symbol('client'); + + constructor(connection: Connection, channel: T) { + super(); + this._connection = connection; + this._channel = channel; + (channel as any)[ChannelOwner.clientSymbol] = this; + } + + abstract _initialize(payload: any): void; +} diff --git a/src/rpc/client/elementHandle.ts b/src/rpc/client/elementHandle.ts new file mode 100644 index 00000000000000..eb63d258e01a9f --- /dev/null +++ b/src/rpc/client/elementHandle.ts @@ -0,0 +1,150 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as types from '../../types'; +import { ElementHandleChannel } from '../channels'; +import { Frame } from './frame'; +import { FuncOn, JSHandle, convertArg } from './jsHandle'; +import { Connection } from '../connection'; + +export class ElementHandle extends JSHandle { + private _elementChannel: ElementHandleChannel; + + static from(handle: ElementHandleChannel): ElementHandle { + return handle._object; + } + + static fromNullable(handle: ElementHandleChannel | null): ElementHandle | null { + return handle ? ElementHandle.from(handle) : null; + } + + constructor(connection: Connection, channel: ElementHandleChannel) { + super(connection, channel); + this._elementChannel = channel; + } + + asElement(): ElementHandle | null { + return this; + } + + async ownerFrame(): Promise { + return Frame.fromNullable(await this._elementChannel.ownerFrame()); + } + + async contentFrame(): Promise { + return Frame.fromNullable(await this._elementChannel.contentFrame()); + } + + async getAttribute(name: string): Promise { + return await this._elementChannel.getAttribute({ name }); + } + + async textContent(): Promise { + return await this._elementChannel.textContent(); + } + + async innerText(): Promise { + return await this._elementChannel.innerText(); + } + + async innerHTML(): Promise { + return await this._elementChannel.innerHTML(); + } + + async dispatchEvent(type: string, eventInit: Object = {}) { + await this._elementChannel.dispatchEvent({ type, eventInit }); + } + + async scrollIntoViewIfNeeded(options?: types.TimeoutOptions) { + await this._elementChannel.scrollIntoViewIfNeeded({ options }); + } + + async hover(options: types.PointerActionOptions & types.PointerActionWaitOptions = {}): Promise { + await this._elementChannel.hover({ options }); + } + + async click(options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { + return await this._elementChannel.click({ options }); + } + + async dblclick(options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}): Promise { + return await this._elementChannel.dblclick({ options }); + } + + 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 }); + } + + async fill(value: string, options: types.NavigatingActionWaitOptions = {}): Promise { + return await this._elementChannel.fill({ value, options }); + } + + async selectText(options: types.TimeoutOptions): Promise { + await this._elementChannel.selectText({ options }); + } + + async setInputFiles(files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}) { + await this._elementChannel.setInputFiles({ files, options }); + } + + async focus(): Promise { + await this._elementChannel.focus(); + } + + async type(text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise { + await this._elementChannel.type({ text, options }); + } + + async press(key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}): Promise { + await this._elementChannel.press({ key, options }); + } + + async check(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + return await this._elementChannel.check({ options }); + } + + async uncheck(options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + return await this._elementChannel.uncheck({ options }); + } + + async boundingBox(): Promise { + return await this._elementChannel.boundingBox(); + } + + async screenshot(options?: types.ElementScreenshotOptions): Promise { + return await this._elementChannel.screenshot({ options }); + } + + async $(selector: string): Promise | null> { + return ElementHandle.fromNullable(await this._elementChannel.querySelector({ selector })) as ElementHandle | null; + } + + async $$(selector: string): Promise[]> { + return (await this._elementChannel.querySelectorAll({ selector })).map(h => ElementHandle.from(h) as ElementHandle); + } + + async $eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise; + async $eval(selector: string, pageFunction: FuncOn, arg?: any): Promise; + async $eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise { + return await this._elementChannel.$eval({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); + } + + async $$eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise; + async $$eval(selector: string, pageFunction: FuncOn, arg?: any): Promise; + async $$eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise { + return await this._elementChannel.$$eval({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); + } +} diff --git a/src/rpc/client/frame.ts b/src/rpc/client/frame.ts new file mode 100644 index 00000000000000..e7487b86493512 --- /dev/null +++ b/src/rpc/client/frame.ts @@ -0,0 +1,231 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { assertMaxArguments } from '../../helper'; +import * as types from '../../types'; +import { FrameChannel } from '../channels'; +import { BrowserContext } from './browserContext'; +import { ChannelOwner } from './channelOwner'; +import { ElementHandle } from './elementHandle'; +import { JSHandle, Func1, FuncOn, SmartHandle, convertArg } from './jsHandle'; +import * as network from './network'; +import { Response } from './network'; +import { Page } from './page'; +import { Connection } from '../connection'; + +export type GotoOptions = types.NavigateOptions & { + referer?: string, +}; + +export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame }, ...args: any) => any; + +export class Frame extends ChannelOwner { + _parentFrame: Frame | null = null; + _url = ''; + private _detached = false; + _childFrames = new Set(); + private _name = ''; + + static from(frame: FrameChannel): Frame { + return frame._object; + } + + static fromNullable(frame: FrameChannel | null): Frame | null { + return frame ? Frame.from(frame) : null; + } + + constructor(connection: Connection, channel: FrameChannel) { + super(connection, channel); + } + + _initialize(payload: { parentFrame: FrameChannel | null }) { + this._parentFrame = payload.parentFrame ? payload.parentFrame._object : null; + if (this._parentFrame) + this._parentFrame._childFrames.add(this); + } + + async goto(url: string, options: GotoOptions = {}): Promise { + return Response.fromNullable(await this._channel.goto({ url, options })); + } + + async waitForNavigation(options: types.WaitForNavigationOptions = {}): Promise { + return Response.fromNullable(await this._channel.waitForNavigation({ options })); + } + + async waitForLoadState(state: types.LifecycleEvent = 'load', options: types.TimeoutOptions = {}): Promise { + await this._channel.waitForLoadState({ state, options }); + } + + async frameElement(): Promise { + return ElementHandle.from(await this._channel.frameElement()); + } + + async evaluateHandle(pageFunction: Func1, arg: Arg): Promise>; + async evaluateHandle(pageFunction: Func1, arg?: any): Promise>; + async evaluateHandle(pageFunction: Func1, arg: Arg): Promise> { + assertMaxArguments(arguments.length, 2); + return JSHandle.from(await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) })) as SmartHandle; + } + + async evaluate(pageFunction: Func1, arg: Arg): Promise; + async evaluate(pageFunction: Func1, arg?: any): Promise; + async evaluate(pageFunction: Func1, arg: Arg): Promise { + assertMaxArguments(arguments.length, 2); + return await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); + } + + async $(selector: string): Promise | null> { + return ElementHandle.fromNullable(await this._channel.querySelector({ selector })) as ElementHandle | null; + } + + async waitForSelector(selector: string, options: types.WaitForElementOptions = {}): Promise | null> { + return ElementHandle.fromNullable(await this._channel.waitForSelector({ selector, options })) as ElementHandle | null; + } + + async dispatchEvent(selector: string, type: string, eventInit?: Object, options: types.TimeoutOptions = {}): Promise { + await this._channel.dispatchEvent({ selector, type, eventInit, options }); + } + + async $eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise; + async $eval(selector: string, pageFunction: FuncOn, arg?: any): Promise; + async $eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise { + assertMaxArguments(arguments.length, 3); + return await this._channel.$eval({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); + } + + async $$eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise; + async $$eval(selector: string, pageFunction: FuncOn, arg?: any): Promise; + async $$eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise { + assertMaxArguments(arguments.length, 3); + return await this._channel.$$eval({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); + } + + async $$(selector: string): Promise[]> { + const result = await this._channel.querySelectorAll({ selector }); + return result.map(c => ElementHandle.from(c) as ElementHandle); + } + + async content(): Promise { + return await this._channel.content(); + } + + async setContent(html: string, options: types.NavigateOptions = {}): Promise { + await this._channel.setContent({ html, options }); + } + + name(): string { + return this._name || ''; + } + + url(): string { + return this._url; + } + + parentFrame(): Frame | null { + return this._parentFrame; + } + + childFrames(): Frame[] { + return Array.from(this._childFrames); + } + + isDetached(): boolean { + return this._detached; + } + + async addScriptTag(options: { url?: string, path?: string, content?: string, type?: string }): Promise { + return ElementHandle.from(await this._channel.addScriptTag({ options })); + } + + async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { + return ElementHandle.from(await this._channel.addStyleTag({ options })); + } + + async click(selector: string, options: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + return await this._channel.click({ selector, options }); + } + + async dblclick(selector: string, options: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + return await this._channel.dblclick({ selector, options }); + } + + async fill(selector: string, value: string, options: types.NavigatingActionWaitOptions = {}) { + return await this._channel.fill({ selector, value, options }); + } + + async focus(selector: string, options: types.TimeoutOptions = {}) { + await this._channel.focus({ selector, options }); + } + + async textContent(selector: string, options: types.TimeoutOptions = {}): Promise { + return await this._channel.textContent({ selector, options }); + } + + async innerText(selector: string, options: types.TimeoutOptions = {}): Promise { + return await this._channel.innerText({ selector, options }); + } + + async innerHTML(selector: string, options: types.TimeoutOptions = {}): Promise { + return await this._channel.innerHTML({ selector, options }); + } + + async getAttribute(selector: string, name: string, options: types.TimeoutOptions = {}): Promise { + return await this._channel.getAttribute({ selector, name, options }); + } + + async hover(selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) { + await this._channel.hover({ selector, options }); + } + + 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 }); + } + + async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options: types.NavigatingActionWaitOptions = {}): Promise { + await this._channel.setInputFiles({ selector, files, options }); + } + + async type(selector: string, text: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { + await this._channel.type({ selector, text, options }); + } + + async press(selector: string, key: string, options: { delay?: number } & types.NavigatingActionWaitOptions = {}) { + await this._channel.press({ selector, key, options }); + } + + async check(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + await this._channel.check({ selector, options }); + } + + async uncheck(selector: string, options: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions = {}) { + await this._channel.uncheck({ selector, options }); + } + + async waitForTimeout(timeout: number) { + await new Promise(fulfill => setTimeout(fulfill, timeout)); + } + + async waitForFunction(pageFunction: Func1, arg: Arg, options?: types.WaitForFunctionOptions): Promise>; + async waitForFunction(pageFunction: Func1, arg?: any, options?: types.WaitForFunctionOptions): Promise>; + async waitForFunction(pageFunction: Func1, arg: Arg, options: types.WaitForFunctionOptions = {}): Promise> { + return JSHandle.from(await this._channel.waitForFunction({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg, options })) as SmartHandle; + } + + async title(): Promise { + return await this._channel.title(); + } +} diff --git a/src/rpc/client/jsHandle.ts b/src/rpc/client/jsHandle.ts new file mode 100644 index 00000000000000..932cdffe0e10e3 --- /dev/null +++ b/src/rpc/client/jsHandle.ts @@ -0,0 +1,122 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JSHandleChannel } from '../channels'; +import { ElementHandle } from './elementHandle'; +import { ChannelOwner } from './channelOwner'; +import { Connection } from '../connection'; + +type NoHandles = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles } : Arg); +type Unboxed = + Arg extends ElementHandle ? T : + Arg extends JSHandle ? T : + Arg extends NoHandles ? Arg : + Arg extends [infer A0] ? [Unboxed] : + Arg extends [infer A0, infer A1] ? [Unboxed, Unboxed] : + Arg extends [infer A0, infer A1, infer A2] ? [Unboxed, Unboxed, Unboxed] : + Arg extends Array ? Array> : + Arg extends object ? { [Key in keyof Arg]: Unboxed } : + Arg; +export type Func0 = string | (() => R | Promise); +export type Func1 = string | ((arg: Unboxed) => R | Promise); +export type FuncOn = string | ((on: On, arg2: Unboxed) => R | Promise); +export type SmartHandle = T extends Node ? ElementHandle : JSHandle; + +export class JSHandle extends ChannelOwner { + protected _jsChannel: JSHandleChannel; + private _preview: string | undefined; + + static from(handle: JSHandleChannel): JSHandle { + return handle._object; + } + + static fromNullable(handle: JSHandleChannel | null): JSHandle | null { + return handle ? JSHandle.from(handle) : null; + } + + constructor(conection: Connection, channel: JSHandleChannel) { + super(conection, channel); + this._jsChannel = channel; + } + + _initialize(params: { preview: string }) { + this._preview = params.preview; + } + + async evaluate(pageFunction: FuncOn, arg: Arg): Promise; + async evaluate(pageFunction: FuncOn, arg?: any): Promise; + async evaluate(pageFunction: FuncOn, arg: Arg): Promise { + return await this._jsChannel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); + } + + async evaluateHandle(pageFunction: FuncOn, arg: Arg): Promise>; + async evaluateHandle(pageFunction: FuncOn, arg?: any): Promise>; + async evaluateHandle(pageFunction: FuncOn, arg: Arg): Promise> { + const handleChannel = await this._jsChannel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === 'function', arg: convertArg(arg) }); + return JSHandle.from(handleChannel) as SmartHandle; + } + + async getProperty(propertyName: string): Promise { + const objectHandle = await this.evaluateHandle((object: any, propertyName: string) => { + const result: any = {__proto__: null}; + result[propertyName] = object[propertyName]; + return result; + }, propertyName); + const properties = await objectHandle.getProperties(); + const result = properties.get(propertyName)!; + objectHandle.dispose(); + return result; + } + + async getProperties(): Promise> { + const map = new Map(); + for (const { name, value } of await this._jsChannel.getPropertyList()) + map.set(name, JSHandle.from(value)); + return map; + } + + async jsonValue(): Promise { + return await this._jsChannel.jsonValue(); + } + + asElement(): ElementHandle | null { + return null; + } + + async dispose() { + return await this._jsChannel.dispose(); + } + + toString(): string { + return this._preview!; + } +} + +export function convertArg(arg: any): any { + if (arg === null) + return null; + if (Array.isArray(arg)) + return arg.map(item => convertArg(item)); + if (arg instanceof ChannelOwner) + return arg._channel; + if (typeof arg === 'object') { + const result: any = {}; + for (const key of Object.keys(arg)) + result[key] = convertArg(arg[key]); + return result; + } + return arg; +} diff --git a/src/rpc/client/network.ts b/src/rpc/client/network.ts new file mode 100644 index 00000000000000..b22bfae3b8af3d --- /dev/null +++ b/src/rpc/client/network.ts @@ -0,0 +1,251 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { URLSearchParams } from 'url'; +import * as types from '../../types'; +import { RequestChannel, ResponseChannel, FrameChannel } from '../channels'; +import { ChannelOwner } from './channelOwner'; +import { Frame } from './frame'; +import { Connection } from '../connection'; + +export type NetworkCookie = { + name: string, + value: string, + domain: string, + path: string, + expires: number, + httpOnly: boolean, + secure: boolean, + sameSite: 'Strict' | 'Lax' | 'None' +}; + +export type SetNetworkCookieParam = { + name: string, + value: string, + url?: string, + domain?: string, + path?: string, + expires?: number, + httpOnly?: boolean, + secure?: boolean, + sameSite?: 'Strict' | 'Lax' | 'None' +}; + +export class Request extends ChannelOwner { + private _redirectedFrom: Request | null = null; + private _redirectedTo: Request | null = null; + private _isNavigationRequest = false; + private _failureText: string | null = null; + private _url: string = ''; + private _resourceType = ''; + private _method = ''; + private _postData: string | null = null; + private _headers: types.Headers = {}; + private _frame: Frame | undefined; + + static from(request: RequestChannel): Request { + return request._object; + } + + static fromNullable(request: RequestChannel | null): Request | null { + return request ? Request.from(request) : null; + } + + constructor(connection: Connection, channel: RequestChannel) { + super(connection, channel); + } + + _initialize(payload: { frame: FrameChannel, redirectedFrom: RequestChannel | null, isNavigationRequest: boolean, + url: string, resourceType: string, method: string, postData: string | null, headers: types.Headers }) { + this._frame = payload.frame._object as Frame; + this._isNavigationRequest = payload.isNavigationRequest; + this._redirectedFrom = Request.fromNullable(payload.redirectedFrom); + if (this._redirectedFrom) + this._redirectedFrom._redirectedTo = this; + this._url = payload.url; + this._resourceType = payload.resourceType; + this._method = payload.method; + this._postData = payload.postData; + this._headers = payload.headers; + } + + url(): string { + return this._url; + } + + resourceType(): string { + return this._resourceType; + } + + method(): string { + return this._method; + } + + postData(): string | null { + return this._postData; + } + + postDataJSON(): Object | null { + if (!this._postData) + return null; + + const contentType = this.headers()['content-type']; + if (!contentType) + return null; + + if (contentType === 'application/x-www-form-urlencoded') { + const entries: Record = {}; + const parsed = new URLSearchParams(this._postData); + for (const [k, v] of parsed.entries()) + entries[k] = v; + return entries; + } + + return JSON.parse(this._postData); + } + + headers(): {[key: string]: string} { + return { ...this._headers }; + } + + async response(): Promise { + return Response.fromNullable(await this._channel.response()); + } + + frame(): Frame { + return this._frame!; + } + + isNavigationRequest(): boolean { + return this._isNavigationRequest; + } + + redirectedFrom(): Request | null { + return this._redirectedFrom; + } + + redirectedTo(): Request | null { + return this._redirectedTo; + } + + failure(): { errorText: string; } | null { + if (this._failureText === null) + return null; + return { + errorText: this._failureText + }; + } +} + +export class Route { + private _request: Request; + + constructor(request: Request) { + this._request = request; + } + + request(): Request { + return this._request; + } + + async abort(errorCode: string = 'failed') { + await this._request._channel.abort({ errorCode }); + } + + async fulfill(response: types.FulfillResponse & { path?: string }) { + await this._request._channel.fulfill({ response }); + } + + async continue(overrides: { method?: string; headers?: types.Headers; postData?: string } = {}) { + await this._request._channel.continue({ overrides }); + } +} + +export type RouteHandler = (route: Route, request: Request) => void; + +export class Response extends ChannelOwner { + private _request: Request | undefined; + private _status: number = 0; + private _statusText: string = ''; + private _url: string = ''; + private _headers: types.Headers = {}; + + static from(response: ResponseChannel): Response { + return response._object; + } + + static fromNullable(response: ResponseChannel | null): Response | null { + return response ? Response.from(response) : null; + } + + constructor(connection: Connection, channel: ResponseChannel) { + super(connection, channel); + } + + _initialize(payload: { request: RequestChannel, url: string, status: number, statusText: string, headers: types.Headers }) { + this._request = Request.from(payload.request); + this._status = payload.status; + this._statusText = payload.statusText; + this._url = payload.url; + this._headers = payload.headers; + } + + url(): string { + return this._url; + } + + ok(): boolean { + return this._status === 0 || (this._status >= 200 && this._status <= 299); + } + + status(): number { + return this._status; + } + + statusText(): string { + return this._statusText; + } + + headers(): object { + return { ...this._headers }; + } + + async finished(): Promise { + return await this._channel.finished(); + } + + async body(): Promise { + return await this._channel.body(); + } + + async text(): Promise { + const content = await this.body(); + return content.toString('utf8'); + } + + async json(): Promise { + const content = await this.text(); + return JSON.parse(content); + } + + request(): Request { + return this._request!; + } + + frame(): Frame { + return this._request!.frame(); + } +} diff --git a/src/rpc/client/page.ts b/src/rpc/client/page.ts new file mode 100644 index 00000000000000..f13d12819f1757 --- /dev/null +++ b/src/rpc/client/page.ts @@ -0,0 +1,413 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventEmitter } from 'events'; +import { Events } from '../../events'; +import { assert, assertMaxArguments, helper, Listener } from '../../helper'; +import * as types from '../../types'; +import { BrowserContextChannel, FrameChannel, PageChannel } 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 { Connection } from '../connection'; + +export class Page extends ChannelOwner { + readonly pdf: ((options?: types.PDFOptions) => Promise) | undefined; + + private _browserContext: BrowserContext | undefined; + private _mainFrame: Frame | undefined; + private _frames = new Set(); + private _workers: Worker[] = []; + private _closed = false; + private _viewportSize: types.Size | null = null; + private _routes: { url: types.URLMatch, handler: RouteHandler }[] = []; + + static from(page: PageChannel): Page { + return page._object; + } + + static fromNullable(page: PageChannel | null): Page | null { + return page ? Page.from(page) : null; + } + + constructor(connection: Connection, channel: PageChannel) { + super(connection, channel); + } + + _initialize(payload: { browserContext: BrowserContextChannel, mainFrame: FrameChannel, viewportSize: types.Size }) { + this._browserContext = BrowserContext.from(payload.browserContext); + this._mainFrame = Frame.from(payload.mainFrame); + this._frames.add(this._mainFrame); + this._viewportSize = payload.viewportSize; + + 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('request', request => this.emit(Events.Page.Request, 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('close', () => this._onClose()); + } + + private _onFrameAttached(frame: Frame) { + this._frames.add(frame); + if (frame._parentFrame) + frame._parentFrame._childFrames.add(frame); + this.emit(Events.Page.FrameAttached, frame); + } + + private _onFrameDetached(frame: Frame) { + this._frames.delete(frame); + if (frame._parentFrame) + frame._parentFrame._childFrames.delete(frame); + this.emit(Events.Page.FrameDetached, frame); + } + + private _onFrameNavigated(frame: Frame, url: string) { + frame._url = url; + this.emit(Events.Page.FrameNavigated, frame); + } + + private _onClose() { + this._browserContext!._pages.delete(this); + this.emit(Events.Page.Close); + } + + context(): BrowserContext { + return this._browserContext!; + } + + async opener(): Promise { + return Page.fromNullable(await this._channel.opener()); + } + + mainFrame(): Frame { + return this._mainFrame!!; + } + + frame(options: string | { name?: string, url?: types.URLMatch }): Frame | null { + const name = helper.isString(options) ? options : options.name; + const url = helper.isObject(options) ? options.url : undefined; + assert(name || url, 'Either name or url matcher should be specified'); + return this.frames().find(f => { + if (name) + return f.name() === name; + return helper.urlMatches(f.url(), url); + }) || null; + } + + frames(): Frame[] { + return [...this._frames]; + } + + setDefaultNavigationTimeout(timeout: number) { + this._channel.setDefaultNavigationTimeoutNoReply({ timeout }); + } + + setDefaultTimeout(timeout: number) { + this._channel.setDefaultTimeoutNoReply({ timeout }); + } + + async $(selector: string): Promise | null> { + return await this._mainFrame!.$(selector); + } + + async waitForSelector(selector: string, options?: types.WaitForElementOptions): Promise | null> { + return await this._mainFrame!.waitForSelector(selector, options); + } + + async dispatchEvent(selector: string, type: string, eventInit?: Object, options?: types.TimeoutOptions): Promise { + return await this._mainFrame!.dispatchEvent(selector, type, eventInit, options); + } + + async evaluateHandle(pageFunction: Func1, arg: Arg): Promise>; + async evaluateHandle(pageFunction: Func1, arg?: any): Promise>; + async evaluateHandle(pageFunction: Func1, arg: Arg): Promise> { + assertMaxArguments(arguments.length, 2); + return await this._mainFrame!.evaluateHandle(pageFunction, arg); + } + + async $eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise; + async $eval(selector: string, pageFunction: FuncOn, arg?: any): Promise; + async $eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise { + assertMaxArguments(arguments.length, 3); + return await this._mainFrame!.$eval(selector, pageFunction, arg); + } + + async $$eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise; + async $$eval(selector: string, pageFunction: FuncOn, arg?: any): Promise; + async $$eval(selector: string, pageFunction: FuncOn, arg: Arg): Promise { + assertMaxArguments(arguments.length, 3); + return await this._mainFrame!.$$eval(selector, pageFunction, arg); + } + + async $$(selector: string): Promise[]> { + return await this._mainFrame!.$$(selector); + } + + async addScriptTag(options: { url?: string; path?: string; content?: string; type?: string; }): Promise { + return await this._mainFrame!.addScriptTag(options); + } + + async addStyleTag(options: { url?: string; path?: string; content?: string; }): Promise { + return await this._mainFrame!.addStyleTag(options); + } + + async exposeFunction(name: string, playwrightFunction: Function) { + await this.exposeBinding(name, (options, ...args: any) => playwrightFunction(...args)); + } + + async exposeBinding(name: string, playwrightBinding: FunctionWithSource) { + await this._channel.exposeBinding({ name }); + } + + async setExtraHTTPHeaders(headers: types.Headers) { + await this._channel.setExtraHTTPHeaders({ headers }); + } + + url(): string { + return this.mainFrame().url(); + } + + async content(): Promise { + return this.mainFrame().content(); + } + + async setContent(html: string, options?: types.NavigateOptions): Promise { + return this.mainFrame().setContent(html, options); + } + + async goto(url: string, options?: GotoOptions): Promise { + return this.mainFrame().goto(url, options); + } + + async reload(options?: types.NavigateOptions): Promise { + return Response.fromNullable(await this._channel.reload({ options })); + } + + async waitForLoadState(state?: types.LifecycleEvent, options?: types.TimeoutOptions): Promise { + return this._mainFrame!.waitForLoadState(state, options); + } + + async waitForNavigation(options?: types.WaitForNavigationOptions): Promise { + return this._mainFrame!.waitForNavigation(options); + } + + async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean), options: types.TimeoutOptions = {}): Promise { + const predicate = (request: Request) => { + if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate)) + return helper.urlMatches(request.url(), urlOrPredicate); + return urlOrPredicate(request); + }; + return this.waitForEvent(Events.Page.Request, { predicate, timeout: options.timeout }); + } + + async waitForResponse(urlOrPredicate: string | RegExp | ((r: Response) => boolean), options: types.TimeoutOptions = {}): Promise { + const predicate = (response: Response) => { + if (helper.isString(urlOrPredicate) || helper.isRegExp(urlOrPredicate)) + return helper.urlMatches(response.url(), urlOrPredicate); + return urlOrPredicate(response); + }; + return this.waitForEvent(Events.Page.Response, { predicate, timeout: options.timeout }); + } + + async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise { + return await this._channel.waitForEvent({ event }); + } + + async goBack(options?: types.NavigateOptions): Promise { + return Response.fromNullable(await this._channel.goBack({ options })); + } + + async goForward(options?: types.NavigateOptions): Promise { + return Response.fromNullable(await this._channel.goForward({ options })); + } + + async emulateMedia(options: { media?: types.MediaType, colorScheme?: types.ColorScheme }) { + await this._channel.emulateMedia({ options }); + } + + async setViewportSize(viewportSize: types.Size) { + await this._channel.setViewportSize({ viewportSize }); + } + + viewportSize(): types.Size | null { + return this._viewportSize; + } + + async evaluate(pageFunction: Func1, arg: Arg): Promise; + async evaluate(pageFunction: Func1, arg?: any): Promise; + async evaluate(pageFunction: Func1, arg: Arg): Promise { + assertMaxArguments(arguments.length, 2); + return this.mainFrame().evaluate(pageFunction, arg); + } + + async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) { + const source = await helper.evaluationScript(script, arg); + await this._channel.addInitScript({ source }); + } + + async route(url: types.URLMatch, handler: RouteHandler): Promise { + this._routes.push({ url, handler }); + if (this._routes.length === 1) + await this._channel.setNetworkInterceptionEnabled({ enabled: true }); + } + + async unroute(url: types.URLMatch, handler?: RouteHandler): Promise { + this._routes = this._routes.filter(route => route.url !== url || (handler && route.handler !== handler)); + if (this._routes.length === 0) + await this._channel.setNetworkInterceptionEnabled({ enabled: false }); + } + + async screenshot(options?: types.ScreenshotOptions): Promise { + return await this._channel.screenshot({ options }); + } + + async title(): Promise { + return this.mainFrame().title(); + } + + async close(options: { runBeforeUnload?: boolean } = {runBeforeUnload: undefined}) { + await this._channel.close({ options }); + } + + isClosed(): boolean { + return this._closed; + } + + async click(selector: string, options?: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + return this.mainFrame().click(selector, options); + } + + async dblclick(selector: string, options?: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + return this.mainFrame().dblclick(selector, options); + } + + async fill(selector: string, value: string, options?: types.NavigatingActionWaitOptions) { + return this.mainFrame().fill(selector, value, options); + } + + async focus(selector: string, options?: types.TimeoutOptions) { + return this.mainFrame().focus(selector, options); + } + + async textContent(selector: string, options?: types.TimeoutOptions): Promise { + return this.mainFrame().textContent(selector, options); + } + + async innerText(selector: string, options?: types.TimeoutOptions): Promise { + return this.mainFrame().innerText(selector, options); + } + + async innerHTML(selector: string, options?: types.TimeoutOptions): Promise { + return this.mainFrame().innerHTML(selector, options); + } + + async getAttribute(selector: string, name: string, options?: types.TimeoutOptions): Promise { + return this.mainFrame().getAttribute(selector, name, options); + } + + async hover(selector: string, options?: types.PointerActionOptions & types.PointerActionWaitOptions) { + return this.mainFrame().hover(selector, options); + } + + async selectOption(selector: string, values: string | ElementHandle | types.SelectOption | string[] | ElementHandle[] | types.SelectOption[] | null, options?: types.NavigatingActionWaitOptions): Promise { + return this.mainFrame().selectOption(selector, values, options); + } + + async setInputFiles(selector: string, files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions): Promise { + return this.mainFrame().setInputFiles(selector, files, options); + } + + async type(selector: string, text: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { + return this.mainFrame().type(selector, text, options); + } + + async press(selector: string, key: string, options?: { delay?: number } & types.NavigatingActionWaitOptions) { + return this.mainFrame().press(selector, key, options); + } + + async check(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + return this.mainFrame().check(selector, options); + } + + async uncheck(selector: string, options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions) { + return this.mainFrame().uncheck(selector, options); + } + + async waitForTimeout(timeout: number) { + await this.mainFrame().waitForTimeout(timeout); + } + + async waitForFunction(pageFunction: Func1, arg: Arg, options?: types.WaitForFunctionOptions): Promise>; + async waitForFunction(pageFunction: Func1, arg?: any, options?: types.WaitForFunctionOptions): Promise>; + async waitForFunction(pageFunction: Func1, arg: Arg, options?: types.WaitForFunctionOptions): Promise> { + return this.mainFrame().waitForFunction(pageFunction, arg, options); + } + + workers(): Worker[] { + return this._workers; + } + + on(event: string | symbol, listener: Listener): this { + if (event === Events.Page.FileChooser) { + if (!this.listenerCount(event)) + this._channel.setFileChooserInterceptedNoReply({ intercepted: true }); + } + super.on(event, listener); + return this; + } + + removeListener(event: string | symbol, listener: Listener): this { + super.removeListener(event, listener); + if (event === Events.Page.FileChooser && !this.listenerCount(event)) + this._channel.setFileChooserInterceptedNoReply({ intercepted: false }); + return this; + } +} + +export class Worker extends EventEmitter { + private _url: string; + private _channel: any; + + constructor(url: string) { + super(); + this._url = url; + } + + url(): string { + return this._url; + } + + async evaluate(pageFunction: Func1, arg: Arg): Promise; + async evaluate(pageFunction: Func1, arg?: any): Promise; + async evaluate(pageFunction: Func1, arg: Arg): Promise { + assertMaxArguments(arguments.length, 2); + return await this._channel.evaluate({ pageFunction, arg }); + } + + async evaluateHandle(pageFunction: Func1, arg: Arg): Promise>; + async evaluateHandle(pageFunction: Func1, arg?: any): Promise>; + async evaluateHandle(pageFunction: Func1, arg: Arg): Promise> { + assertMaxArguments(arguments.length, 2); + return await this._channel.evaluateHandle({ pageFunction, arg }); + } +} diff --git a/src/rpc/connection.ts b/src/rpc/connection.ts new file mode 100644 index 00000000000000..25811836fcca80 --- /dev/null +++ b/src/rpc/connection.ts @@ -0,0 +1,145 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventEmitter } from 'ws'; +import { Browser } from './client/browser'; +import { BrowserContext } from './client/browserContext'; +import { BrowserType } from './client/browserType'; +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 debug = require('debug'); +import { Channel } from './channels'; + +export class Connection { + private _channels = new Map(); + sendMessageToServerTransport = (message: any): Promise => Promise.resolve(); + + constructor() {} + + createRemoteObject(type: string, guid: string): any { + const channel = this._createChannel(guid) as any; + this._channels.set(guid, channel); + let result: ChannelOwner; + switch (type) { + case 'browserType': + result = new BrowserType(this, channel); + break; + case 'browser': + result = new Browser(this, channel); + break; + case 'context': + result = new BrowserContext(this, channel); + break; + case 'page': + result = new Page(this, channel); + break; + case 'frame': + result = new Frame(this, channel); + break; + case 'request': + result = new Request(this, channel); + break; + case 'response': + result = new Response(this, channel); + break; + case 'jsHandle': + result = new JSHandle(this, channel); + break; + case 'elementHandle': + result = new ElementHandle(this, channel); + break; + default: + throw new Error('Missing type ' + type); + } + channel._object = result; + return result; + } + + async sendMessageToServer(message: { guid: string, method: string, params: any }) { + const converted = {...message, params: this._replaceChannelsWithGuids(message.params)}; + debug('pw:channel:command')(converted); + const response = await this.sendMessageToServerTransport(converted); + debug('pw:channel:response')(response); + return this._replaceGuidsWithChannels(response); + } + + dispatchMessageFromServer(message: { guid: string, method: string, params: any }) { + debug('pw:channel:event')(message); + const { guid, method, params } = message; + + if (method === '__create__') { + this.createRemoteObject(params.type, guid); + return; + } + + const channel = this._channels.get(guid)!; + if (message.method === '__init__') { + channel._object._initialize(this._replaceGuidsWithChannels(params)); + return; + } + channel.emit(method, this._replaceGuidsWithChannels(params)); + } + + private _createChannel(guid: string): Channel { + const base = new EventEmitter(); + (base as any)._guid = guid; + return new Proxy(base, { + get: (obj: any, prop) => { + if (String(prop).startsWith('_')) + return obj[prop]; + if (prop === 'then') + return obj.then; + if (prop === 'emit') + return obj.emit; + if (prop === 'on') + return obj.on; + if (prop === 'addEventListener') + return obj.addListener; + if (prop === 'removeEventListener') + return obj.removeListener; + return (params: any) => this.sendMessageToServer({ guid, method: String(prop), params }); + }, + }); + } + + private _replaceChannelsWithGuids(payload: any): any { + if (!payload) + return payload; + if (Array.isArray(payload)) + return payload.map(p => this._replaceChannelsWithGuids(p)); + if (payload._guid) + return { guid: payload._guid }; + if (typeof payload === 'object') + return Object.fromEntries([...Object.entries(payload)].map(([n,v]) => [n, this._replaceChannelsWithGuids(v)])); + return payload; + } + + private _replaceGuidsWithChannels(payload: any): any { + if (!payload) + return payload; + if (Array.isArray(payload)) + return payload.map(p => this._replaceGuidsWithChannels(p)); + if (payload.guid && this._channels.has(payload.guid)) + return this._channels.get(payload.guid); + 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 new file mode 100644 index 00000000000000..c731caa2fe3775 --- /dev/null +++ b/src/rpc/dispatcher.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventEmitter } from 'events'; +import { helper } from '../helper'; +import { Channel } from './channels'; + +export class Dispatcher extends EventEmitter implements Channel { + readonly _guid: string; + readonly _type: string; + protected _scope: DispatcherScope; + _object: any; + + constructor(scope: DispatcherScope, object: any, type: string, guid = type + '@' + helper.guid()) { + super(); + this._type = type; + this._guid = guid; + this._object = object; + this._scope = scope; + scope.dispatchers.set(this._guid, this); + object[scope.dispatcherSymbol] = this; + this._scope.sendMessageToClient(this._guid, '__create__', { type }); + } + + _initialize(payload: any) { + this._scope.sendMessageToClient(this._guid, '__init__', payload); + } + + _dispatchEvent(method: string, params: Dispatcher | any = {}) { + this._scope.sendMessageToClient(this._guid, method, params); + } +} + +export class DispatcherScope { + readonly dispatchers = new Map(); + readonly dispatcherSymbol = Symbol('dispatcher'); + sendMessageToClientTransport = (message: any) => {}; + + async sendMessageToClient(guid: string, method: string, params: any): Promise { + this.sendMessageToClientTransport({ guid, method, params: this._replaceDispatchersWithGuids(params) }); + } + + 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; + } + + private _replaceDispatchersWithGuids(payload: any): any { + if (!payload) + return payload; + if (payload instanceof Dispatcher) + return { guid: payload._guid }; + if (Array.isArray(payload)) + return payload.map(p => this._replaceDispatchersWithGuids(p)); + if (typeof payload === 'object') + return Object.fromEntries([...Object.entries(payload)].map(([n,v]) => [n, this._replaceDispatchersWithGuids(v)])); + return payload; + } + + private _replaceGuidsWithDispatchers(payload: any): any { + if (!payload) + return payload; + if (Array.isArray(payload)) + return payload.map(p => this._replaceGuidsWithDispatchers(p)); + if (payload.guid && this.dispatchers.has(payload.guid)) + return this.dispatchers.get(payload.guid); + 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 new file mode 100644 index 00000000000000..4984a885cd4fe9 --- /dev/null +++ b/src/rpc/server/browserContextDispatcher.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as types from '../../types'; +import { BrowserContextBase } from '../../browserContext'; +import { Events } from '../../events'; +import { BrowserDispatcher } from './browserDispatcher'; +import { Dispatcher, DispatcherScope } from '../dispatcher'; +import { PageDispatcher } from './pageDispatcher'; +import { PageChannel, BrowserContextChannel } from '../channels'; + +export class BrowserContextDispatcher extends Dispatcher implements BrowserContextChannel { + private _context: BrowserContextBase; + + static from(scope: DispatcherScope, browserContext: BrowserContextBase): BrowserContextDispatcher { + if ((browserContext as any)[scope.dispatcherSymbol]) + return (browserContext as any)[scope.dispatcherSymbol]; + return new BrowserContextDispatcher(scope, browserContext); + } + + constructor(scope: DispatcherScope, context: BrowserContextBase) { + super(scope, context, 'context'); + this._initialize({ + browser: BrowserDispatcher.from(scope, context._browserBase) + }); + this._context = context; + context.on(Events.BrowserContext.Page, page => this._dispatchEvent('page', PageDispatcher.from(this._scope, page))); + context.on(Events.BrowserContext.Close, () => { + this._dispatchEvent('close'); + }); + } + + async setDefaultNavigationTimeoutNoReply(params: { timeout: number }) { + this._context.setDefaultNavigationTimeout(params.timeout); + } + + async setDefaultTimeoutNoReply(params: { timeout: number }) { + this._context.setDefaultTimeout(params.timeout); + } + + async exposeBinding(params: { name: string }): Promise { + } + + async newPage(): Promise { + return PageDispatcher.from(this._scope, await this._context.newPage()); + } + + async cookies(params: { urls: string[] }): Promise { + return await this._context.cookies(params.urls); + } + + async addCookies(params: { cookies: types.SetNetworkCookieParam[] }): Promise { + await this._context.addCookies(params.cookies); + } + + async clearCookies(): Promise { + await this._context.clearCookies(); + } + + async grantPermissions(params: { permissions: string[], options: { origin?: string } }): Promise { + await this._context.grantPermissions(params.permissions, params.options); + } + + async clearPermissions(): Promise { + await this._context.clearPermissions(); + } + + async setGeolocation(params: { geolocation: types.Geolocation | null }): Promise { + await this._context.setGeolocation(params.geolocation); + } + + async setExtraHTTPHeaders(params: { headers: types.Headers }): Promise { + await this._context.setExtraHTTPHeaders(params.headers); + } + + async setOffline(params: { offline: boolean }): Promise { + await this._context.setOffline(params.offline); + } + + async setHTTPCredentials(params: { httpCredentials: types.Credentials | null }): Promise { + await this._context.setHTTPCredentials(params.httpCredentials); + } + + async addInitScript(params: { source: string }): Promise { + await this._context._doAddInitScript(params.source); + } + + async setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise { + } + + async waitForEvent(params: { event: string }): Promise { + } + + async close(): Promise { + this._context.close(); + } +} diff --git a/src/rpc/server/browserDispatcher.ts b/src/rpc/server/browserDispatcher.ts new file mode 100644 index 00000000000000..1ac9f081d834bf --- /dev/null +++ b/src/rpc/server/browserDispatcher.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BrowserBase } from '../../browser'; +import { BrowserContextBase } from '../../browserContext'; +import * as types from '../../types'; +import { BrowserContextDispatcher } from './browserContextDispatcher'; +import { BrowserChannel, BrowserContextChannel, PageChannel } from '../channels'; +import { Dispatcher, DispatcherScope } from '../dispatcher'; +import { PageDispatcher } from './pageDispatcher'; + +export class BrowserDispatcher extends Dispatcher implements BrowserChannel { + private _browser: BrowserBase; + + static from(scope: DispatcherScope, browser: BrowserBase): BrowserDispatcher { + if ((browser as any)[scope.dispatcherSymbol]) + return (browser as any)[scope.dispatcherSymbol]; + return new BrowserDispatcher(scope, browser); + } + + static fromNullable(scope: DispatcherScope, browser: BrowserBase | null): BrowserDispatcher | null { + if (!browser) + return null; + return BrowserDispatcher.from(scope, browser); + } + + constructor(scope: DispatcherScope, browser: BrowserBase) { + super(scope, browser, 'browser'); + this._initialize({}); + this._browser = browser; + } + + async newContext(params: { options?: types.BrowserContextOptions }): Promise { + return BrowserContextDispatcher.from(this._scope, await this._browser.newContext(params.options) as BrowserContextBase); + } + + async newPage(params: { options?: types.BrowserContextOptions }): Promise { + return PageDispatcher.from(this._scope, await this._browser.newPage(params.options)); + } + + async close(): Promise { + await this._browser.close(); + } +} diff --git a/src/rpc/server/browserTypeDispatcher.ts b/src/rpc/server/browserTypeDispatcher.ts new file mode 100644 index 00000000000000..234d64e89b97d7 --- /dev/null +++ b/src/rpc/server/browserTypeDispatcher.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BrowserBase } from '../../browser'; +import { BrowserTypeBase } from '../../server/browserType'; +import * as types from '../../types'; +import { BrowserDispatcher } from './browserDispatcher'; +import { BrowserChannel, BrowserTypeChannel, BrowserContextChannel } from '../channels'; +import { Dispatcher, DispatcherScope } from '../dispatcher'; +import { BrowserContextBase } from '../../browserContext'; +import { BrowserContextDispatcher } from './browserContextDispatcher'; + +export class BrowserTypeDispatcher extends Dispatcher implements BrowserTypeChannel { + private _browserType: BrowserTypeBase; + + static from(scope: DispatcherScope, browserType: BrowserTypeBase): BrowserTypeDispatcher { + if ((browserType as any)[scope.dispatcherSymbol]) + return (browserType as any)[scope.dispatcherSymbol]; + return new BrowserTypeDispatcher(scope, browserType); + } + + constructor(scope: DispatcherScope, browserType: BrowserTypeBase) { + super(scope, browserType, 'browserType', browserType.name()); + this._initialize({ executablePath: browserType.executablePath(), name: browserType.name() }); + this._browserType = browserType; + } + + async launch(params: { options?: types.LaunchOptions }): Promise { + const browser = await this._browserType.launch(params.options || undefined); + return BrowserDispatcher.from(this._scope, browser as BrowserBase); + } + + async launchPersistentContext(params: { userDataDir: string, options?: types.LaunchOptions & types.BrowserContextOptions }): Promise { + const browserContext = await this._browserType.launchPersistentContext(params.userDataDir, params.options); + return BrowserContextDispatcher.from(this._scope, browserContext as BrowserContextBase); + } + + async connect(params: { options: types.ConnectOptions }): Promise { + const browser = await this._browserType.connect(params.options); + return BrowserDispatcher.from(this._scope, browser as BrowserBase); + } +} diff --git a/src/rpc/server/elementHandlerDispatcher.ts b/src/rpc/server/elementHandlerDispatcher.ts new file mode 100644 index 00000000000000..6c63dbae31861c --- /dev/null +++ b/src/rpc/server/elementHandlerDispatcher.ts @@ -0,0 +1,164 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ElementHandle } from '../../dom'; +import * as js from '../../javascript'; +import * as types from '../../types'; +import { ElementHandleChannel, FrameChannel } from '../channels'; +import { DispatcherScope } from '../dispatcher'; +import { convertArg, FrameDispatcher } from './frameDispatcher'; +import { JSHandleDispatcher } from './jsHandleDispatcher'; + +export class ElementHandleDispatcher extends JSHandleDispatcher implements ElementHandleChannel { + private _elementHandle: ElementHandle; + + static from(scope: DispatcherScope, handle: js.JSHandle): JSHandleDispatcher { + if ((handle as any)[scope.dispatcherSymbol]) + return (handle as any)[scope.dispatcherSymbol]; + return handle.asElement() ? new ElementHandleDispatcher(scope, handle.asElement()!) : new JSHandleDispatcher(scope, handle); + } + + static fromNullable(scope: DispatcherScope, handle: js.JSHandle | null): JSHandleDispatcher | null { + if (!handle) + return null; + return ElementHandleDispatcher.from(scope, handle); + } + + static fromElement(scope: DispatcherScope, handle: ElementHandle): ElementHandleDispatcher { + if ((handle as any)[scope.dispatcherSymbol]) + return (handle as any)[scope.dispatcherSymbol]; + return new ElementHandleDispatcher(scope, handle); + } + + static fromNullableElement(scope: DispatcherScope, handle: ElementHandle | null): ElementHandleDispatcher | null { + if (!handle) + return null; + return ElementHandleDispatcher.fromElement(scope, handle); + } + + + constructor(scope: DispatcherScope, elementHandle: ElementHandle) { + super(scope, elementHandle, true); + this._elementHandle = elementHandle; + this._initialize({ preview: elementHandle.toString(), frame: FrameDispatcher.from(scope, elementHandle._context.frame) }); + this._elementHandle = elementHandle; + } + + async ownerFrame(): Promise { + return FrameDispatcher.fromNullable(this._scope, await this._elementHandle.ownerFrame()); + } + + async contentFrame(): Promise { + return FrameDispatcher.fromNullable(this._scope, await this._elementHandle.contentFrame()); + } + + async getAttribute(params: { name: string }): Promise { + return this._elementHandle.getAttribute(params.name); + } + + async textContent(): Promise { + return this._elementHandle.textContent(); + } + + async innerText(): Promise { + return this._elementHandle.innerText(); + } + + async innerHTML(): Promise { + return this._elementHandle.innerHTML(); + } + + async dispatchEvent(params: { type: string, eventInit: Object }) { + await this._elementHandle.dispatchEvent(params.type, params.eventInit); + } + + async scrollIntoViewIfNeeded(params: { options?: types.TimeoutOptions }) { + await this._elementHandle.scrollIntoViewIfNeeded(params.options); + } + + async hover(params: { options?: types.PointerActionOptions & types.PointerActionWaitOptions }) { + await this._elementHandle.hover(params.options); + } + + async click(params: { options?: types.MouseClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions }) { + await this._elementHandle.click(params.options); + } + + async dblclick(params: { options?: types.MouseMultiClickOptions & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions }) { + await this._elementHandle.dblclick(params.options); + } + + 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); + } + + async fill(params: { value: string, options: types.NavigatingActionWaitOptions }) { + await this._elementHandle.fill(params.value, params.options); + } + + async selectText(params: { options?: types.TimeoutOptions }) { + await this._elementHandle.selectText(params.options); + } + + async setInputFiles(params: { files: string | types.FilePayload | string[] | types.FilePayload[], options?: types.NavigatingActionWaitOptions }) { + await this._elementHandle.setInputFiles(params.files, params.options); + } + + async focus() { + await this._elementHandle.focus(); + } + + async type(params: { text: string, options: { delay?: number } & types.NavigatingActionWaitOptions }) { + await this._elementHandle.type(params.text, params.options); + } + + async press(params: { key: string, options: { delay?: number } & types.NavigatingActionWaitOptions }) { + await this._elementHandle.type(params.key, params.options); + } + + async check(params: { options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions }) { + await this._elementHandle.check(params.options); + } + + async uncheck(params: { options?: types.PointerActionWaitOptions & types.NavigatingActionWaitOptions }) { + await this._elementHandle.uncheck(params.options); + } + + async boundingBox(): Promise { + return await this._elementHandle.boundingBox(); + } + + async screenshot(params: { options?: types.ElementScreenshotOptions }): Promise { + return await this._elementHandle.screenshot(params.options); + } + + async querySelector(params: { selector: string }): Promise { + return ElementHandleDispatcher.fromNullableElement(this._scope, await this._elementHandle.$(params.selector)); + } + + async querySelectorAll(params: { selector: string }): Promise { + const elements = await this._elementHandle.$$(params.selector); + return elements.map(e => ElementHandleDispatcher.fromElement(this._scope, e)); + } + + async $eval(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise { + return this._elementHandle._$evalExpression(params.selector, params.expression, params.isFunction, convertArg(this._scope, params.arg)); + } + + async $$eval(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise { + return this._elementHandle._$$evalExpression(params.selector, params.expression, params.isFunction, convertArg(this._scope, params.arg)); + } +} diff --git a/src/rpc/server/frameDispatcher.ts b/src/rpc/server/frameDispatcher.ts new file mode 100644 index 00000000000000..105bf7dc236b32 --- /dev/null +++ b/src/rpc/server/frameDispatcher.ts @@ -0,0 +1,203 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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 { JSHandleDispatcher } from './jsHandleDispatcher'; +import { ResponseDispatcher } from './networkDispatchers'; +import { PageDispatcher } from './pageDispatcher'; + +export class FrameDispatcher extends Dispatcher implements FrameChannel { + private _frame: Frame; + + static from(scope: DispatcherScope, frame: Frame): FrameDispatcher { + if ((frame as any)[scope.dispatcherSymbol]) + return (frame as any)[scope.dispatcherSymbol]; + return new FrameDispatcher(scope, frame); + } + + static fromNullable(scope: DispatcherScope, frame: Frame | null): FrameDispatcher | null { + if (!frame) + return null; + return FrameDispatcher.from(scope, frame); + } + + constructor(scope: DispatcherScope, frame: Frame) { + super(scope, frame, 'frame'); + 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() + }); + } + + async goto(params: { url: string, options: types.GotoOptions }): Promise { + return ResponseDispatcher.fromNullable(this._scope, await this._frame.goto(params.url, params.options)); + } + + async waitForLoadState(params: { state?: 'load' | 'domcontentloaded' | 'networkidle', options?: types.TimeoutOptions }): Promise { + await this._frame.waitForLoadState(params.state, params.options); + } + + async waitForNavigation(params: { options?: types.WaitForNavigationOptions }): Promise { + return ResponseDispatcher.fromNullable(this._scope, await this._frame.waitForNavigation(params.options)); + } + + async frameElement(): Promise { + return ElementHandleDispatcher.fromElement(this._scope, await this._frame.frameElement()); + } + + async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise { + return this._frame._evaluateExpression(params.expression, params.isFunction, convertArg(this._scope, params.arg)); + } + + async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise { + return ElementHandleDispatcher.fromElement(this._scope, await this._frame._evaluateExpressionHandle(params.expression, params.isFunction, convertArg(this._scope, params.arg))); + } + + async waitForSelector(params: { selector: string, options: types.WaitForElementOptions }): Promise { + return ElementHandleDispatcher.fromNullableElement(this._scope, await this._frame.waitForSelector(params.selector)); + } + + async dispatchEvent(params: { selector: string, type: string, eventInit: Object | undefined, options: types.TimeoutOptions }): Promise { + return this._frame.dispatchEvent(params.selector, params.type, params.eventInit, params.options); + } + + async $eval(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise { + return this._frame._$evalExpression(params.selector, params.expression, params.isFunction, convertArg(this._scope, params.arg)); + } + + async $$eval(params: { selector: string, expression: string, isFunction: boolean, arg: any }): Promise { + return this._frame._$$evalExpression(params.selector, params.expression, params.isFunction, convertArg(this._scope, params.arg)); + } + + async querySelector(params: { selector: string }): Promise { + return ElementHandleDispatcher.fromNullableElement(this._scope, await this._frame.$(params.selector)); + } + + async querySelectorAll(params: { selector: string }): Promise { + const elements = await this._frame.$$(params.selector); + return elements.map(e => ElementHandleDispatcher.fromElement(this._scope, e)); + } + + async content(): Promise { + return await this._frame.content(); + } + + async setContent(params: { html: string, options: types.NavigateOptions }): Promise { + await this._frame.setContent(params.html, params.options); + } + + async addScriptTag(params: { options: { url?: string | undefined, path?: string | undefined, content?: string | undefined, type?: string | undefined } }): Promise { + return ElementHandleDispatcher.fromElement(this._scope, await this._frame.addScriptTag(params.options)); + } + + async addStyleTag(params: { options: { url?: string | undefined, path?: string | undefined, content?: string | undefined } }): Promise { + return ElementHandleDispatcher.fromElement(this._scope, await this._frame.addStyleTag(params.options)); + } + + async click(params: { selector: string, options: types.PointerActionOptions & types.MouseClickOptions & types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise { + await this._frame.click(params.selector, params.options); + } + + async dblclick(params: { selector: string, options: types.PointerActionOptions & types.MouseMultiClickOptions & types.TimeoutOptions & { force?: boolean }}): Promise { + await this._frame.dblclick(params.selector, params.options); + } + + async fill(params: { selector: string, value: string, options: types.NavigatingActionWaitOptions }): Promise { + await this._frame.fill(params.selector, params.value, params.options); + } + + async focus(params: { selector: string, options: types.TimeoutOptions }): Promise { + await this._frame.focus(params.selector, params.options); + } + + async textContent(params: { selector: string, options: types.TimeoutOptions }): Promise { + return await this._frame.textContent(params.selector, params.options); + } + + async innerText(params: { selector: string, options: types.TimeoutOptions }): Promise { + return await this._frame.innerText(params.selector, params.options); + } + + async innerHTML(params: { selector: string, options: types.TimeoutOptions }): Promise { + return await this._frame.innerHTML(params.selector, params.options); + } + + async getAttribute(params: { selector: string, name: string, options: types.TimeoutOptions }): Promise { + return await this._frame.getAttribute(params.selector, params.name, params.options); + } + + async hover(params: { selector: string, options: types.PointerActionOptions & types.TimeoutOptions & { force?: boolean } }): Promise { + await this._frame.hover(params.selector, params.options); + } + + 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); + } + + async setInputFiles(params: { selector: string, files: string | string[] | types.FilePayload | types.FilePayload[], options: types.NavigatingActionWaitOptions }): Promise { + await this._frame.setInputFiles(params.selector, params.files, params.options); + } + + async type(params: { selector: string, text: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise { + await this._frame.type(params.selector, params.text, params.options); + } + + async press(params: { selector: string, key: string, options: { delay?: number | undefined } & types.TimeoutOptions & { noWaitAfter?: boolean } }): Promise { + await this._frame.press(params.selector, params.key, params.options); + } + + async check(params: { selector: string, options: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise { + await this._frame.check(params.selector, params.options); + } + + async uncheck(params: { selector: string, options: types.TimeoutOptions & { force?: boolean } & { noWaitAfter?: boolean } }): Promise { + await this._frame.uncheck(params.selector, params.options); + } + + async waitForFunction(params: { expression: string, isFunction: boolean, arg: any; options: types.WaitForFunctionOptions }): Promise { + return ElementHandleDispatcher.from(this._scope, await this._frame._waitForFunctionExpression(params.expression, params.isFunction, convertArg(this._scope, params.arg), params.options)); + } + + async title(): Promise { + return await this._frame.title(); + } +} + +export function convertArg(scope: DispatcherScope, arg: any): any { + if (arg === null) + return null; + if (Array.isArray(arg)) + return arg.map(item => convertArg(scope, item)); + if (arg instanceof JSHandleDispatcher) + return arg._object; + if (typeof arg === 'object') { + const result: any = {}; + for (const key of Object.keys(arg)) + result[key] = convertArg(scope, arg[key]); + return result; + } + return arg; +} diff --git a/src/rpc/server/jsHandleDispatcher.ts b/src/rpc/server/jsHandleDispatcher.ts new file mode 100644 index 00000000000000..4ec0a0c70a6d23 --- /dev/null +++ b/src/rpc/server/jsHandleDispatcher.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as js from '../../javascript'; +import { JSHandleChannel } from '../channels'; +import { Dispatcher, DispatcherScope } from '../dispatcher'; +import { convertArg } from './frameDispatcher'; + +export class JSHandleDispatcher extends Dispatcher implements JSHandleChannel { + readonly _jsHandle: js.JSHandle; + + constructor(scope: DispatcherScope, jsHandle: js.JSHandle, omitInit?: boolean) { + super(scope, jsHandle, jsHandle.asElement() ? 'elementHandle' : 'jsHandle'); + if (!omitInit) + this._initialize({ preview: jsHandle.toString() }); + this._jsHandle = jsHandle; + } + + async evaluateExpression(params: { expression: string, isFunction: boolean, arg: any }): Promise { + return this._jsHandle._evaluateExpression(params.expression, params.isFunction, true /* returnByValue */, convertArg(this._scope, params.arg)); + } + + async evaluateExpressionHandle(params: { expression: string, isFunction: boolean, arg: any}): Promise { + const jsHandle = await this._jsHandle._evaluateExpression(params.expression, params.isFunction, false /* returnByValue */, convertArg(this._scope, params.arg)); + return new JSHandleDispatcher(this._scope, jsHandle); + } + + async getPropertyList(): Promise<{ name: string, value: JSHandleChannel }[]> { + const map = await this._jsHandle.getProperties(); + const result = []; + for (const [name, value] of map) + result.push({ name, value: new JSHandleDispatcher(this._scope, value) }); + return result; + } + + async jsonValue(): Promise { + return this._jsHandle.jsonValue(); + } + + async dispose() { + await this._jsHandle.dispose(); + } +} diff --git a/src/rpc/server/networkDispatchers.ts b/src/rpc/server/networkDispatchers.ts new file mode 100644 index 00000000000000..0b79054b90bfaa --- /dev/null +++ b/src/rpc/server/networkDispatchers.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Request, Response } from '../../network'; +import * as types from '../../types'; +import { RequestChannel, ResponseChannel } from '../channels'; +import { Dispatcher, DispatcherScope } from '../dispatcher'; +import { FrameDispatcher } from './frameDispatcher'; + +export class RequestDispatcher extends Dispatcher implements RequestChannel { + private _request: Request; + + static from(scope: DispatcherScope, request: Request): RequestDispatcher { + if ((request as any)[scope.dispatcherSymbol]) + return (request as any)[scope.dispatcherSymbol]; + return new RequestDispatcher(scope, request); + } + + static fromNullable(scope: DispatcherScope, request: Request | null): RequestDispatcher | null { + return request ? RequestDispatcher.from(scope, request) : null; + } + + constructor(scope: DispatcherScope, request: Request) { + super(scope, request, 'request'); + this._initialize({ + url: request.url(), + resourceType: request.resourceType(), + method: request.method(), + postData: request.postData(), + headers: request.headers(), + isNavigationRequest: request.isNavigationRequest(), + failure: request.failure(), + frame: FrameDispatcher.from(this._scope, request.frame()), + redirectedFrom: RequestDispatcher.fromNullable(this._scope, request.redirectedFrom()), + redirectedTo: RequestDispatcher.fromNullable(this._scope, request.redirectedTo()), + }); + 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()); + } +} + +export class ResponseDispatcher extends Dispatcher implements ResponseChannel { + private _response: Response; + + static from(scope: DispatcherScope, response: Response): ResponseDispatcher { + if ((response as any)[scope.dispatcherSymbol]) + return (response as any)[scope.dispatcherSymbol]; + return new ResponseDispatcher(scope, response); + } + + static fromNullable(scope: DispatcherScope, response: Response | null): ResponseDispatcher | null { + return response ? ResponseDispatcher.from(scope, response) : null; + } + + constructor(scope: DispatcherScope, response: Response) { + super(scope, response, 'response'); + this._initialize({ + frame: FrameDispatcher.from(this._scope, response.frame()), + request: RequestDispatcher.from(this._scope, response.request())!, + url: response.url(), + ok: response.ok(), + status: response.status(), + statusText: response.statusText(), + headers: response.headers(), + }); + this._response = response; + } + + async finished(): Promise { + return await this._response.finished(); + } + + async body(): Promise { + return await this._response.body(); + } +} diff --git a/src/rpc/server/pageDispatcher.ts b/src/rpc/server/pageDispatcher.ts new file mode 100644 index 00000000000000..0e35233090f9ce --- /dev/null +++ b/src/rpc/server/pageDispatcher.ts @@ -0,0 +1,137 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the 'License'); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an 'AS IS' BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Events } from '../../events'; +import { Frame } from '../../frames'; +import { Page } from '../../page'; +import * as types from '../../types'; +import { PageChannel, ResponseChannel } from '../channels'; +import { Dispatcher, DispatcherScope } from '../dispatcher'; +import { BrowserContextDispatcher } from './browserContextDispatcher'; +import { FrameDispatcher } from './frameDispatcher'; +import { RequestDispatcher, ResponseDispatcher } from './networkDispatchers'; + +export class PageDispatcher extends Dispatcher implements PageChannel { + private _page: Page; + + static from(scope: DispatcherScope, page: Page): PageDispatcher { + if ((page as any)[scope.dispatcherSymbol]) + return (page as any)[scope.dispatcherSymbol]; + return new PageDispatcher(scope, page); + } + + static fromNullable(scope: DispatcherScope, page: Page | null): PageDispatcher | null { + if (!page) + return null; + return PageDispatcher.from(scope, page); + } + + constructor(scope: DispatcherScope, page: Page) { + super(scope, page, 'page'); + this._initialize({ + browserContext: BrowserContextDispatcher.from(scope, page._browserContext), + mainFrame: FrameDispatcher.from(scope, page.mainFrame()), + frames: page.frames().map(f => FrameDispatcher.from(this._scope, f)), + }); + this._page = page; + 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.Request, request => this._dispatchEvent('request', 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))); + } + + async setDefaultNavigationTimeoutNoReply(params: { timeout: number }) { + this._page.setDefaultNavigationTimeout(params.timeout); + } + + async setDefaultTimeoutNoReply(params: { timeout: number }) { + this._page.setDefaultTimeout(params.timeout); + } + + async opener(): Promise { + return PageDispatcher.fromNullable(this._scope, await this._page.opener()); + } + + async exposeBinding(params: { name: string }): Promise { + } + + async setExtraHTTPHeaders(params: { headers: types.Headers }): Promise { + await this._page.setExtraHTTPHeaders(params.headers); + } + + async reload(params: { options?: types.NavigateOptions }): Promise { + return ResponseDispatcher.fromNullable(this._scope, await this._page.reload(params.options)); + } + + async waitForEvent(params: { event: string }): Promise { + } + + async goBack(params: { options?: types.NavigateOptions }): Promise { + return ResponseDispatcher.fromNullable(this._scope, await this._page.goBack(params.options)); + } + + async goForward(params: { options?: types.NavigateOptions }): Promise { + return ResponseDispatcher.fromNullable(this._scope, await this._page.goForward(params.options)); + } + + async emulateMedia(params: { options: { media?: 'screen' | 'print', colorScheme?: 'dark' | 'light' | 'no-preference' } }): Promise { + await this._page.emulateMedia(params.options); + } + + async setViewportSize(params: { viewportSize: types.Size }): Promise { + await this._page.setViewportSize(params.viewportSize); + } + + async addInitScript(params: { source: string }): Promise { + await this._page._addInitScriptExpression(params.source); + } + + async setNetworkInterceptionEnabled(params: { enabled: boolean }): Promise { + } + + async screenshot(params: { options?: types.ScreenshotOptions }): Promise { + return await this._page.screenshot(params.options); + } + + async close(params: { options?: { runBeforeUnload?: boolean } }): Promise { + await this._page.close(params.options); + } + + async setFileChooserInterceptedNoReply(params: { intercepted: boolean }) { + } + + async title() { + return await this._page.title(); + } + + _onFrameAttached(frame: Frame) { + this._dispatchEvent('frameAttached', FrameDispatcher.from(this._scope, frame)); + } + + _onFrameNavigated(frame: Frame) { + this._dispatchEvent('frameNavigated', { frame: FrameDispatcher.from(this._scope, frame), url: frame.url() }); + } + + _onFrameDetached(frame: Frame) { + this._dispatchEvent('frameDetached', FrameDispatcher.from(this._scope, frame)); + } +}