From 225e65e07644e7a1c2067fef8146917424afdf36 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Wed, 23 Dec 2020 14:15:16 -0800 Subject: [PATCH] feat(cli): share console api between cli and debug mode (#4807) --- src/cli/cli.ts | 2 ++ src/client/browserContext.ts | 5 ++++ src/client/frame.ts | 1 + src/debug/debugController.ts | 23 ++-------------- src/debug/injected/consoleApi.ts | 4 +++ ...config.js => consoleApi.webpack.config.js} | 6 ++--- src/debug/injected/debugScript.ts | 27 ------------------- src/dispatchers/browserContextDispatcher.ts | 5 ++++ src/protocol/channels.ts | 9 +++++++ src/protocol/protocol.yml | 6 +++++ src/protocol/validator.ts | 4 +++ src/server/browserContext.ts | 10 +++++++ test/selector-generator.spec.ts | 6 ++--- utils/check_deps.js | 2 +- utils/runWebpack.js | 2 +- 15 files changed, 56 insertions(+), 56 deletions(-) rename src/debug/injected/{debugScript.webpack.config.js => consoleApi.webpack.config.js} (91%) delete mode 100644 src/debug/injected/debugScript.ts diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 633bcf55ca..5df78587bb 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -23,6 +23,7 @@ import * as program from 'commander'; import * as os from 'os'; import * as fs from 'fs'; import { installBrowsersWithProgressBar } from '../install/installer'; +import * as consoleApiSource from '../generated/consoleApiSource'; // TODO: we can import from '../..' instead, but that requires generating types // before build, and currently type generator depends on the build. @@ -284,6 +285,7 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi async function open(options: Options, url: string | undefined) { const { context } = await launchContext(options, false); + context._extendInjectedScript(consoleApiSource.source); await openPage(context, url); if (process.env.PWCLI_EXIT_FOR_TEST) await Promise.all(context.pages().map(p => p.close())); diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index fd1a493b2e..ea6ef97a98 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -29,6 +29,7 @@ import { Waiter } from './waiter'; import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState } from './types'; import { isUnderTest, headersObjectToArray, mkdirIfNeeded } from '../utils/utils'; import { isSafeCloseError } from '../utils/errors'; +import { serializeArgument } from './jsHandle'; const fsWriteFileAsync = util.promisify(fs.writeFile.bind(fs)); const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); @@ -253,6 +254,10 @@ export class BrowserContext extends ChannelOwner(source: string, arg?: Arg) { + await this._channel.extendInjectedScript({ source, arg: serializeArgument(arg) }); + } } export async function prepareBrowserContextOptions(options: BrowserContextOptions): Promise { diff --git a/src/client/frame.ts b/src/client/frame.ts index 8f85e9347b..17fd0f46b0 100644 --- a/src/client/frame.ts +++ b/src/client/frame.ts @@ -435,6 +435,7 @@ export class Frame extends ChannelOwner(source: string, arg?: Arg): Promise { const result = await this._channel.extendInjectedScript({ source, arg: serializeArgument(arg) }); return JSHandle.from(result.handle); diff --git a/src/debug/debugController.ts b/src/debug/debugController.ts index dd3747be0e..ff89c94571 100644 --- a/src/debug/debugController.ts +++ b/src/debug/debugController.ts @@ -15,10 +15,8 @@ */ import { BrowserContext, ContextListener, contextListeners } from '../server/browserContext'; -import * as frames from '../server/frames'; -import { Page } from '../server/page'; import { isDebugMode } from '../utils/utils'; -import * as debugScriptSource from '../generated/debugScriptSource'; +import * as consoleApiSource from '../generated/consoleApiSource'; export function installDebugController() { contextListeners.add(new DebugController()); @@ -27,25 +25,8 @@ export function installDebugController() { class DebugController implements ContextListener { async onContextCreated(context: BrowserContext): Promise { if (isDebugMode()) - installDebugControllerInContext(context); + context.extendInjectedScript(consoleApiSource.source); } async onContextWillDestroy(context: BrowserContext): Promise {} async onContextDidDestroy(context: BrowserContext): Promise {} } - -async function ensureInstalledInFrame(frame: frames.Frame) { - try { - await frame.extendInjectedScript(debugScriptSource.source); - } catch (e) { - } -} - -async function installInPage(page: Page) { - page.on(Page.Events.FrameNavigated, ensureInstalledInFrame); - await Promise.all(page.frames().map(ensureInstalledInFrame)); -} - -export async function installDebugControllerInContext(context: BrowserContext) { - context.on(BrowserContext.Events.Page, installInPage); - await Promise.all(context.pages().map(installInPage)); -} diff --git a/src/debug/injected/consoleApi.ts b/src/debug/injected/consoleApi.ts index 851e5b5779..1c0c03b2a4 100644 --- a/src/debug/injected/consoleApi.ts +++ b/src/debug/injected/consoleApi.ts @@ -22,6 +22,8 @@ export class ConsoleAPI { constructor(injectedScript: InjectedScript) { this._injectedScript = injectedScript; + if ((window as any).playwright) + return; (window as any).playwright = { $: (selector: string) => this._querySelector(selector), $$: (selector: string) => this._querySelectorAll(selector), @@ -58,3 +60,5 @@ export class ConsoleAPI { return generateSelector(this._injectedScript, element).selector; } } + +export default ConsoleAPI; diff --git a/src/debug/injected/debugScript.webpack.config.js b/src/debug/injected/consoleApi.webpack.config.js similarity index 91% rename from src/debug/injected/debugScript.webpack.config.js rename to src/debug/injected/consoleApi.webpack.config.js index d2564351c4..b15af4b766 100644 --- a/src/debug/injected/debugScript.webpack.config.js +++ b/src/debug/injected/consoleApi.webpack.config.js @@ -18,7 +18,7 @@ const path = require('path'); const InlineSource = require('../../server/injected/webpack-inline-source-plugin'); module.exports = { - entry: path.join(__dirname, 'debugScript.ts'), + entry: path.join(__dirname, 'consoleApi.ts'), devtool: 'source-map', module: { rules: [ @@ -37,10 +37,10 @@ module.exports = { }, output: { libraryTarget: 'var', - filename: 'debugScriptSource.js', + filename: 'consoleApiSource.js', path: path.resolve(__dirname, '../../../lib/server/injected/packed') }, plugins: [ - new InlineSource(path.join(__dirname, '..', '..', 'generated', 'debugScriptSource.ts')), + new InlineSource(path.join(__dirname, '..', '..', 'generated', 'consoleApiSource.ts')), ] }; diff --git a/src/debug/injected/debugScript.ts b/src/debug/injected/debugScript.ts deleted file mode 100644 index 8645480608..0000000000 --- a/src/debug/injected/debugScript.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * 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 { ConsoleAPI } from './consoleApi'; -import type InjectedScript from '../../server/injected/injectedScript'; - -export default class DebugScript { - consoleAPI: ConsoleAPI | undefined; - constructor(injectedScript: InjectedScript) { - if ((window as any).playwright) - return; - this.consoleAPI = new ConsoleAPI(injectedScript); - } -} diff --git a/src/dispatchers/browserContextDispatcher.ts b/src/dispatchers/browserContextDispatcher.ts index 31b53971d4..d2d1eed39a 100644 --- a/src/dispatchers/browserContextDispatcher.ts +++ b/src/dispatchers/browserContextDispatcher.ts @@ -21,6 +21,7 @@ import * as channels from '../protocol/channels'; import { RouteDispatcher, RequestDispatcher } from './networkDispatchers'; import { CRBrowserContext } from '../server/chromium/crBrowser'; import { CDPSessionDispatcher } from './cdpSessionDispatcher'; +import { parseArgument } from './jsHandleDispatcher'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { private _context: BrowserContext; @@ -125,6 +126,10 @@ export class BrowserContextDispatcher extends Dispatcher { + await this._context.extendInjectedScript(params.source, parseArgument(params.arg)); + } + async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise { if (this._object._browser._options.name !== 'chromium') throw new Error(`CDP session is only available in Chromium`); diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index d4dedb5c68..8343f7a60c 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -556,6 +556,7 @@ export interface BrowserContextChannel extends Channel { setNetworkInterceptionEnabled(params: BrowserContextSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise; setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise; storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise; + extendInjectedScript(params: BrowserContextExtendInjectedScriptParams, metadata?: Metadata): Promise; crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise; } export type BrowserContextBindingCallEvent = { @@ -697,6 +698,14 @@ export type BrowserContextStorageStateResult = { cookies: NetworkCookie[], origins: OriginStorage[], }; +export type BrowserContextExtendInjectedScriptParams = { + source: string, + arg: SerializedArgument, +}; +export type BrowserContextExtendInjectedScriptOptions = { + +}; +export type BrowserContextExtendInjectedScriptResult = void; export type BrowserContextCrNewCDPSessionParams = { page: PageChannel, }; diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 402e49c731..41676535a3 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -601,6 +601,12 @@ BrowserContext: type: array items: OriginStorage + extendInjectedScript: + experimental: True + parameters: + source: string + arg: SerializedArgument + crNewCDPSession: parameters: page: Page diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 210627e4ae..bacf62d2f6 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -337,6 +337,10 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { offline: tBoolean, }); scheme.BrowserContextStorageStateParams = tOptional(tObject({})); + scheme.BrowserContextExtendInjectedScriptParams = tObject({ + source: tString, + arg: tType('SerializedArgument'), + }); scheme.BrowserContextCrNewCDPSessionParams = tObject({ page: tChannel('Page'), }); diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 9dcb98fdb4..24acdedca5 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -378,6 +378,16 @@ export abstract class BrowserContext extends EventEmitter { await page.close(); } } + + async extendInjectedScript(source: string, arg?: any) { + const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(source, arg).catch(e => {}); + const installInPage = (page: Page) => { + page.on(Page.Events.FrameNavigated, installInFrame); + return Promise.all(page.frames().map(installInFrame)); + }; + this.on(BrowserContext.Events.Page, installInPage); + return Promise.all(this.pages().map(installInPage)); + } } export function assertBrowserContextIsNotOwned(context: BrowserContext) { diff --git a/test/selector-generator.spec.ts b/test/selector-generator.spec.ts index f3c2d18e56..0abfe35ec8 100644 --- a/test/selector-generator.spec.ts +++ b/test/selector-generator.spec.ts @@ -18,11 +18,11 @@ import { folio } from './fixtures'; import * as path from 'path'; import type { Page, Frame } from '..'; -const { installDebugControllerInContext } = require(path.join(__dirname, '..', 'lib', 'debug', 'debugController')); +const { source } = require(path.join(__dirname, '..', 'lib', 'generated', 'consoleApiSource')); const fixtures = folio.extend(); -fixtures.context.override(async ({ context, toImpl }, run) => { - await installDebugControllerInContext(toImpl(context)); +fixtures.context.override(async ({ context }, run) => { + await (context as any)._extendInjectedScript(source); await run(context); }); const { describe, it, expect } = fixtures.build(); diff --git a/utils/check_deps.js b/utils/check_deps.js index 4604cc964b..dcf523ac18 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -136,7 +136,7 @@ DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/ser DEPS['src/service.ts'] = ['src/remote/']; // CLI should only use client-side features. -DEPS['src/cli/'] = ['src/client/**', 'src/install/**']; +DEPS['src/cli/'] = ['src/client/**', 'src/install/**', 'src/generated/']; checkDeps().catch(e => { console.error(e && e.stack ? e.stack : e); diff --git a/utils/runWebpack.js b/utils/runWebpack.js index 01f6de5760..5437f32f19 100644 --- a/utils/runWebpack.js +++ b/utils/runWebpack.js @@ -20,7 +20,7 @@ const path = require('path'); const files = [ path.join('src', 'server', 'injected', 'injectedScript.webpack.config.js'), path.join('src', 'server', 'injected', 'utilityScript.webpack.config.js'), - path.join('src', 'debug', 'injected', 'debugScript.webpack.config.js'), + path.join('src', 'debug', 'injected', 'consoleApi.webpack.config.js'), ]; function runOne(runner, file) {