From 464fdc180093cfb207bb08d44ccdd437964b2df1 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sun, 24 Jan 2021 19:21:19 -0800 Subject: [PATCH] chore: make recorder a supplement (#5131) --- src/cli/cli.ts | 69 ++++++------ src/cli/driver.ts | 2 +- src/client/browserContext.ts | 8 -- .../supplements/consoleApiSupplement.ts | 23 ++++ .../supplements/recorderOutputs.ts} | 14 +-- src/client/supplements/recorderSupplement.ts | 51 +++++++++ src/dispatchers/browserContextDispatcher.ts | 15 ++- src/inprocess.ts | 2 +- src/protocol/channels.ts | 33 ++++-- src/protocol/protocol.yml | 16 ++- src/protocol/validator.ts | 10 +- src/remote/playwrightServer.ts | 2 +- src/server/browserContext.ts | 12 +-- .../supplements/consoleApiSupplement.ts | 30 ++++++ .../injected/consoleApi.ts | 0 .../injected/consoleApi.webpack.config.js | 0 .../injected/html.ts | 0 .../injected/recorder.ts | 16 +-- .../injected/recorder.webpack.config.js | 0 .../injected/selectorGenerator.ts | 0 .../inspectorController.ts | 7 +- .../supplements/recorder}/codeGenerator.ts | 10 +- .../supplements/recorder}/csharp.ts | 24 ++--- .../supplements/recorder}/javascript.ts | 23 ++-- .../supplements/recorder/language.ts} | 15 ++- .../supplements/recorder}/python.ts | 24 ++--- .../supplements/recorder}/recorderActions.ts | 0 .../supplements/recorder}/utils.ts | 4 +- .../supplements/recorderSupplement.ts} | 101 +++++++++++------- test/cli/cli-codegen.spec.ts | 29 ++--- test/cli/cli.fixtures.ts | 14 +-- test/selector-generator.spec.ts | 3 +- utils/build/build.js | 4 +- utils/check_deps.js | 6 +- 34 files changed, 341 insertions(+), 226 deletions(-) create mode 100644 src/client/supplements/consoleApiSupplement.ts rename src/{cli/codegen/outputs.ts => client/supplements/recorderOutputs.ts} (92%) create mode 100644 src/client/supplements/recorderSupplement.ts create mode 100644 src/server/supplements/consoleApiSupplement.ts rename src/server/{inspector => supplements}/injected/consoleApi.ts (100%) rename src/server/{inspector => supplements}/injected/consoleApi.webpack.config.js (100%) rename src/server/{inspector => supplements}/injected/html.ts (100%) rename src/server/{inspector => supplements}/injected/recorder.ts (97%) rename src/server/{inspector => supplements}/injected/recorder.webpack.config.js (100%) rename src/server/{inspector => supplements}/injected/selectorGenerator.ts (100%) rename src/server/{inspector => supplements}/inspectorController.ts (85%) rename src/{cli/codegen => server/supplements/recorder}/codeGenerator.ts (96%) rename src/{cli/codegen/languages => server/supplements/recorder}/csharp.ts (93%) rename src/{cli/codegen/languages => server/supplements/recorder}/javascript.ts (93%) rename src/{cli/codegen/languages/index.ts => server/supplements/recorder/language.ts} (67%) rename src/{cli/codegen/languages => server/supplements/recorder}/python.ts (92%) rename src/{cli/codegen => server/supplements/recorder}/recorderActions.ts (100%) rename src/{cli/codegen => server/supplements/recorder}/utils.ts (93%) rename src/{cli/codegen/recorderController.ts => server/supplements/recorderSupplement.ts} (57%) diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 2df4cba122a23..c8e71956ef370 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -22,16 +22,17 @@ import * as path from 'path'; import * as program from 'commander'; import * as os from 'os'; import * as fs from 'fs'; -import { OutputMultiplexer, TerminalOutput, FileOutput } from './codegen/outputs'; -import { CodeGenerator, CodeGeneratorOutput } from './codegen/codeGenerator'; -import { JavaScriptLanguageGenerator, LanguageGenerator } from './codegen/languages'; -import { PythonLanguageGenerator } from './codegen/languages/python'; -import { CSharpLanguageGenerator } from './codegen/languages/csharp'; -import { RecorderController } from './codegen/recorderController'; import { runServer, printApiJson, installBrowsers } from './driver'; import { showTraceViewer } from './traceViewer/traceViewer'; -import type { Browser, BrowserContext, Page, BrowserType, BrowserContextOptions, LaunchOptions } from '../..'; import * as playwright from '../..'; +import { BrowserContext } from '../client/browserContext'; +import { Browser } from '../client/browser'; +import { Page } from '../client/page'; +import { BrowserType } from '../client/browserType'; +import { BrowserContextOptions, LaunchOptions } from '../client/types'; +import { RecorderOutput, RecorderSupplement } from '../client/supplements/recorderSupplement'; +import { ConsoleApiSupplement } from '../client/supplements/consoleApiSupplement'; +import { FileOutput, OutputMultiplexer, TerminalOutput } from '../client/supplements/recorderOutputs'; program .version('Version ' + require('../../package.json').version) @@ -317,36 +318,31 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi async function open(options: Options, url: string | undefined) { const { context } = await launchContext(options, false); - (context as any)._exposeConsoleApi(); + new ConsoleApiSupplement(context); await openPage(context, url); if (process.env.PWCLI_EXIT_FOR_TEST) await Promise.all(context.pages().map(p => p.close())); } -async function codegen(options: Options, url: string | undefined, target: string, outputFile?: string) { - let languageGenerator: LanguageGenerator; - - switch (target) { - case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break; - case 'csharp': languageGenerator = new CSharpLanguageGenerator(); break; - case 'python': - case 'python-async': languageGenerator = new PythonLanguageGenerator(target === 'python-async'); break; - default: throw new Error(`Invalid target: '${target}'`); - } - - const { context, browserName, launchOptions, contextOptions } = await launchContext(options, false); - - if (process.env.PWTRACE) - (contextOptions as any)._traceDir = path.join(process.cwd(), '.trace'); - - const outputs: CodeGeneratorOutput[] = [TerminalOutput.create(process.stdout, languageGenerator.highlighterType())]; +async function codegen(options: Options, url: string | undefined, language: string, outputFile?: string) { + const { context, launchOptions, contextOptions } = await launchContext(options, false); + let highlighterType = language; + if (highlighterType === 'python-async') + highlighterType = 'python'; + const outputs: RecorderOutput[] = [TerminalOutput.create(process.stdout, highlighterType)]; if (outputFile) outputs.push(new FileOutput(outputFile)); const output = new OutputMultiplexer(outputs); - const generator = new CodeGenerator(browserName, launchOptions, contextOptions, output, languageGenerator, options.device, options.saveStorage); - new RecorderController(context, generator); - (context as any)._exposeConsoleApi(); + new ConsoleApiSupplement(context); + new RecorderSupplement(context, + language, + launchOptions, + contextOptions, + options.device, + options.saveStorage, + output); + await openPage(context, url); if (process.env.PWCLI_EXIT_FOR_TEST) await Promise.all(context.pages().map(p => p.close())); @@ -387,20 +383,23 @@ async function pdf(options: Options, captureOptions: CaptureOptions, url: string await browser.close(); } -function lookupBrowserType(options: Options): BrowserType { +function lookupBrowserType(options: Options): BrowserType { let name = options.browser; if (options.device) { const device = playwright.devices[options.device]; name = device.defaultBrowserType; } + let browserType: any; switch (name) { - case 'chromium': return playwright.chromium!; - case 'webkit': return playwright.webkit!; - case 'firefox': return playwright.firefox!; - case 'cr': return playwright.chromium!; - case 'wk': return playwright.webkit!; - case 'ff': return playwright.firefox!; + case 'chromium': browserType = playwright.chromium; break; + case 'webkit': browserType = playwright.webkit; break; + case 'firefox': browserType = playwright.firefox; break; + case 'cr': browserType = playwright.chromium; break; + case 'wk': browserType = playwright.webkit; break; + case 'ff': browserType = playwright.firefox; break; } + if (browserType) + return browserType; program.help(); } diff --git a/src/cli/driver.ts b/src/cli/driver.ts index 608e8a15a2246..9d393506732d0 100644 --- a/src/cli/driver.ts +++ b/src/cli/driver.ts @@ -18,7 +18,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { installInspectorController } from '../server/inspector/inspectorController'; +import { installInspectorController } from '../server/supplements/inspectorController'; import { DispatcherConnection } from '../dispatchers/dispatcher'; import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher'; import { installBrowsersWithProgressBar } from '../install/installer'; diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index 1c397a088e06f..26084e21e5678 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -253,14 +253,6 @@ export class BrowserContext extends ChannelOwner() { - await this._channel.enableRecorder(); - } } export async function prepareBrowserContextOptions(options: BrowserContextOptions): Promise { diff --git a/src/client/supplements/consoleApiSupplement.ts b/src/client/supplements/consoleApiSupplement.ts new file mode 100644 index 0000000000000..33599a78d09fe --- /dev/null +++ b/src/client/supplements/consoleApiSupplement.ts @@ -0,0 +1,23 @@ +/** + * 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'; + +export class ConsoleApiSupplement { + constructor(context: BrowserContext) { + context._channel.consoleSupplementExpose().catch(e => {}); + } +} diff --git a/src/cli/codegen/outputs.ts b/src/client/supplements/recorderOutputs.ts similarity index 92% rename from src/cli/codegen/outputs.ts rename to src/client/supplements/recorderOutputs.ts index f3251163ff814..fc1d6279866b8 100644 --- a/src/cli/codegen/outputs.ts +++ b/src/client/supplements/recorderOutputs.ts @@ -18,11 +18,11 @@ import * as fs from 'fs'; import * as querystring from 'querystring'; import { Writable } from 'stream'; import * as hljs from '../../third_party/highlightjs/highlightjs'; -import { CodeGeneratorOutput } from './codeGenerator'; +import { RecorderOutput } from './recorderSupplement'; -export class OutputMultiplexer implements CodeGeneratorOutput { - private _outputs: CodeGeneratorOutput[] - constructor(outputs: CodeGeneratorOutput[]) { +export class OutputMultiplexer implements RecorderOutput { + private _outputs: RecorderOutput[] + constructor(outputs: RecorderOutput[]) { this._outputs = outputs; } @@ -58,7 +58,7 @@ export class BufferOutput { } } -export class FileOutput extends BufferOutput implements CodeGeneratorOutput { +export class FileOutput extends BufferOutput implements RecorderOutput { private _fileName: string; constructor(fileName: string) { @@ -71,7 +71,7 @@ export class FileOutput extends BufferOutput implements CodeGeneratorOutput { } } -export class TerminalOutput implements CodeGeneratorOutput { +export class TerminalOutput implements RecorderOutput { private _output: Writable private _language: string; @@ -127,7 +127,7 @@ export class TerminalOutput implements CodeGeneratorOutput { flush() {} } -export class FlushingTerminalOutput extends BufferOutput implements CodeGeneratorOutput { +export class FlushingTerminalOutput extends BufferOutput implements RecorderOutput { private _output: Writable constructor(output: Writable) { diff --git a/src/client/supplements/recorderSupplement.ts b/src/client/supplements/recorderSupplement.ts new file mode 100644 index 0000000000000..b875c5d1b7d8f --- /dev/null +++ b/src/client/supplements/recorderSupplement.ts @@ -0,0 +1,51 @@ +/** + * 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 path from 'path'; + +import { BrowserContext } from '../browserContext'; +import { BrowserContextOptions, LaunchOptions } from '../types'; + +export class RecorderSupplement { + constructor(context: BrowserContext, + language: string, + launchOptions: LaunchOptions, + contextOptions: BrowserContextOptions, + device: string | undefined, + saveStorage: string | undefined, + output: RecorderOutput) { + + if (process.env.PWTRACE) + contextOptions._traceDir = path.join(process.cwd(), '.trace'); + + context._channel.on('recorderSupplementPrintLn', event => output.printLn(event.text)); + context._channel.on('recorderSupplementPopLn', event => output.popLn(event.text)); + context.on('close', () => output.flush()); + context._channel.recorderSupplementEnable({ + language, + launchOptions, + contextOptions, + device, + saveStorage, + }).catch(e => {}); + } +} + +export interface RecorderOutput { + printLn(text: string): void; + popLn(text: string): void; + flush(): void; +} diff --git a/src/dispatchers/browserContextDispatcher.ts b/src/dispatchers/browserContextDispatcher.ts index ce4efdb2b2b2d..10b7e00a72a6e 100644 --- a/src/dispatchers/browserContextDispatcher.ts +++ b/src/dispatchers/browserContextDispatcher.ts @@ -21,6 +21,8 @@ import * as channels from '../protocol/channels'; import { RouteDispatcher, RequestDispatcher } from './networkDispatchers'; import { CRBrowserContext } from '../server/chromium/crBrowser'; import { CDPSessionDispatcher } from './cdpSessionDispatcher'; +import { RecorderSupplement } from '../server/supplements/recorderSupplement'; +import { ConsoleApiSupplement } from '../server/supplements/consoleApiSupplement'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { private _context: BrowserContext; @@ -125,12 +127,17 @@ export class BrowserContextDispatcher extends Dispatcher { - await this._context.exposeConsoleApi(); + async consoleSupplementExpose(): Promise { + const consoleApi = new ConsoleApiSupplement(this._context); + await consoleApi.install(); } - async enableRecorder(): Promise { - await this._context.enableRecorder(); + async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise { + const recorder = new RecorderSupplement(this._context, params, { + printLn: text => this._dispatchEvent('recorderSupplementPrintLn', { text }), + popLn: text => this._dispatchEvent('recorderSupplementPopLn', { text }), + }); + await recorder.install(); } async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise { diff --git a/src/inprocess.ts b/src/inprocess.ts index e9a89d6858464..9f3d26b1481c2 100644 --- a/src/inprocess.ts +++ b/src/inprocess.ts @@ -20,7 +20,7 @@ import type { Playwright as PlaywrightAPI } from './client/playwright'; import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher'; import { Connection } from './client/connection'; import { BrowserServerLauncherImpl } from './browserServerImpl'; -import { installInspectorController } from './server/inspector/inspectorController'; +import { installInspectorController } from './server/supplements/inspectorController'; import { installTracer } from './trace/tracer'; import { installHarTracer } from './trace/harTracer'; import * as path from 'path'; diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 38b40d950e9c4..e0e20aea8e1b4 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -535,6 +535,8 @@ export interface BrowserContextChannel extends Channel { on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this; on(event: 'crBackgroundPage', callback: (params: BrowserContextCrBackgroundPageEvent) => void): this; on(event: 'crServiceWorker', callback: (params: BrowserContextCrServiceWorkerEvent) => void): this; + on(event: 'recorderSupplementPrintLn', callback: (params: BrowserContextRecorderSupplementPrintLnEvent) => void): this; + on(event: 'recorderSupplementPopLn', callback: (params: BrowserContextRecorderSupplementPopLnEvent) => void): this; addCookies(params: BrowserContextAddCookiesParams, metadata?: Metadata): Promise; addInitScript(params: BrowserContextAddInitScriptParams, metadata?: Metadata): Promise; clearCookies(params?: BrowserContextClearCookiesParams, metadata?: Metadata): Promise; @@ -552,8 +554,8 @@ export interface BrowserContextChannel extends Channel { setNetworkInterceptionEnabled(params: BrowserContextSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise; setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise; storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise; - exposeConsoleApi(params?: BrowserContextExposeConsoleApiParams, metadata?: Metadata): Promise; - enableRecorder(params?: BrowserContextEnableRecorderParams, metadata?: Metadata): Promise; + consoleSupplementExpose(params?: BrowserContextConsoleSupplementExposeParams, metadata?: Metadata): Promise; + recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise; crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise; } export type BrowserContextBindingCallEvent = { @@ -573,6 +575,12 @@ export type BrowserContextCrBackgroundPageEvent = { export type BrowserContextCrServiceWorkerEvent = { worker: WorkerChannel, }; +export type BrowserContextRecorderSupplementPrintLnEvent = { + text: string, +}; +export type BrowserContextRecorderSupplementPopLnEvent = { + text: string, +}; export type BrowserContextAddCookiesParams = { cookies: SetNetworkCookie[], }; @@ -695,12 +703,21 @@ export type BrowserContextStorageStateResult = { cookies: NetworkCookie[], origins: OriginStorage[], }; -export type BrowserContextExposeConsoleApiParams = {}; -export type BrowserContextExposeConsoleApiOptions = {}; -export type BrowserContextExposeConsoleApiResult = void; -export type BrowserContextEnableRecorderParams = {}; -export type BrowserContextEnableRecorderOptions = {}; -export type BrowserContextEnableRecorderResult = void; +export type BrowserContextConsoleSupplementExposeParams = {}; +export type BrowserContextConsoleSupplementExposeOptions = {}; +export type BrowserContextConsoleSupplementExposeResult = void; +export type BrowserContextRecorderSupplementEnableParams = { + language: string, + launchOptions: any, + contextOptions: any, + device?: string, + saveStorage?: string, +}; +export type BrowserContextRecorderSupplementEnableOptions = { + device?: string, + saveStorage?: string, +}; +export type BrowserContextRecorderSupplementEnableResult = void; export type BrowserContextCrNewCDPSessionParams = { page: PageChannel, }; diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 3e63927e68848..8bdaf0900fa34 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -599,11 +599,17 @@ BrowserContext: type: array items: OriginStorage - exposeConsoleApi: + consoleSupplementExpose: experimental: True - enableRecorder: + recorderSupplementEnable: experimental: True + parameters: + language: string + launchOptions: json + contextOptions: json + device: string? + saveStorage: string? crNewCDPSession: parameters: @@ -636,7 +642,13 @@ BrowserContext: parameters: worker: Worker + recorderSupplementPrintLn: + parameters: + text: string + recorderSupplementPopLn: + parameters: + text: string Page: type: interface diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 918d4aae4e71f..0548ef0ffc20e 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -335,8 +335,14 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { offline: tBoolean, }); scheme.BrowserContextStorageStateParams = tOptional(tObject({})); - scheme.BrowserContextExposeConsoleApiParams = tOptional(tObject({})); - scheme.BrowserContextEnableRecorderParams = tOptional(tObject({})); + scheme.BrowserContextConsoleSupplementExposeParams = tOptional(tObject({})); + scheme.BrowserContextRecorderSupplementEnableParams = tObject({ + language: tString, + launchOptions: tAny, + contextOptions: tAny, + device: tOptional(tString), + saveStorage: tOptional(tString), + }); scheme.BrowserContextCrNewCDPSessionParams = tObject({ page: tChannel('Page'), }); diff --git a/src/remote/playwrightServer.ts b/src/remote/playwrightServer.ts index 3a12d06914b07..c1e7ef6b2db21 100644 --- a/src/remote/playwrightServer.ts +++ b/src/remote/playwrightServer.ts @@ -17,7 +17,7 @@ import * as debug from 'debug'; import * as http from 'http'; import * as WebSocket from 'ws'; -import { installInspectorController } from '../server/inspector/inspectorController'; +import { installInspectorController } from '../server/supplements/inspectorController'; import { DispatcherConnection } from '../dispatchers/dispatcher'; import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher'; import { Playwright } from '../server/playwright'; diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 6dee12a2b3032..7709118863dd3 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -19,8 +19,6 @@ import { EventEmitter } from 'events'; import { TimeoutSettings } from '../utils/timeoutSettings'; import { mkdirIfNeeded } from '../utils/utils'; import { Browser, BrowserOptions } from './browser'; -import * as consoleApiSource from '../generated/consoleApiSource'; -import * as recorderSource from '../generated/recorderSource'; import * as dom from './dom'; import { Download } from './download'; import * as frames from './frames'; @@ -381,15 +379,7 @@ export abstract class BrowserContext extends EventEmitter { } } - async exposeConsoleApi() { - await this._extendInjectedScript(consoleApiSource.source); - } - - async enableRecorder() { - await this._extendInjectedScript(recorderSource.source); - } - - private async _extendInjectedScript(source: string) { + async extendInjectedScript(source: string) { const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(source).catch(e => {}); const installInPage = (page: Page) => { page.on(Page.Events.InternalFrameNavigatedToNewDocument, installInFrame); diff --git a/src/server/supplements/consoleApiSupplement.ts b/src/server/supplements/consoleApiSupplement.ts new file mode 100644 index 0000000000000..60b342bb15143 --- /dev/null +++ b/src/server/supplements/consoleApiSupplement.ts @@ -0,0 +1,30 @@ +/** + * 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 consoleApiSource from '../../generated/consoleApiSource'; +import { BrowserContext } from '../browserContext'; + +export class ConsoleApiSupplement { + private _context: BrowserContext; + + constructor(context: BrowserContext) { + this._context = context; + } + + async install() { + await this._context.extendInjectedScript(consoleApiSource.source); + } +} diff --git a/src/server/inspector/injected/consoleApi.ts b/src/server/supplements/injected/consoleApi.ts similarity index 100% rename from src/server/inspector/injected/consoleApi.ts rename to src/server/supplements/injected/consoleApi.ts diff --git a/src/server/inspector/injected/consoleApi.webpack.config.js b/src/server/supplements/injected/consoleApi.webpack.config.js similarity index 100% rename from src/server/inspector/injected/consoleApi.webpack.config.js rename to src/server/supplements/injected/consoleApi.webpack.config.js diff --git a/src/server/inspector/injected/html.ts b/src/server/supplements/injected/html.ts similarity index 100% rename from src/server/inspector/injected/html.ts rename to src/server/supplements/injected/html.ts diff --git a/src/server/inspector/injected/recorder.ts b/src/server/supplements/injected/recorder.ts similarity index 97% rename from src/server/inspector/injected/recorder.ts rename to src/server/supplements/injected/recorder.ts index 5a52d4a3bbfa6..99715811a9e33 100644 --- a/src/server/inspector/injected/recorder.ts +++ b/src/server/supplements/injected/recorder.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import type * as actions from '../../../cli/codegen/recorderActions'; +import type * as actions from '../recorder/recorderActions'; import type InjectedScript from '../../injected/injectedScript'; import { generateSelector } from './selectorGenerator'; import { html } from './html'; declare global { interface Window { - performPlaywrightAction: (action: actions.Action) => Promise; - recordPlaywrightAction: (action: actions.Action) => Promise; - commitLastAction: () => Promise; + playwrightRecorderPerformAction: (action: actions.Action) => Promise; + playwrightRecorderRecordAction: (action: actions.Action) => Promise; + playwrightRecorderCommitAction: () => Promise; } } @@ -238,7 +238,7 @@ export class Recorder { const { selector, elements } = generateSelector(this._injectedScript, hoveredElement); if ((this._hoveredModel && this._hoveredModel.selector === selector) || this._hoveredElement !== hoveredElement) return; - window.commitLastAction(); + window.playwrightRecorderCommitAction(); this._hoveredModel = selector ? { selector, elements } : null; this._updateHighlight(); if ((window as any)._highlightUpdatedForTest) @@ -331,7 +331,7 @@ export class Recorder { } if (elementType === 'file') { - window.recordPlaywrightAction({ + window.playwrightRecorderRecordAction({ name: 'setInputFiles', selector: this._activeModel!.selector, signals: [], @@ -343,7 +343,7 @@ export class Recorder { // Non-navigating actions are simply recorded by Playwright. if (this._consumedDueWrongTarget(event)) return; - window.recordPlaywrightAction({ + window.playwrightRecorderRecordAction({ name: 'fill', selector: this._activeModel!.selector, signals: [], @@ -434,7 +434,7 @@ export class Recorder { private async _performAction(action: actions.Action) { this._performingAction = true; - await window.performPlaywrightAction(action); + await window.playwrightRecorderPerformAction(action); this._performingAction = false; // Action could have changed DOM, update hovered model selectors. diff --git a/src/server/inspector/injected/recorder.webpack.config.js b/src/server/supplements/injected/recorder.webpack.config.js similarity index 100% rename from src/server/inspector/injected/recorder.webpack.config.js rename to src/server/supplements/injected/recorder.webpack.config.js diff --git a/src/server/inspector/injected/selectorGenerator.ts b/src/server/supplements/injected/selectorGenerator.ts similarity index 100% rename from src/server/inspector/injected/selectorGenerator.ts rename to src/server/supplements/injected/selectorGenerator.ts diff --git a/src/server/inspector/inspectorController.ts b/src/server/supplements/inspectorController.ts similarity index 85% rename from src/server/inspector/inspectorController.ts rename to src/server/supplements/inspectorController.ts index 8b03d319c0393..e86607063f4a2 100644 --- a/src/server/inspector/inspectorController.ts +++ b/src/server/supplements/inspectorController.ts @@ -16,6 +16,7 @@ import { BrowserContext, ContextListener, contextListeners } from '../browserContext'; import { isDebugMode } from '../../utils/utils'; +import { ConsoleApiSupplement } from './consoleApiSupplement'; export function installInspectorController() { contextListeners.add(new InspectorController()); @@ -23,8 +24,10 @@ export function installInspectorController() { class InspectorController implements ContextListener { async onContextCreated(context: BrowserContext): Promise { - if (isDebugMode()) - context.exposeConsoleApi(); + if (isDebugMode()) { + const consoleApi = new ConsoleApiSupplement(context); + await consoleApi.install(); + } } async onContextWillDestroy(context: BrowserContext): Promise {} async onContextDidDestroy(context: BrowserContext): Promise {} diff --git a/src/cli/codegen/codeGenerator.ts b/src/server/supplements/recorder/codeGenerator.ts similarity index 96% rename from src/cli/codegen/codeGenerator.ts rename to src/server/supplements/recorder/codeGenerator.ts index e205859208c7b..63632d8db177f 100644 --- a/src/cli/codegen/codeGenerator.ts +++ b/src/server/supplements/recorder/codeGenerator.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -import type { LaunchOptions, Frame, BrowserContextOptions } from '../../..'; -import { LanguageGenerator } from './languages'; +import type { BrowserContextOptions, LaunchOptions } from '../../../..'; +import { Frame } from '../../frames'; +import { LanguageGenerator } from './language'; import { Action, Signal } from './recorderActions'; export type ActionInContext = { @@ -28,7 +29,6 @@ export type ActionInContext = { export interface CodeGeneratorOutput { printLn(text: string): void; popLn(text: string): void; - flush(): void; } export class CodeGenerator { @@ -50,10 +50,6 @@ export class CodeGenerator { this._output.printLn(this._footerText); } - exit() { - this._output.flush(); - } - addAction(action: ActionInContext) { this.willPerformAction(action); this.didPerformAction(action); diff --git a/src/cli/codegen/languages/csharp.ts b/src/server/supplements/recorder/csharp.ts similarity index 93% rename from src/cli/codegen/languages/csharp.ts rename to src/server/supplements/recorder/csharp.ts index de97fef718d94..c653c93169b6a 100644 --- a/src/cli/codegen/languages/csharp.ts +++ b/src/server/supplements/recorder/csharp.ts @@ -15,18 +15,14 @@ */ import type { BrowserContextOptions, LaunchOptions } from '../../../..'; -import * as playwright from '../../../..'; -import { HighlighterType, LanguageGenerator } from '.'; -import { ActionInContext } from '../codeGenerator'; -import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from '../recorderActions'; -import { MouseClickOptions, toModifiers } from '../utils'; +import { LanguageGenerator, sanitizeDeviceOptions } from './language'; +import { ActionInContext } from './codeGenerator'; +import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions'; +import { MouseClickOptions, toModifiers } from './utils'; +import deviceDescriptors = require('../../deviceDescriptors'); export class CSharpLanguageGenerator implements LanguageGenerator { - highlighterType(): HighlighterType { - return 'csharp'; - } - generateAction(actionInContext: ActionInContext, performingAction: boolean): string { const { action, pageAlias, frame } = actionInContext; const formatter = new CSharpFormatter(0); @@ -240,16 +236,10 @@ function toPascal(value: string): string { } function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined): string { - const device = deviceName && playwright.devices[deviceName]; + const device = deviceName && deviceDescriptors[deviceName]; if (!device) return formatArgs(options); - // Filter out all the properties from the device descriptor. - const cleanedOptions: Record = {}; - for (const property in options) { - if ((device as any)[property] !== (options as any)[property]) - cleanedOptions[property] = (options as any)[property]; - } - const serializedObject = formatObject(cleanedOptions, ' '); + const serializedObject = formatObject(sanitizeDeviceOptions(device, options), ' '); // When there are no additional context options, we still want to spread the device inside. if (!serializedObject) diff --git a/src/cli/codegen/languages/javascript.ts b/src/server/supplements/recorder/javascript.ts similarity index 93% rename from src/cli/codegen/languages/javascript.ts rename to src/server/supplements/recorder/javascript.ts index ade581f78b8c1..f35301a6a89d1 100644 --- a/src/cli/codegen/languages/javascript.ts +++ b/src/server/supplements/recorder/javascript.ts @@ -15,18 +15,14 @@ */ import type { BrowserContextOptions, LaunchOptions } from '../../../..'; -import * as playwright from '../../../..'; -import { HighlighterType, LanguageGenerator } from '.'; -import { ActionInContext } from '../codeGenerator'; -import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from '../recorderActions'; -import { MouseClickOptions, toModifiers } from '../utils'; +import { LanguageGenerator, sanitizeDeviceOptions } from './language'; +import { ActionInContext } from './codeGenerator'; +import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions'; +import { MouseClickOptions, toModifiers } from './utils'; +import deviceDescriptors = require('../../deviceDescriptors'); export class JavaScriptLanguageGenerator implements LanguageGenerator { - highlighterType(): HighlighterType { - return 'javascript'; - } - generateAction(actionInContext: ActionInContext, performingAction: boolean): string { const { action, pageAlias, frame } = actionInContext; const formatter = new JavaScriptFormatter(2); @@ -195,16 +191,11 @@ function formatObjectOrVoid(value: any, indent = ' '): string { } function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined): string { - const device = deviceName && playwright.devices[deviceName]; + const device = deviceName && deviceDescriptors[deviceName]; if (!device) return formatObjectOrVoid(options); // Filter out all the properties from the device descriptor. - const cleanedOptions: Record = {}; - for (const property in options) { - if ((device as any)[property] !== (options as any)[property]) - cleanedOptions[property] = (options as any)[property]; - } - let serializedObject = formatObjectOrVoid(cleanedOptions); + let serializedObject = formatObjectOrVoid(sanitizeDeviceOptions(device, options)); // When there are no additional context options, we still want to spread the device inside. if (!serializedObject) serializedObject = '{\n}'; diff --git a/src/cli/codegen/languages/index.ts b/src/server/supplements/recorder/language.ts similarity index 67% rename from src/cli/codegen/languages/index.ts rename to src/server/supplements/recorder/language.ts index 8611dd6243199..b5a57a7008503 100644 --- a/src/cli/codegen/languages/index.ts +++ b/src/server/supplements/recorder/language.ts @@ -15,15 +15,20 @@ */ import type { BrowserContextOptions, LaunchOptions } from '../../../..'; -import { ActionInContext } from '../codeGenerator'; - -export type HighlighterType = 'javascript' | 'csharp' | 'python'; +import { ActionInContext } from './codeGenerator'; export interface LanguageGenerator { generateHeader(browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName?: string): string; generateAction(actionInContext: ActionInContext, performingAction: boolean): string; generateFooter(saveStorage: string | undefined): string; - highlighterType(): HighlighterType; } -export { JavaScriptLanguageGenerator } from './javascript'; \ No newline at end of file +export function sanitizeDeviceOptions(device: any, options: BrowserContextOptions): BrowserContextOptions { + // Filter out all the properties from the device descriptor. + const cleanedOptions: Record = {}; + for (const property in options) { + if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property])) + cleanedOptions[property] = (options as any)[property]; + } + return cleanedOptions; +} diff --git a/src/cli/codegen/languages/python.ts b/src/server/supplements/recorder/python.ts similarity index 92% rename from src/cli/codegen/languages/python.ts rename to src/server/supplements/recorder/python.ts index 64a0ca075543d..e9a217c7b7161 100644 --- a/src/cli/codegen/languages/python.ts +++ b/src/server/supplements/recorder/python.ts @@ -15,11 +15,11 @@ */ import type { BrowserContextOptions, LaunchOptions } from '../../../..'; -import * as playwright from '../../../..'; -import { HighlighterType, LanguageGenerator } from '.'; -import { ActionInContext } from '../codeGenerator'; -import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from '../recorderActions'; -import { MouseClickOptions, toModifiers } from '../utils'; +import { LanguageGenerator, sanitizeDeviceOptions } from './language'; +import { ActionInContext } from './codeGenerator'; +import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions'; +import { MouseClickOptions, toModifiers } from './utils'; +import deviceDescriptors = require('../../deviceDescriptors'); export class PythonLanguageGenerator implements LanguageGenerator { private _awaitPrefix: '' | 'await '; @@ -32,10 +32,6 @@ export class PythonLanguageGenerator implements LanguageGenerator { this._asyncPrefix = isAsync ? 'async ' : ''; } - highlighterType(): HighlighterType { - return 'python'; - } - generateAction(actionInContext: ActionInContext, performingAction: boolean): string { const { action, pageAlias, frame } = actionInContext; const formatter = new PythonFormatter(4); @@ -217,16 +213,10 @@ function formatOptions(value: any, hasArguments: boolean): string { } function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined): string { - const device = deviceName && playwright.devices[deviceName]; + const device = deviceName && deviceDescriptors[deviceName]; if (!device) return formatOptions(options, false); - // Filter out all the properties from the device descriptor. - const cleanedOptions: Record = {}; - for (const property in options) { - if ((device as any)[property] !== (options as any)[property]) - cleanedOptions[property] = (options as any)[property]; - } - return `**playwright.devices["${deviceName}"]` + formatOptions(cleanedOptions, true); + return `**playwright.devices["${deviceName}"]` + formatOptions(sanitizeDeviceOptions(device, options), true); } class PythonFormatter { diff --git a/src/cli/codegen/recorderActions.ts b/src/server/supplements/recorder/recorderActions.ts similarity index 100% rename from src/cli/codegen/recorderActions.ts rename to src/server/supplements/recorder/recorderActions.ts diff --git a/src/cli/codegen/utils.ts b/src/server/supplements/recorder/utils.ts similarity index 93% rename from src/cli/codegen/utils.ts rename to src/server/supplements/recorder/utils.ts index fb80c094a0051..264577dfe9cce 100644 --- a/src/cli/codegen/utils.ts +++ b/src/server/supplements/recorder/utils.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import type { Page } from '../../..'; +import { Frame } from '../../frames'; import * as actions from './recorderActions'; -export type MouseClickOptions = Parameters[1]; +export type MouseClickOptions = Parameters[2]; export function toClickOptions(action: actions.ClickAction): { method: 'click' | 'dblclick', options: MouseClickOptions } { let method: 'click' | 'dblclick' = 'click'; diff --git a/src/cli/codegen/recorderController.ts b/src/server/supplements/recorderSupplement.ts similarity index 57% rename from src/cli/codegen/recorderController.ts rename to src/server/supplements/recorderSupplement.ts index d51685fa7bac9..2a56ae335e866 100644 --- a/src/cli/codegen/recorderController.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -14,52 +14,74 @@ * limitations under the License. */ -import type { Page, BrowserContext, Frame, Download, Dialog } from '../../..'; -import * as actions from './recorderActions'; -import { CodeGenerator, ActionInContext } from './codeGenerator'; -import { toClickOptions, toModifiers } from './utils'; +import * as actions from './recorder/recorderActions'; +import { CodeGenerator, ActionInContext, CodeGeneratorOutput } from './recorder/codeGenerator'; +import { toClickOptions, toModifiers } from './recorder/utils'; +import { Page } from '../page'; +import { Frame } from '../frames'; +import { BrowserContext } from '../browserContext'; +import { LanguageGenerator } from './recorder/language'; +import { JavaScriptLanguageGenerator } from './recorder/javascript'; +import { CSharpLanguageGenerator } from './recorder/csharp'; +import { PythonLanguageGenerator } from './recorder/python'; +import { ProgressController } from '../progress'; +import * as recorderSource from '../../generated/recorderSource'; type BindingSource = { frame: Frame, page: Page }; -export class RecorderController { +export class RecorderSupplement { private _generator: CodeGenerator; private _pageAliases = new Map(); private _lastPopupOrdinal = 0; private _lastDialogOrdinal = 0; private _timers = new Set(); + private _context: BrowserContext; + + constructor(context: BrowserContext, params: { language: string, launchOptions: any, contextOptions: any, device?: string, saveStorage?: string}, output: CodeGeneratorOutput) { + this._context = context; + let languageGenerator: LanguageGenerator; + + switch (params.language) { + case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break; + case 'csharp': languageGenerator = new CSharpLanguageGenerator(); break; + case 'python': + case 'python-async': languageGenerator = new PythonLanguageGenerator(params.language === 'python-async'); break; + default: throw new Error(`Invalid target: '${params.language}'`); + } + const generator = new CodeGenerator(context._browser._options.name, params.launchOptions, params.contextOptions, output, languageGenerator, params.device, params.saveStorage); + this._generator = generator; + } - constructor(context: BrowserContext, generator: CodeGenerator) { - (context as any)._enableRecorder(); + async install() { + this._context.on('page', page => this._onPage(page)); + for (const page of this._context.pages()) + this._onPage(page); - this._generator = generator; + this._context.once('close', () => { + for (const timer of this._timers) + clearTimeout(timer); + this._timers.clear(); + }); // Input actions that potentially lead to navigation are intercepted on the page and are // performed by the Playwright. - context.exposeBinding('performPlaywrightAction', - (source: BindingSource, action: actions.Action) => this._performAction(source.frame, action)).catch(e => {}); + await this._context.exposeBinding('playwrightRecorderPerformAction', false, + (source: BindingSource, action: actions.Action) => this._performAction(source.frame, action)); // Other non-essential actions are simply being recorded. - context.exposeBinding('recordPlaywrightAction', - (source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action)).catch(e => {}); + await this._context.exposeBinding('playwrightRecorderRecordAction', false, + (source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action)); // Commits last action so that no further signals are added to it. - context.exposeBinding('commitLastAction', - (source: BindingSource, action: actions.Action) => this._generator.commitLastAction()).catch(e => {}); - - context.on('page', page => this._onPage(page)); - for (const page of context.pages()) - this._onPage(page); + await this._context.exposeBinding('playwrightRecorderCommitAction', false, + (source: BindingSource, action: actions.Action) => this._generator.commitLastAction()); - context.once('close', () => { - for (const timer of this._timers) - clearTimeout(timer); - this._timers.clear(); - this._generator.exit(); - }); + await this._context.extendInjectedScript(recorderSource.source); } private async _onPage(page: Page) { // First page is called page, others are called popup1, popup2, etc. + const frame = page.mainFrame(); page.on('close', () => { this._pageAliases.delete(page); this._generator.addAction({ @@ -72,10 +94,10 @@ export class RecorderController { } }); }); - page.on('framenavigated', frame => this._onFrameNavigated(frame, page)); - page.on('download', download => this._onDownload(page, download)); - page.on('popup', popup => this._onPopup(page, popup)); - page.on('dialog', dialog => this._onDialog(page, dialog)); + frame.on(Frame.Events.Navigation, () => this._onFrameNavigated(frame, page)); + page.on(Page.Events.Download, () => this._onDownload(page)); + page.on(Page.Events.Popup, popup => this._onPopup(page, popup)); + page.on(Page.Events.Dialog, () => this._onDialog(page)); const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : ''; const pageAlias = 'page' + suffix; this._pageAliases.set(page, pageAlias); @@ -91,7 +113,7 @@ export class RecorderController { committed: true, action: { name: 'openPage', - url: page.url(), + url: page.mainFrame().url(), signals: [], } }); @@ -99,7 +121,8 @@ export class RecorderController { } private async _performAction(frame: Frame, action: actions.Action) { - const page = frame.page(); + const page = frame._page; + const controller = new ProgressController(); const actionInContext: ActionInContext = { pageAlias: this._pageAliases.get(page)!, frame, @@ -108,19 +131,19 @@ export class RecorderController { this._generator.willPerformAction(actionInContext); if (action.name === 'click') { const { options } = toClickOptions(action); - await frame.click(action.selector, options); + await frame.click(controller, action.selector, options); } if (action.name === 'press') { const modifiers = toModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); - await frame.press(action.selector, shortcut); + await frame.press(controller, action.selector, shortcut); } if (action.name === 'check') - await frame.check(action.selector); + await frame.check(controller, action.selector); if (action.name === 'uncheck') - await frame.uncheck(action.selector); + await frame.uncheck(controller, action.selector); if (action.name === 'select') - await frame.selectOption(action.selector, action.options); + await frame.selectOption(controller, action.selector, [], action.options.map(value => ({ value }))); const timer = setTimeout(() => { actionInContext.committed = true; this._timers.delete(timer); @@ -132,15 +155,13 @@ export class RecorderController { private async _recordAction(frame: Frame, action: actions.Action) { // We are lacking frame.page() in this._generator.addAction({ - pageAlias: this._pageAliases.get(frame.page())!, + pageAlias: this._pageAliases.get(frame._page)!, frame, action }); } private _onFrameNavigated(frame: Frame, page: Page) { - if (frame.parentFrame()) - return; const pageAlias = this._pageAliases.get(page); this._generator.signal(pageAlias!, frame, { name: 'navigation', url: frame.url() }); } @@ -150,12 +171,12 @@ export class RecorderController { const popupAlias = this._pageAliases.get(popup)!; this._generator.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias }); } - private _onDownload(page: Page, download: Download) { + private _onDownload(page: Page) { const pageAlias = this._pageAliases.get(page)!; this._generator.signal(pageAlias, page.mainFrame(), { name: 'download' }); } - private _onDialog(page: Page, dialog: Dialog) { + private _onDialog(page: Page) { const pageAlias = this._pageAliases.get(page)!; this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) }); } diff --git a/test/cli/cli-codegen.spec.ts b/test/cli/cli-codegen.spec.ts index b83ea5d19742f..0555f0e3053cb 100644 --- a/test/cli/cli-codegen.spec.ts +++ b/test/cli/cli-codegen.spec.ts @@ -235,8 +235,7 @@ describe('cli codegen', (test, { browserName, headful }) => { recorder.waitForOutput('check'), page.click('input') ]); - await recorder.waitForOutput('check'); - expect(recorder.output()).toContain(` + await recorder.waitForOutput(` // Check input[name="accept"] await page.check('input[name="accept"]');`); expect(message.text()).toBe('true'); @@ -253,8 +252,7 @@ describe('cli codegen', (test, { browserName, headful }) => { recorder.waitForOutput('check'), page.keyboard.press('Space') ]); - await recorder.waitForOutput('check'); - expect(recorder.output()).toContain(` + await recorder.waitForOutput(` // Check input[name="accept"] await page.check('input[name="accept"]');`); expect(message.text()).toBe('true'); @@ -321,7 +319,6 @@ describe('cli codegen', (test, { browserName, headful }) => { const selector = await recorder.hoverOverElement('a'); expect(selector).toBe('text="link"'); - await Promise.all([ page.waitForNavigation(), recorder.waitForOutput('assert'), @@ -398,8 +395,7 @@ describe('cli codegen', (test, { browserName, headful }) => { await page.setInputFiles('input[type=file]', 'test/assets/file-to-upload.txt'); await page.click('input[type=file]'); - await recorder.waitForOutput('setInputFiles'); - expect(recorder.output()).toContain(` + await recorder.waitForOutput(` // Upload file-to-upload.txt await page.setInputFiles('input[type="file"]', 'file-to-upload.txt');`); }); @@ -415,8 +411,7 @@ describe('cli codegen', (test, { browserName, headful }) => { await page.setInputFiles('input[type=file]', ['test/assets/file-to-upload.txt', 'test/assets/file-to-upload-2.txt']); await page.click('input[type=file]'); - await recorder.waitForOutput('setInputFiles'); - expect(recorder.output()).toContain(` + await recorder.waitForOutput(` // Upload file-to-upload.txt, file-to-upload-2.txt await page.setInputFiles('input[type="file"]', ['file-to-upload.txt', 'file-to-upload-2.txt']);`); }); @@ -432,13 +427,14 @@ describe('cli codegen', (test, { browserName, headful }) => { await page.setInputFiles('input[type=file]', []); await page.click('input[type=file]'); - await recorder.waitForOutput('setInputFiles'); - expect(recorder.output()).toContain(` + await recorder.waitForOutput(` // Clear selected files await page.setInputFiles('input[type="file"]', []);`); }); - it('should download files', async ({ page, recorder, httpServer }) => { + it('should download files', (test, {browserName}) => { + test.fixme(browserName === 'webkit', 'Generated page.waitForNavigation next to page.waitForEvent(download)'); + }, async ({ page, recorder, httpServer }) => { httpServer.setHandler((req: http.IncomingMessage, res: http.ServerResponse) => { const pathName = url.parse(req.url!).path; if (pathName === '/download') { @@ -458,8 +454,7 @@ describe('cli codegen', (test, { browserName, headful }) => { page.waitForEvent('download'), page.click('text=Download') ]); - await recorder.waitForOutput('page.click'); - expect(recorder.output()).toContain(` + await recorder.waitForOutput(` // Click text="Download" const [download] = await Promise.all([ page.waitForEvent('download'), @@ -476,8 +471,7 @@ describe('cli codegen', (test, { browserName, headful }) => { await dialog.dismiss(); }); await page.click('text="click me"'); - await recorder.waitForOutput('page.once'); - expect(recorder.output()).toContain(` + await recorder.waitForOutput(` // Click text="click me" page.once('dialog', dialog => { console.log(\`Dialog message: $\{dialog.message()}\`); @@ -500,8 +494,7 @@ describe('cli codegen', (test, { browserName, headful }) => { `, httpServer.PREFIX); for (let i = 1; i < 3; ++i) { await page.evaluate('pushState()'); - await recorder.waitForOutput(`seqNum=${i}`); - expect(recorder.output()).toContain(`await page.goto('${httpServer.PREFIX}/#seqNum=${i}');`); + await recorder.waitForOutput(`await page.goto('${httpServer.PREFIX}/#seqNum=${i}');`); } }); diff --git a/test/cli/cli.fixtures.ts b/test/cli/cli.fixtures.ts index c06a589e8eba7..5a0d441c13f7f 100644 --- a/test/cli/cli.fixtures.ts +++ b/test/cli/cli.fixtures.ts @@ -21,10 +21,8 @@ import { ChildProcess, spawn } from 'child_process'; import { folio as baseFolio } from '../fixtures'; import type { Page, BrowserType, Browser, BrowserContext } from '../..'; export { config } from 'folio'; -import { RecorderController } from '../../src/cli/codegen/recorderController'; -import { TerminalOutput } from '../../src/cli/codegen/outputs'; -import { JavaScriptLanguageGenerator } from '../../src/cli/codegen/languages'; -import { CodeGenerator } from '../../src/cli/codegen/codeGenerator'; +import { FlushingTerminalOutput } from '../../lib/client/supplements/recorderOutputs'; +import { RecorderSupplement } from '../../lib/client/supplements/recorderSupplement'; type WorkerFixtures = { browserType: BrowserType; @@ -41,12 +39,10 @@ type TestFixtures = { export const fixtures = baseFolio.extend(); fixtures.contextWrapper.init(async ({ browser }, runTest) => { - const context = await browser.newContext(); + const context = await browser.newContext() as BrowserContext; const outputBuffer = new WritableBuffer(); - const output = new TerminalOutput(outputBuffer as any as Writable, 'javascript'); - const languageGenerator = new JavaScriptLanguageGenerator(); - const generator = new CodeGenerator('chromium', {}, {}, output, languageGenerator, undefined, undefined); - new RecorderController(context, generator); + const output = new FlushingTerminalOutput(outputBuffer as any as Writable); + new RecorderSupplement(context, 'javascript', {}, {}, undefined, undefined, output); await runTest({ context, output: outputBuffer }); await context.close(); }); diff --git a/test/selector-generator.spec.ts b/test/selector-generator.spec.ts index 9407e820f23fa..b7aa06b638916 100644 --- a/test/selector-generator.spec.ts +++ b/test/selector-generator.spec.ts @@ -16,10 +16,11 @@ import { folio } from './fixtures'; import type { Page, Frame } from '..'; +import { ConsoleApiSupplement } from '../lib/client/supplements/consoleApiSupplement'; const fixtures = folio.extend(); fixtures.context.override(async ({ context }, run) => { - await (context as any)._exposeConsoleApi(); + new ConsoleApiSupplement(context); await run(context); }); const { describe, it, expect } = fixtures.build(); diff --git a/utils/build/build.js b/utils/build/build.js index 1de9904f066d6..5a38d8a49b1d1 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -68,8 +68,8 @@ function runBuild() { const webPackFiles = [ 'src/server/injected/injectedScript.webpack.config.js', 'src/server/injected/utilityScript.webpack.config.js', - 'src/server/inspector/injected/consoleApi.webpack.config.js', - 'src/server/inspector/injected/recorder.webpack.config.js', + 'src/server/supplements/injected/consoleApi.webpack.config.js', + 'src/server/supplements/injected/recorder.webpack.config.js', 'src/cli/traceViewer/web/web.webpack.config.js', ]; for (const file of webPackFiles) { diff --git a/utils/check_deps.js b/utils/check_deps.js index 5071e07fe8b8c..34847dbf0ff7b 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -125,13 +125,15 @@ DEPS['src/server/'] = [ // Can depend on any files in these subdirectories. 'src/server/common/**', 'src/server/injected/**', + 'src/server/supplements/**', ]; // No dependencies for code shared between node and page. DEPS['src/server/common/'] = []; // Strict dependencies for injected code. DEPS['src/server/injected/'] = ['src/server/common/']; -DEPS['src/server/inspector/injected/'] = ['src/server/common/', 'src/cli/codegen/', 'src/server/injected/']; + +DEPS['src/client/supplements/'] = ['src/client/']; // Electron and Clank use chromium internally. DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/']; @@ -144,7 +146,7 @@ DEPS['src/cli/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerIm DEPS['src/trace/'] = ['src/utils/', 'src/client/**', 'src/server/**']; // The service is a cross-cutting feature, and so it depends on a bunch of things. -DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/server/', 'src/server/inspector/', 'src/server/electron/', 'src/trace/']; +DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/server/', 'src/server/supplements/', 'src/server/electron/', 'src/trace/']; DEPS['src/service.ts'] = ['src/remote/']; // CLI should only use client-side features.