/** * 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 { BrowserContext } from '../../browserContext'; import { Frame } from '../../frames'; import { Request } from '../../network'; import { Page, Worker } from '../../page'; import * as channels from '../../protocol/channels'; import { Dispatcher, DispatcherScope, lookupDispatcher, lookupNullableDispatcher } from './dispatcher'; import { parseError, serializeError } from '../../protocol/serializers'; import { ConsoleMessageDispatcher } from './consoleMessageDispatcher'; import { DialogDispatcher } from './dialogDispatcher'; import { DownloadDispatcher } from './downloadDispatcher'; import { FrameDispatcher } from './frameDispatcher'; import { RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers'; import { serializeResult, parseArgument } from './jsHandleDispatcher'; import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher'; import { FileChooser } from '../../fileChooser'; import { CRCoverage } from '../../chromium/crCoverage'; export class PageDispatcher extends Dispatcher implements channels.PageChannel { private _page: Page; constructor(scope: DispatcherScope, page: Page) { // TODO: theoretically, there could be more than one frame already. // If we split pageCreated and pageReady, there should be no main frame during pageCreated. super(scope, page, 'Page', { mainFrame: FrameDispatcher.from(scope, page.mainFrame()), viewportSize: page.viewportSize() || undefined, isClosed: page.isClosed() }); this._page = page; page.on(Page.Events.Close, () => this._dispatchEvent('close')); page.on(Page.Events.Console, message => this._dispatchEvent('console', { message: new ConsoleMessageDispatcher(this._scope, message) })); page.on(Page.Events.Crash, () => this._dispatchEvent('crash')); page.on(Page.Events.DOMContentLoaded, () => this._dispatchEvent('domcontentloaded')); page.on(Page.Events.Dialog, dialog => this._dispatchEvent('dialog', { dialog: new DialogDispatcher(this._scope, dialog) })); page.on(Page.Events.Download, dialog => this._dispatchEvent('download', { download: new DownloadDispatcher(this._scope, dialog) })); this._page.on(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', { element: new ElementHandleDispatcher(this._scope, fileChooser.element()), isMultiple: fileChooser.isMultiple() })); page.on(Page.Events.FrameAttached, frame => this._onFrameAttached(frame)); page.on(Page.Events.FrameDetached, frame => this._onFrameDetached(frame)); page.on(Page.Events.Load, () => this._dispatchEvent('load')); page.on(Page.Events.PageError, error => this._dispatchEvent('pageError', { error: serializeError(error) })); page.on(Page.Events.Popup, page => this._dispatchEvent('popup', { page: lookupDispatcher(page) })); page.on(Page.Events.Request, request => this._dispatchEvent('request', { request: RequestDispatcher.from(this._scope, request) })); page.on(Page.Events.RequestFailed, (request: Request) => this._dispatchEvent('requestFailed', { request: RequestDispatcher.from(this._scope, request), failureText: request._failureText })); page.on(Page.Events.RequestFinished, request => this._dispatchEvent('requestFinished', { request: RequestDispatcher.from(scope, request) })); page.on(Page.Events.Response, response => this._dispatchEvent('response', { response: new ResponseDispatcher(this._scope, response) })); page.on(Page.Events.Worker, worker => this._dispatchEvent('worker', { worker: new WorkerDispatcher(this._scope, worker) })); } async setDefaultNavigationTimeoutNoReply(params: channels.PageSetDefaultNavigationTimeoutNoReplyParams): Promise { this._page.setDefaultNavigationTimeout(params.timeout); } async setDefaultTimeoutNoReply(params: channels.PageSetDefaultTimeoutNoReplyParams): Promise { this._page.setDefaultTimeout(params.timeout); } async opener(): Promise { return { page: lookupNullableDispatcher(await this._page.opener()) }; } async exposeBinding(params: channels.PageExposeBindingParams): Promise { await this._page.exposeBinding(params.name, (source, ...args) => { const binding = new BindingCallDispatcher(this._scope, params.name, source, args); this._dispatchEvent('bindingCall', { binding }); return binding.promise(); }); } async setExtraHTTPHeaders(params: channels.PageSetExtraHTTPHeadersParams): Promise { await this._page.setExtraHTTPHeaders(params.headers); } async reload(params: channels.PageReloadParams): Promise { return { response: lookupNullableDispatcher(await this._page.reload(params)) }; } async goBack(params: channels.PageGoBackParams): Promise { return { response: lookupNullableDispatcher(await this._page.goBack(params)) }; } async goForward(params: channels.PageGoForwardParams): Promise { return { response: lookupNullableDispatcher(await this._page.goForward(params)) }; } async emulateMedia(params: channels.PageEmulateMediaParams): Promise { await this._page.emulateMedia({ media: params.media === 'null' ? null : params.media, colorScheme: params.colorScheme === 'null' ? null : params.colorScheme, }); } async setViewportSize(params: channels.PageSetViewportSizeParams): Promise { await this._page.setViewportSize(params.viewportSize); } async addInitScript(params: channels.PageAddInitScriptParams): Promise { await this._page._addInitScriptExpression(params.source); } async setNetworkInterceptionEnabled(params: channels.PageSetNetworkInterceptionEnabledParams): Promise { if (!params.enabled) { await this._page._setRequestInterceptor(undefined); return; } this._page._setRequestInterceptor((route, request) => { this._dispatchEvent('route', { route: new RouteDispatcher(this._scope, route), request: RequestDispatcher.from(this._scope, request) }); }); } async screenshot(params: channels.PageScreenshotParams): Promise { return { binary: (await this._page.screenshot(params)).toString('base64') }; } async close(params: channels.PageCloseParams): Promise { await this._page.close(params); } async setFileChooserInterceptedNoReply(params: channels.PageSetFileChooserInterceptedNoReplyParams): Promise { await this._page._setFileChooserIntercepted(params.intercepted); } async keyboardDown(params: channels.PageKeyboardDownParams): Promise { await this._page.keyboard.down(params.key); } async keyboardUp(params: channels.PageKeyboardUpParams): Promise { await this._page.keyboard.up(params.key); } async keyboardInsertText(params: channels.PageKeyboardInsertTextParams): Promise { await this._page.keyboard.insertText(params.text); } async keyboardType(params: channels.PageKeyboardTypeParams): Promise { await this._page.keyboard.type(params.text, params); } async keyboardPress(params: channels.PageKeyboardPressParams): Promise { await this._page.keyboard.press(params.key, params); } async mouseMove(params: channels.PageMouseMoveParams): Promise { await this._page.mouse.move(params.x, params.y, params); } async mouseDown(params: channels.PageMouseDownParams): Promise { await this._page.mouse.down(params); } async mouseUp(params: channels.PageMouseUpParams): Promise { await this._page.mouse.up(params); } async mouseClick(params: channels.PageMouseClickParams): Promise { await this._page.mouse.click(params.x, params.y, params); } async accessibilitySnapshot(params: channels.PageAccessibilitySnapshotParams): Promise { const rootAXNode = await this._page.accessibility.snapshot({ interestingOnly: params.interestingOnly, root: params.root ? (params.root as ElementHandleDispatcher)._elementHandle : undefined }); return { rootAXNode: rootAXNode || undefined }; } async pdf(params: channels.PagePdfParams): Promise { if (!this._page.pdf) throw new Error('PDF generation is only supported for Headless Chromium'); const buffer = await this._page.pdf(params); return { pdf: buffer.toString('base64') }; } async bringToFront(): Promise { await this._page.bringToFront(); } async crStartJSCoverage(params: channels.PageCrStartJSCoverageParams): Promise { const coverage = this._page.coverage as CRCoverage; await coverage.startJSCoverage(params); } async crStopJSCoverage(): Promise { const coverage = this._page.coverage as CRCoverage; return { entries: await coverage.stopJSCoverage() }; } async crStartCSSCoverage(params: channels.PageCrStartCSSCoverageParams): Promise { const coverage = this._page.coverage as CRCoverage; await coverage.startCSSCoverage(params); } async crStopCSSCoverage(): Promise { const coverage = this._page.coverage as CRCoverage; return { entries: await coverage.stopCSSCoverage() }; } _onFrameAttached(frame: Frame) { this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this._scope, frame) }); } _onFrameDetached(frame: Frame) { this._dispatchEvent('frameDetached', { frame: lookupDispatcher(frame) }); } } export class WorkerDispatcher extends Dispatcher implements channels.WorkerChannel { constructor(scope: DispatcherScope, worker: Worker) { super(scope, worker, 'Worker', { url: worker.url() }); worker.on(Worker.Events.Close, () => this._dispatchEvent('close')); } async evaluateExpression(params: channels.WorkerEvaluateExpressionParams): Promise { return { value: serializeResult(await this._object._evaluateExpression(params.expression, params.isFunction, parseArgument(params.arg))) }; } async evaluateExpressionHandle(params: channels.WorkerEvaluateExpressionHandleParams): Promise { return { handle: createHandle(this._scope, await this._object._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg))) }; } } export class BindingCallDispatcher extends Dispatcher<{}, channels.BindingCallInitializer> implements channels.BindingCallChannel { private _resolve: ((arg: any) => void) | undefined; private _reject: ((error: any) => void) | undefined; private _promise: Promise; constructor(scope: DispatcherScope, name: string, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) { super(scope, {}, 'BindingCall', { frame: lookupDispatcher(source.frame), name, args: args.map(serializeResult), }); this._promise = new Promise((resolve, reject) => { this._resolve = resolve; this._reject = reject; }); } promise() { return this._promise; } async resolve(params: channels.BindingCallResolveParams): Promise { this._resolve!(parseArgument(params.result)); } async reject(params: channels.BindingCallRejectParams): Promise { this._reject!(parseError(params.error)); } }