feat(cli): share console api between cli and debug mode (#4807)

This commit is contained in:
Dmitry Gozman 2020-12-23 14:15:16 -08:00 committed by GitHub
parent f709e2300c
commit 225e65e076
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 56 additions and 56 deletions

View File

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

View File

@ -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<channels.BrowserContextChannel,
throw e;
}
}
async _extendInjectedScript<Arg>(source: string, arg?: Arg) {
await this._channel.extendInjectedScript({ source, arg: serializeArgument(arg) });
}
}
export async function prepareBrowserContextOptions(options: BrowserContextOptions): Promise<channels.BrowserNewContextOptions> {

View File

@ -435,6 +435,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel, channels.FrameIni
});
}
// TODO: remove once playwright-cli does not use this one anymore.
async _extendInjectedScript<Arg>(source: string, arg?: Arg): Promise<JSHandle> {
const result = await this._channel.extendInjectedScript({ source, arg: serializeArgument(arg) });
return JSHandle.from(result.handle);

View File

@ -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<void> {
if (isDebugMode())
installDebugControllerInContext(context);
context.extendInjectedScript(consoleApiSource.source);
}
async onContextWillDestroy(context: BrowserContext): Promise<void> {}
async onContextDidDestroy(context: BrowserContext): Promise<void> {}
}
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));
}

View File

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

View File

@ -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')),
]
};

View File

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

View File

@ -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<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel {
private _context: BrowserContext;
@ -125,6 +126,10 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
await this._context.close();
}
async extendInjectedScript(params: channels.BrowserContextExtendInjectedScriptParams): Promise<void> {
await this._context.extendInjectedScript(params.source, parseArgument(params.arg));
}
async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise<channels.BrowserContextCrNewCDPSessionResult> {
if (this._object._browser._options.name !== 'chromium')
throw new Error(`CDP session is only available in Chromium`);

View File

@ -556,6 +556,7 @@ export interface BrowserContextChannel extends Channel {
setNetworkInterceptionEnabled(params: BrowserContextSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise<BrowserContextSetNetworkInterceptionEnabledResult>;
setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise<BrowserContextSetOfflineResult>;
storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise<BrowserContextStorageStateResult>;
extendInjectedScript(params: BrowserContextExtendInjectedScriptParams, metadata?: Metadata): Promise<BrowserContextExtendInjectedScriptResult>;
crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise<BrowserContextCrNewCDPSessionResult>;
}
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,
};

View File

@ -601,6 +601,12 @@ BrowserContext:
type: array
items: OriginStorage
extendInjectedScript:
experimental: True
parameters:
source: string
arg: SerializedArgument
crNewCDPSession:
parameters:
page: Page

View File

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

View File

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

View File

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

View File

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

View File

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