From 449adfd3ae28b69c43de03c1ee43b3443c422d69 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 11 Feb 2021 17:46:54 -0800 Subject: [PATCH] chore(recorder): move recording output into the gui app (#5425) --- index.js | 1 - src/cli/cli.ts | 2 - src/client/android.ts | 6 +- src/client/browser.ts | 4 +- src/client/browserContext.ts | 33 +- src/client/browserType.ts | 13 +- src/client/electron.ts | 1 + src/dispatchers/androidDispatcher.ts | 2 +- src/dispatchers/browserContextDispatcher.ts | 14 +- src/protocol/channels.ts | 29 +- src/protocol/protocol.yml | 22 +- src/protocol/validator.ts | 13 +- src/server/android/android.ts | 8 +- src/server/browser.ts | 9 +- src/server/browserContext.ts | 5 +- src/server/browserType.ts | 4 +- src/server/chromium/chromium.ts | 6 +- src/server/chromium/crBrowser.ts | 2 +- src/server/electron/electron.ts | 5 +- src/server/firefox/ffBrowser.ts | 2 +- src/server/playwright.ts | 1 - src/server/supplements/inspectorController.ts | 8 +- src/server/supplements/recorder/outputs.ts | 78 ---- .../supplements/recorder/recorderApp.ts | 22 +- src/server/supplements/recorderSupplement.ts | 21 +- src/server/types.ts | 8 +- src/server/webkit/wkBrowser.ts | 2 +- src/web/recorder/recorder.tsx | 3 + ...-codegen.spec.ts => cli-codegen-1.spec.ts} | 311 +--------------- test/cli/cli-codegen-2.spec.ts | 337 ++++++++++++++++++ test/cli/cli-codegen-csharp.spec.ts | 34 +- test/cli/cli-codegen-javascript.spec.ts | 38 +- test/cli/cli-codegen-python-async.spec.ts | 33 +- test/cli/cli-codegen-python.spec.ts | 33 +- test/cli/cli.fixtures.ts | 93 ++--- test/fixtures.ts | 3 +- test/pause.spec.ts | 33 +- test/recorder.fixtures.ts | 40 +++ utils/check_deps.js | 2 +- 39 files changed, 583 insertions(+), 698 deletions(-) rename test/cli/{cli-codegen.spec.ts => cli-codegen-1.spec.ts} (53%) create mode 100644 test/cli/cli-codegen-2.spec.ts create mode 100644 test/recorder.fixtures.ts diff --git a/index.js b/index.js index c1b4fa08a1..0bce8a7abc 100644 --- a/index.js +++ b/index.js @@ -15,5 +15,4 @@ */ const { setUnderTest } = require('./lib/utils/utils'); -setUnderTest(); // Note: we must call setUnderTest before initializing. module.exports = require('./lib/inprocess'); diff --git a/src/cli/cli.ts b/src/cli/cli.ts index ec37c76239..675c432aa5 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -332,7 +332,6 @@ async function open(options: Options, url: string | undefined, language: string) contextOptions, device: options.device, saveStorage: options.saveStorage, - terminal: !!process.stdout.columns, }); await openPage(context, url); if (process.env.PWCLI_EXIT_FOR_TEST) @@ -350,7 +349,6 @@ async function codegen(options: Options, url: string | undefined, language: stri device: options.device, saveStorage: options.saveStorage, startRecording: true, - terminal: !!process.stdout.columns, outputFile: outputFile ? path.resolve(outputFile) : undefined }); await openPage(context, url); diff --git a/src/client/android.ts b/src/client/android.ts index 63b5d19743..a9fddb07d5 100644 --- a/src/client/android.ts +++ b/src/client/android.ts @@ -19,7 +19,7 @@ import * as util from 'util'; import { isString } from '../utils/utils'; import * as channels from '../protocol/channels'; import { Events } from './events'; -import { BrowserContext, prepareBrowserContextOptions } from './browserContext'; +import { BrowserContext, prepareBrowserContextParams } from './browserContext'; import { ChannelOwner } from './channelOwner'; import * as api from '../../types/types'; import * as types from './types'; @@ -230,7 +230,7 @@ export class AndroidDevice extends ChannelOwner { return this._wrapApiCall('androidDevice.launchBrowser', async () => { - const contextOptions = await prepareBrowserContextOptions(options); + const contextOptions = await prepareBrowserContextParams(options); const { context } = await this._channel.launchBrowser(contextOptions); return BrowserContext.from(context) as ChromiumBrowserContext; }); @@ -394,7 +394,7 @@ export class AndroidWebView extends EventEmitter implements api.AndroidWebView { private async _fetchPage(): Promise { return this._device._wrapApiCall('androidWebView.page', async () => { - const { context } = await this._device._channel.connectToWebView({ pid: this._data.pid }); + const { context } = await this._device._channel.connectToWebView({ pid: this._data.pid, sdkLanguage: 'javascript' }); return BrowserContext.from(context).pages()[0]; }); } diff --git a/src/client/browser.ts b/src/client/browser.ts index 58891a62de..a6f9f27c61 100644 --- a/src/client/browser.ts +++ b/src/client/browser.ts @@ -15,7 +15,7 @@ */ import * as channels from '../protocol/channels'; -import { BrowserContext, prepareBrowserContextOptions } from './browserContext'; +import { BrowserContext, prepareBrowserContextParams } from './browserContext'; import { Page } from './page'; import { ChannelOwner } from './channelOwner'; import { Events } from './events'; @@ -47,7 +47,7 @@ export class Browser extends ChannelOwner { if (this._isRemote && options._traceDir) throw new Error(`"_traceDir" is not supported in connected browser`); - const contextOptions = await prepareBrowserContextOptions(options); + const contextOptions = await prepareBrowserContextParams(options); const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context); context._options = contextOptions; this._contexts.add(context); diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index 429c45a778..1dd1f3fc53 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -43,9 +43,9 @@ export class BrowserContext extends ChannelOwner; - _options: channels.BrowserNewContextParams = {}; - private _stdout: NodeJS.WriteStream; - private _stderr: NodeJS.WriteStream; + _options: channels.BrowserNewContextParams = { + sdkLanguage: 'javascript' + }; static from(context: channels.BrowserContextChannel): BrowserContext { return (context as any)._object; @@ -64,23 +64,9 @@ export class BrowserContext extends ChannelOwner this._onClose()); this._channel.on('page', ({page}) => this._onPage(Page.from(page))); this._channel.on('route', ({ route, request }) => this._onRoute(network.Route.from(route), network.Request.from(request))); - this._stdout = process.stdout; - this._stderr = process.stderr; - this._channel.on('stdout', ({ data }) => { - this._stdout.write(Buffer.from(data, 'base64')); - this._pushTerminalSize(); - }); - this._channel.on('stderr', ({ data }) => { - this._stderr.write(Buffer.from(data, 'base64')); - this._pushTerminalSize(); - }); this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f)); } - private _pushTerminalSize() { - this._channel.setTerminalSizeNoReply({ rows: process.stdout.rows, columns: process.stdout.columns }).catch(() => {}); - } - private _onPage(page: Page): void { this._pages.add(page); this.emit(Events.BrowserContext.Page, page); @@ -283,31 +269,30 @@ export class BrowserContext extends ChannelOwner { +export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise { if (options.videoSize && !options.videosPath) throw new Error(`"videoSize" option requires "videosPath" to be specified`); if (options.extraHTTPHeaders) network.validateHeaders(options.extraHTTPHeaders); - const contextOptions: channels.BrowserNewContextParams = { + const contextParams: channels.BrowserNewContextParams = { + sdkLanguage: 'javascript', ...options, viewport: options.viewport === null ? undefined : options.viewport, noDefaultViewport: options.viewport === null, extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, storageState: typeof options.storageState === 'string' ? JSON.parse(await fsReadFileAsync(options.storageState, 'utf8')) : options.storageState, }; - if (!contextOptions.recordVideo && options.videosPath) { - contextOptions.recordVideo = { + if (!contextParams.recordVideo && options.videosPath) { + contextParams.recordVideo = { dir: options.videosPath, size: options.videoSize }; } - return contextOptions; + return contextParams; } diff --git a/src/client/browserType.ts b/src/client/browserType.ts index 7db2ccc799..401b2c5628 100644 --- a/src/client/browserType.ts +++ b/src/client/browserType.ts @@ -16,7 +16,7 @@ import * as channels from '../protocol/channels'; import { Browser } from './browser'; -import { BrowserContext, prepareBrowserContextOptions } from './browserContext'; +import { BrowserContext, prepareBrowserContextParams } from './browserContext'; import { ChannelOwner } from './channelOwner'; import { LaunchOptions, LaunchServerOptions, ConnectOptions, LaunchPersistentContextOptions } from './types'; import WebSocket from 'ws'; @@ -94,17 +94,17 @@ export class BrowserType extends ChannelOwner { return this._wrapApiCall('browserType.launchPersistentContext', async () => { assert(!(options as any).port, 'Cannot specify a port without launching as a server.'); - const contextOptions = await prepareBrowserContextOptions(options); - const persistentOptions: channels.BrowserTypeLaunchPersistentContextParams = { - ...contextOptions, + const contextParams = await prepareBrowserContextParams(options); + const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = { + ...contextParams, ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined, ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs), env: options.env ? envObjectToArray(options.env) : undefined, userDataDir, }; - const result = await this._channel.launchPersistentContext(persistentOptions); + const result = await this._channel.launchPersistentContext(persistentParams); const context = BrowserContext.from(result.context); - context._options = contextOptions; + context._options = contextParams; context._logger = options.logger; return context; }, options.logger); @@ -192,6 +192,7 @@ export class BrowserType extends ChannelOwner { const result = await this._channel.connectOverCDP({ + sdkLanguage: 'javascript', wsEndpoint: params.wsEndpoint, slowMo: params.slowMo, timeout: params.timeout diff --git a/src/client/electron.ts b/src/client/electron.ts index ed50f9e228..eac33bc7dd 100644 --- a/src/client/electron.ts +++ b/src/client/electron.ts @@ -46,6 +46,7 @@ export class Electron extends ChannelOwner { return this._wrapApiCall('electron.launch', async () => { const params: channels.ElectronLaunchParams = { + sdkLanguage: 'javascript', ...options, env: envObjectToArray(options.env ? options.env : process.env), }; diff --git a/src/dispatchers/androidDispatcher.ts b/src/dispatchers/androidDispatcher.ts index 9ecb05747c..aba3f4f674 100644 --- a/src/dispatchers/androidDispatcher.ts +++ b/src/dispatchers/androidDispatcher.ts @@ -165,7 +165,7 @@ export class AndroidDeviceDispatcher extends Dispatcher { - return { context: new BrowserContextDispatcher(this._scope, await this._object.connectToWebView(params.pid)) }; + return { context: new BrowserContextDispatcher(this._scope, await this._object.connectToWebView(params.pid, params.sdkLanguage)) }; } } diff --git a/src/dispatchers/browserContextDispatcher.ts b/src/dispatchers/browserContextDispatcher.ts index 51e75f5a5d..fbd1cc4a13 100644 --- a/src/dispatchers/browserContextDispatcher.ts +++ b/src/dispatchers/browserContextDispatcher.ts @@ -23,6 +23,7 @@ import { CRBrowserContext } from '../server/chromium/crBrowser'; import { CDPSessionDispatcher } from './cdpSessionDispatcher'; import { RecorderSupplement } from '../server/supplements/recorderSupplement'; import { CallMetadata } from '../server/instrumentation'; +import { isUnderTest } from '../utils/utils'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { private _context: BrowserContext; @@ -38,8 +39,6 @@ export class BrowserContextDispatcher extends Dispatcher this._dispatchEvent('stdout', { data: Buffer.from(data, 'utf8').toString('base64') })); - context.on(BrowserContext.Events.StdErr, data => this._dispatchEvent('stderr', { data: Buffer.from(data, 'utf8').toString('base64') })); if (context._browser.options.name === 'chromium') { for (const page of (context as CRBrowserContext).backgroundPages()) @@ -134,12 +133,9 @@ export class BrowserContextDispatcher extends Dispatcher { - this._context.terminalSize = params; - } } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index e8ec138629..f473baff54 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -255,6 +255,7 @@ export type BrowserTypeLaunchResult = { }; export type BrowserTypeLaunchPersistentContextParams = { userDataDir: string, + sdkLanguage: string, executablePath?: string, args?: string[], ignoreAllDefaultArgs?: boolean, @@ -384,6 +385,7 @@ export type BrowserTypeLaunchPersistentContextResult = { context: BrowserContextChannel, }; export type BrowserTypeConnectOverCDPParams = { + sdkLanguage: string, wsEndpoint: string, slowMo?: number, timeout?: number, @@ -415,6 +417,7 @@ export type BrowserCloseParams = {}; export type BrowserCloseOptions = {}; export type BrowserCloseResult = void; export type BrowserNewContextParams = { + sdkLanguage: string, noDefaultViewport?: boolean, viewport?: { width: number, @@ -556,8 +559,6 @@ export interface BrowserContextChannel extends Channel { on(event: 'close', callback: (params: BrowserContextCloseEvent) => void): this; on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this; on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this; - on(event: 'stdout', callback: (params: BrowserContextStdoutEvent) => void): this; - on(event: 'stderr', callback: (params: BrowserContextStderrEvent) => void): this; on(event: 'crBackgroundPage', callback: (params: BrowserContextCrBackgroundPageEvent) => void): this; on(event: 'crServiceWorker', callback: (params: BrowserContextCrServiceWorkerEvent) => void): this; addCookies(params: BrowserContextAddCookiesParams, metadata?: Metadata): Promise; @@ -580,7 +581,6 @@ export interface BrowserContextChannel extends Channel { pause(params?: BrowserContextPauseParams, metadata?: Metadata): Promise; recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise; crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise; - setTerminalSizeNoReply(params: BrowserContextSetTerminalSizeNoReplyParams, metadata?: Metadata): Promise; } export type BrowserContextBindingCallEvent = { binding: BindingCallChannel, @@ -593,12 +593,6 @@ export type BrowserContextRouteEvent = { route: RouteChannel, request: RequestChannel, }; -export type BrowserContextStdoutEvent = { - data: Binary, -}; -export type BrowserContextStderrEvent = { - data: Binary, -}; export type BrowserContextCrBackgroundPageEvent = { page: PageChannel, }; @@ -731,22 +725,21 @@ export type BrowserContextPauseParams = {}; export type BrowserContextPauseOptions = {}; export type BrowserContextPauseResult = void; export type BrowserContextRecorderSupplementEnableParams = { - language: string, + language?: string, startRecording?: boolean, launchOptions?: any, contextOptions?: any, device?: string, saveStorage?: string, - terminal?: boolean, outputFile?: string, }; export type BrowserContextRecorderSupplementEnableOptions = { + language?: string, startRecording?: boolean, launchOptions?: any, contextOptions?: any, device?: string, saveStorage?: string, - terminal?: boolean, outputFile?: string, }; export type BrowserContextRecorderSupplementEnableResult = void; @@ -759,15 +752,6 @@ export type BrowserContextCrNewCDPSessionOptions = { export type BrowserContextCrNewCDPSessionResult = { session: CDPSessionChannel, }; -export type BrowserContextSetTerminalSizeNoReplyParams = { - rows?: number, - columns?: number, -}; -export type BrowserContextSetTerminalSizeNoReplyOptions = { - rows?: number, - columns?: number, -}; -export type BrowserContextSetTerminalSizeNoReplyResult = void; // ----------- Page ----------- export type PageInitializer = { @@ -2482,6 +2466,7 @@ export interface ElectronChannel extends Channel { launch(params: ElectronLaunchParams, metadata?: Metadata): Promise; } export type ElectronLaunchParams = { + sdkLanguage: string, executablePath?: string, args?: string[], cwd?: string, @@ -2783,6 +2768,7 @@ export type AndroidDeviceInputDragOptions = { }; export type AndroidDeviceInputDragResult = void; export type AndroidDeviceLaunchBrowserParams = { + sdkLanguage: string, pkg?: string, ignoreHTTPSErrors?: boolean, javaScriptEnabled?: boolean, @@ -2918,6 +2904,7 @@ export type AndroidDeviceSetDefaultTimeoutNoReplyOptions = { }; export type AndroidDeviceSetDefaultTimeoutNoReplyResult = void; export type AndroidDeviceConnectToWebViewParams = { + sdkLanguage: string, pid: number, }; export type AndroidDeviceConnectToWebViewOptions = { diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index e46126160b..0cef00d25c 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -313,6 +313,7 @@ BrowserType: launchPersistentContext: parameters: userDataDir: string + sdkLanguage: string executablePath: string? args: type: array? @@ -401,6 +402,7 @@ BrowserType: connectOverCDP: parameters: + sdkLanguage: string wsEndpoint: string slowMo: number? timeout: number? @@ -421,6 +423,7 @@ Browser: newContext: parameters: + sdkLanguage: string noDefaultViewport: boolean? viewport: type: object? @@ -623,13 +626,12 @@ BrowserContext: recorderSupplementEnable: experimental: True parameters: - language: string + language: string? startRecording: boolean? launchOptions: json? contextOptions: json? device: string? saveStorage: string? - terminal: boolean? outputFile: string? crNewCDPSession: @@ -638,11 +640,6 @@ BrowserContext: returns: session: CDPSession - setTerminalSizeNoReply: - parameters: - rows: number? - columns: number? - events: bindingCall: @@ -660,14 +657,6 @@ BrowserContext: route: Route request: Request - stdout: - parameters: - data: binary - - stderr: - parameters: - data: binary - crBackgroundPage: parameters: page: Page @@ -2106,6 +2095,7 @@ Electron: launch: parameters: + sdkLanguage: string executablePath: string? args: type: array? @@ -2322,6 +2312,7 @@ AndroidDevice: launchBrowser: parameters: + sdkLanguage: string pkg: string? ignoreHTTPSErrors: boolean? javaScriptEnabled: boolean? @@ -2415,6 +2406,7 @@ AndroidDevice: connectToWebView: parameters: + sdkLanguage: string pid: number returns: context: BrowserContext diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index ce965611da..03c2f1e266 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -168,6 +168,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { }); scheme.BrowserTypeLaunchPersistentContextParams = tObject({ userDataDir: tString, + sdkLanguage: tString, executablePath: tOptional(tString), args: tOptional(tArray(tString)), ignoreAllDefaultArgs: tOptional(tBoolean), @@ -231,12 +232,14 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { })), }); scheme.BrowserTypeConnectOverCDPParams = tObject({ + sdkLanguage: tString, wsEndpoint: tString, slowMo: tOptional(tNumber), timeout: tOptional(tNumber), }); scheme.BrowserCloseParams = tOptional(tObject({})); scheme.BrowserNewContextParams = tObject({ + sdkLanguage: tString, noDefaultViewport: tOptional(tBoolean), viewport: tOptional(tObject({ width: tNumber, @@ -349,22 +352,17 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { scheme.BrowserContextStorageStateParams = tOptional(tObject({})); scheme.BrowserContextPauseParams = tOptional(tObject({})); scheme.BrowserContextRecorderSupplementEnableParams = tObject({ - language: tString, + language: tOptional(tString), startRecording: tOptional(tBoolean), launchOptions: tOptional(tAny), contextOptions: tOptional(tAny), device: tOptional(tString), saveStorage: tOptional(tString), - terminal: tOptional(tBoolean), outputFile: tOptional(tString), }); scheme.BrowserContextCrNewCDPSessionParams = tObject({ page: tChannel('Page'), }); - scheme.BrowserContextSetTerminalSizeNoReplyParams = tObject({ - rows: tOptional(tNumber), - columns: tOptional(tNumber), - }); scheme.PageSetDefaultNavigationTimeoutNoReplyParams = tObject({ timeout: tNumber, }); @@ -925,6 +923,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { }); scheme.CDPSessionDetachParams = tOptional(tObject({})); scheme.ElectronLaunchParams = tObject({ + sdkLanguage: tString, executablePath: tOptional(tString), args: tOptional(tArray(tString)), cwd: tOptional(tString), @@ -1030,6 +1029,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { steps: tNumber, }); scheme.AndroidDeviceLaunchBrowserParams = tObject({ + sdkLanguage: tString, pkg: tOptional(tString), ignoreHTTPSErrors: tOptional(tBoolean), javaScriptEnabled: tOptional(tBoolean), @@ -1093,6 +1093,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { timeout: tNumber, }); scheme.AndroidDeviceConnectToWebViewParams = tObject({ + sdkLanguage: tString, pid: tNumber, }); scheme.AndroidDeviceCloseParams = tOptional(tObject({})); diff --git a/src/server/android/android.ts b/src/server/android/android.ts index d3402edeeb..3e98f84c7e 100644 --- a/src/server/android/android.ts +++ b/src/server/android/android.ts @@ -233,7 +233,7 @@ export class AndroidDevice extends SdkObject { this.emit(AndroidDevice.Events.Closed); } - async launchBrowser(pkg: string = 'com.android.chrome', options: types.BrowserContextOptions = {}): Promise { + async launchBrowser(pkg: string = 'com.android.chrome', options: types.BrowserContextOptions): Promise { debug('pw:android')('Force-stopping', pkg); await this._backend.runCommand(`shell:am force-stop ${pkg}`); @@ -245,14 +245,14 @@ export class AndroidDevice extends SdkObject { return await this._connectToBrowser(socketName, options); } - async connectToWebView(pid: number): Promise { + async connectToWebView(pid: number, sdkLanguage: string): Promise { const webView = this._webViews.get(pid); if (!webView) throw new Error('WebView has been closed'); - return await this._connectToBrowser(`webview_devtools_remote_${pid}`); + return await this._connectToBrowser(`webview_devtools_remote_${pid}`, { sdkLanguage }); } - private async _connectToBrowser(socketName: string, options: types.BrowserContextOptions = {}): Promise { + private async _connectToBrowser(socketName: string, options: types.BrowserContextOptions): Promise { const socket = await this._waitForLocalAbstract(socketName); const androidBrowser = new AndroidBrowser(this, socket); await androidBrowser._init(); diff --git a/src/server/browser.ts b/src/server/browser.ts index 47064f2233..698964a600 100644 --- a/src/server/browser.ts +++ b/src/server/browser.ts @@ -33,11 +33,10 @@ export interface BrowserProcess { export type PlaywrightOptions = { registry: registry.Registry, - isInternal: boolean, rootSdkObject: SdkObject, }; -export type BrowserOptions = PlaywrightOptions & types.UIOptions & { +export type BrowserOptions = PlaywrightOptions & { name: string, isChromium: boolean, downloadsPath?: string, @@ -47,6 +46,7 @@ export type BrowserOptions = PlaywrightOptions & types.UIOptions & { proxy?: ProxySettings, protocolLogger: types.ProtocolLogger, browserLogsCollector: RecentLogsCollector, + slowMo?: number; }; export abstract class Browser extends SdkObject { @@ -66,12 +66,12 @@ export abstract class Browser extends SdkObject { this.options = options; } - abstract newContext(options?: types.BrowserContextOptions): Promise; + abstract newContext(options: types.BrowserContextOptions): Promise; abstract contexts(): BrowserContext[]; abstract isConnected(): boolean; abstract version(): string; - async newPage(options?: types.BrowserContextOptions): Promise { + async newPage(options: types.BrowserContextOptions): Promise { const context = await this.newContext(options); const page = await context.newPage(); page._ownedContext = context; @@ -131,4 +131,3 @@ export abstract class Browser extends SdkObject { await new Promise(x => this.once(Browser.Events.Disconnected, x)); } } - diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 62dc2edbd7..6bf085489b 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -63,8 +63,6 @@ export abstract class BrowserContext extends SdkObject { Page: 'page', VideoStarted: 'videostarted', BeforeClose: 'beforeclose', - StdOut: 'stdout', - StdErr: 'stderr', }; readonly _timeoutSettings = new TimeoutSettings(); @@ -81,7 +79,6 @@ export abstract class BrowserContext extends SdkObject { readonly _browserContextId: string | undefined; private _selectors?: Selectors; private _origins = new Set(); - terminalSize: { rows?: number, columns?: number } = {}; constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { super(browser); @@ -281,7 +278,7 @@ export abstract class BrowserContext extends SdkObject { await this._browser.close(); // Bookkeeping. - await this.instrumentation.onContextWillDestroy(this); + await this.instrumentation.onContextDidDestroy(this); this._didCloseInternal(); } await this._closePromise; diff --git a/src/server/browserType.ts b/src/server/browserType.ts index 83502d2fa4..d1c3fb8c9f 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -69,7 +69,7 @@ export abstract class BrowserType extends SdkObject { return browser; } - async launchPersistentContext(metadata: CallMetadata, userDataDir?: string, options: types.LaunchPersistentOptions = {}): Promise { + async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: types.LaunchPersistentOptions): Promise { options = validateLaunchOptions(options); const controller = new ProgressController(metadata, this); const persistent: types.BrowserContextOptions = options; @@ -223,7 +223,7 @@ export abstract class BrowserType extends SdkObject { return { browserProcess, downloadsPath, transport }; } - async connectOverCDP(metadata: CallMetadata, wsEndpoint: string, uiOptions: types.UIOptions, timeout?: number): Promise { + async connectOverCDP(metadata: CallMetadata, wsEndpoint: string, options: { slowMo?: number, sdkLanguage: string }, timeout?: number): Promise { throw new Error('CDP connections are only supported by Chromium'); } diff --git a/src/server/chromium/chromium.ts b/src/server/chromium/chromium.ts index e2c388d4fc..db9b1c39d7 100644 --- a/src/server/chromium/chromium.ts +++ b/src/server/chromium/chromium.ts @@ -42,7 +42,7 @@ export class Chromium extends BrowserType { this._devtools = this._createDevTools(); } - async connectOverCDP(metadata: CallMetadata, wsEndpoint: string, uiOptions: types.UIOptions, timeout?: number) { + async connectOverCDP(metadata: CallMetadata, wsEndpoint: string, options: { slowMo?: number, sdkLanguage: string }, timeout?: number) { const controller = new ProgressController(metadata, this); controller.setLogName('browser'); const browserLogsCollector = new RecentLogsCollector(); @@ -58,10 +58,10 @@ export class Chromium extends BrowserType { }; const browserOptions: BrowserOptions = { ...this._playwrightOptions, - ...uiOptions, + slowMo: options.slowMo, name: 'chromium', isChromium: true, - persistent: { noDefaultViewport: true }, + persistent: { sdkLanguage: options.sdkLanguage, noDefaultViewport: true }, browserProcess, protocolLogger: helper.debugProtocolLogger(), browserLogsCollector, diff --git a/src/server/chromium/crBrowser.ts b/src/server/chromium/crBrowser.ts index 006e9653fb..8e5f6856c0 100644 --- a/src/server/chromium/crBrowser.ts +++ b/src/server/chromium/crBrowser.ts @@ -97,7 +97,7 @@ export class CRBrowser extends Browser { this._session.on('Target.detachedFromTarget', this._onDetachedFromTarget.bind(this)); } - async newContext(options: types.BrowserContextOptions = {}): Promise { + async newContext(options: types.BrowserContextOptions): Promise { validateBrowserContextOptions(options, this.options); const { browserContextId } = await this._session.send('Target.createBrowserContext', { disposeOnDetach: true, diff --git a/src/server/electron/electron.ts b/src/server/electron/electron.ts index 851104f51b..5d0b86c2b2 100644 --- a/src/server/electron/electron.ts +++ b/src/server/electron/electron.ts @@ -35,6 +35,7 @@ import { RecentLogsCollector } from '../../utils/debugLogger'; import { internalCallMetadata, SdkObject } from '../instrumentation'; export type ElectronLaunchOptionsBase = { + sdkLanguage: string, executablePath?: string, args?: string[], cwd?: string, @@ -130,7 +131,7 @@ export class Electron extends SdkObject { this._playwrightOptions = playwrightOptions; } - async launch(options: ElectronLaunchOptionsBase = {}): Promise { + async launch(options: ElectronLaunchOptionsBase): Promise { const { args = [], } = options; @@ -182,7 +183,7 @@ export class Electron extends SdkObject { name: 'electron', isChromium: true, headful: true, - persistent: { noDefaultViewport: true }, + persistent: { sdkLanguage: options.sdkLanguage, noDefaultViewport: true }, browserProcess, protocolLogger: helper.debugProtocolLogger(), browserLogsCollector, diff --git a/src/server/firefox/ffBrowser.ts b/src/server/firefox/ffBrowser.ts index 5a820f0818..90ab3f7696 100644 --- a/src/server/firefox/ffBrowser.ts +++ b/src/server/firefox/ffBrowser.ts @@ -71,7 +71,7 @@ export class FFBrowser extends Browser { return !this._connection._closed; } - async newContext(options: types.BrowserContextOptions = {}): Promise { + async newContext(options: types.BrowserContextOptions): Promise { validateBrowserContextOptions(options, this.options); if (options.isMobile) throw new Error('options.isMobile is not supported in Firefox'); diff --git a/src/server/playwright.ts b/src/server/playwright.ts index 436a55b1d0..e8909527af 100644 --- a/src/server/playwright.ts +++ b/src/server/playwright.ts @@ -48,7 +48,6 @@ export class Playwright extends SdkObject { const instrumentation = multiplexInstrumentation(listeners); super({ attribution: {}, instrumentation } as any); this.options = { - isInternal, registry: new Registry(path.join(__dirname, '..', '..')), rootSdkObject: this, }; diff --git a/src/server/supplements/inspectorController.ts b/src/server/supplements/inspectorController.ts index 6926ba3b47..51b623b89a 100644 --- a/src/server/supplements/inspectorController.ts +++ b/src/server/supplements/inspectorController.ts @@ -22,12 +22,8 @@ import { debugLogger } from '../../utils/debugLogger'; export class InspectorController implements InstrumentationListener { async onContextCreated(context: BrowserContext): Promise { - if (isDebugMode()) { - RecorderSupplement.getOrCreate(context, { - language: process.env.PW_CLI_TARGET_LANG || 'javascript', - terminal: true, - }); - } + if (isDebugMode()) + RecorderSupplement.getOrCreate(context); } onCallLog(logName: string, message: string): void { diff --git a/src/server/supplements/recorder/outputs.ts b/src/server/supplements/recorder/outputs.ts index 0250d738b9..693266bbc5 100644 --- a/src/server/supplements/recorder/outputs.ts +++ b/src/server/supplements/recorder/outputs.ts @@ -15,8 +15,6 @@ */ import fs from 'fs'; -import * as querystring from 'querystring'; -import * as hljs from '../../../third_party/highlightjs/highlightjs'; export interface RecorderOutput { printLn(text: string): void; @@ -111,79 +109,3 @@ export class FileOutput extends BufferedOutput implements RecorderOutput { fs.writeFileSync(this._fileName, this.buffer()); } } - -export class TerminalOutput implements RecorderOutput { - private _output: Writable; - private _language: string; - - static create(output: Writable, language: string) { - if (process.stdout.columns) - return new TerminalOutput(output, language); - return new FlushingTerminalOutput(output); - } - - constructor(output: Writable, language: string) { - this._output = output; - this._language = language; - } - - private _highlight(text: string) { - let highlightedCode = hljs.highlight(this._language, text).value; - highlightedCode = querystring.unescape(highlightedCode); - highlightedCode = highlightedCode.replace(//g, '\x1b[38;5;205m'); - highlightedCode = highlightedCode.replace(//g, '\x1b[38;5;220m'); - highlightedCode = highlightedCode.replace(//g, '\x1b[38;5;159m'); - highlightedCode = highlightedCode.replace(//g, ''); - highlightedCode = highlightedCode.replace(//g, '\x1b[38;5;78m'); - highlightedCode = highlightedCode.replace(//g, '\x1b[38;5;130m'); - highlightedCode = highlightedCode.replace(//g, '\x1b[38;5;23m'); - highlightedCode = highlightedCode.replace(//g, '\x1b[38;5;242m'); - highlightedCode = highlightedCode.replace(//g, ''); - highlightedCode = highlightedCode.replace(//g, ''); - highlightedCode = highlightedCode.replace(//g, ''); - highlightedCode = highlightedCode.replace(/<\/span>/g, '\x1b[0m'); - highlightedCode = highlightedCode.replace(/'/g, "'"); - highlightedCode = highlightedCode.replace(/"/g, '"'); - highlightedCode = highlightedCode.replace(/>/g, '>'); - highlightedCode = highlightedCode.replace(/</g, '<'); - highlightedCode = highlightedCode.replace(/&/g, '&'); - return highlightedCode; - } - - printLn(text: string) { - // Split into lines for highlighter to not fail. - for (const line of text.split('\n')) - this._output.write(this._highlight(line) + '\n'); - } - - popLn(text: string) { - const terminalWidth = this._output.columns(); - for (const line of text.split('\n')) { - const terminalLines = ((line.length - 1) / terminalWidth | 0) + 1; - for (let i = 0; i < terminalLines; ++i) - this._output.write('\u001B[1A\u001B[2K'); - } - } - - flush() {} -} - -export class FlushingTerminalOutput extends BufferedOutput implements RecorderOutput { - private _output: Writable - - constructor(output: Writable) { - super(); - this._output = output; - } - - printLn(text: string) { - super.printLn(text); - this._output.write('-------------8<-------------\n'); - this._output.write(this.buffer() + '\n'); - this._output.write('-------------8<-------------\n'); - } - - flush() { - this._output.write(this.buffer() + '\n'); - } -} diff --git a/src/server/supplements/recorder/recorderApp.ts b/src/server/supplements/recorder/recorderApp.ts index e9d3b828ff..6e6172e4cd 100644 --- a/src/server/supplements/recorder/recorderApp.ts +++ b/src/server/supplements/recorder/recorderApp.ts @@ -23,6 +23,8 @@ import { ProgressController } from '../../progress'; import { createPlaywright } from '../../playwright'; import { EventEmitter } from 'events'; import { internalCallMetadata } from '../../instrumentation'; +import { isUnderTest } from '../../../utils/utils'; +import { BrowserContext } from '../../browserContext'; const readFileAsync = util.promisify(fs.readFile); @@ -90,14 +92,17 @@ export class RecorderApp extends EventEmitter { await mainFrame.goto(internalCallMetadata(), 'https://playwright/index.html'); } - static async open(): Promise { + static async open(inspectedContext: BrowserContext): Promise { const recorderPlaywright = createPlaywright(true); - const context = await recorderPlaywright.chromium.launchPersistentContext(internalCallMetadata(), undefined, { + const context = await recorderPlaywright.chromium.launchPersistentContext(internalCallMetadata(), '', { + sdkLanguage: inspectedContext._options.sdkLanguage, args: [ '--app=data:text/html,', - '--window-size=300,800', + '--window-size=600,600', + '--window-position=1280,10', ], - noDefaultViewport: true + noDefaultViewport: true, + headless: isUnderTest() }); const controller = new ProgressController(internalCallMetadata(), context._browser); @@ -127,6 +132,15 @@ export class RecorderApp extends EventEmitter { await this._page.mainFrame()._evaluateExpression(((param: { text: string, language: string }) => { window.playwrightSetSource(param); }).toString(), true, { text, language }, 'main').catch(() => {}); + + // Testing harness for runCLI mode. + { + if (process.env.PWCLI_EXIT_FOR_TEST) { + process.stdout.write('\n-------------8<-------------\n'); + process.stdout.write(text); + process.stdout.write('\n-------------8<-------------\n'); + } + } } async bringToFront() { diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts index d6e3cf2a94..d2081558a8 100644 --- a/src/server/supplements/recorderSupplement.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -27,7 +27,7 @@ import { CSharpLanguageGenerator } from './recorder/csharp'; import { PythonLanguageGenerator } from './recorder/python'; import * as recorderSource from '../../generated/recorderSource'; import * as consoleApiSource from '../../generated/consoleApiSource'; -import { BufferedOutput, FileOutput, FlushingTerminalOutput, OutputMultiplexer, RecorderOutput, TerminalOutput, Writable } from './recorder/outputs'; +import { BufferedOutput, FileOutput, OutputMultiplexer, RecorderOutput } from './recorder/outputs'; import { EventData, Mode, RecorderApp } from './recorder/recorderApp'; import { internalCallMetadata } from '../instrumentation'; @@ -51,7 +51,7 @@ export class RecorderSupplement { private _highlighterType: string; private _params: channels.BrowserContextRecorderSupplementEnableParams; - static getOrCreate(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams): Promise { + static getOrCreate(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise { let recorderPromise = (context as any)[symbol] as Promise; if (!recorderPromise) { const recorder = new RecorderSupplement(context, params); @@ -66,22 +66,19 @@ export class RecorderSupplement { this._params = params; this._mode = params.startRecording ? 'recording' : 'none'; let languageGenerator: LanguageGenerator; - switch (params.language) { + const language = params.language || context._options.sdkLanguage; + switch (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}'`); } - let highlighterType = params.language; + let highlighterType = language; if (highlighterType === 'python-async') highlighterType = 'python'; - const writable: Writable = { - write: (text: string) => context.emit(BrowserContext.Events.StdOut, text), - columns: () => context.terminalSize.columns || 80 - }; - const outputs: RecorderOutput[] = [params.terminal ? new TerminalOutput(writable, highlighterType) : new FlushingTerminalOutput(writable)]; + const outputs: RecorderOutput[] = []; this._highlighterType = highlighterType; this._bufferedOutput = new BufferedOutput(async text => { if (this._recorderApp) @@ -99,7 +96,7 @@ export class RecorderSupplement { } async install() { - const recorderApp = await RecorderApp.open(); + const recorderApp = await RecorderApp.open(this._context); this._recorderApp = recorderApp; recorderApp.once('close', () => { this._recorderApp = null; @@ -153,10 +150,6 @@ export class RecorderSupplement { await this._context.exposeBinding('_playwrightRecorderCommitAction', false, (source: BindingSource, action: actions.Action) => this._generator.commitLastAction()); - await this._context.exposeBinding('_playwrightRecorderPrintSelector', false, (_, text) => { - this._context.emit(BrowserContext.Events.StdOut, `Selector: \x1b[38;5;130m${text}\x1b[0m\n`); - }); - await this._context.exposeBinding('_playwrightRecorderState', false, () => { return { mode: this._mode }; }); diff --git a/src/server/types.ts b/src/server/types.ts index 1e2486990a..7d172f8af5 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -215,6 +215,7 @@ export type SetNetworkCookieParam = { }; export type BrowserContextOptions = { + sdkLanguage: string, viewport?: Size, noDefaultViewport?: boolean, ignoreHTTPSErrors?: boolean, @@ -248,7 +249,7 @@ export type BrowserContextOptions = { export type EnvArray = { name: string, value: string }[]; -type LaunchOptionsBase = UIOptions & { +type LaunchOptionsBase = { executablePath?: string, args?: string[], ignoreDefaultArgs?: string[], @@ -263,6 +264,7 @@ type LaunchOptionsBase = UIOptions & { proxy?: ProxySettings, downloadsPath?: string, chromiumSandbox?: boolean, + slowMo?: number; }; export type LaunchOptions = LaunchOptionsBase & { firefoxUserPrefs?: { [key: string]: string | number | boolean }, @@ -338,7 +340,3 @@ export type SetStorageState = { cookies?: SetNetworkCookieParam[], origins?: OriginStorage[] } - -export type UIOptions = { - slowMo?: number; -} diff --git a/src/server/webkit/wkBrowser.ts b/src/server/webkit/wkBrowser.ts index 51aa7fa346..6c73435847 100644 --- a/src/server/webkit/wkBrowser.ts +++ b/src/server/webkit/wkBrowser.ts @@ -73,7 +73,7 @@ export class WKBrowser extends Browser { this._didClose(); } - async newContext(options: types.BrowserContextOptions = {}): Promise { + async newContext(options: types.BrowserContextOptions): Promise { validateBrowserContextOptions(options, this.options); const createOptions = options.proxy ? { proxyServer: options.proxy.server, diff --git a/src/web/recorder/recorder.tsx b/src/web/recorder/recorder.tsx index fc54a7dba9..7fdb5275de 100644 --- a/src/web/recorder/recorder.tsx +++ b/src/web/recorder/recorder.tsx @@ -28,6 +28,7 @@ declare global { playwrightSetPaused: (paused: boolean) => void; playwrightSetSource: (params: { text: string, language: string }) => void; dispatch(data: any): Promise; + playwrightSourceEchoForTest?: (text: string) => Promise; } } @@ -43,6 +44,8 @@ export const Recorder: React.FC = ({ window.playwrightSetMode = setMode; window.playwrightSetSource = setSource; window.playwrightSetPaused = setPaused; + if (window.playwrightSourceEchoForTest) + window.playwrightSourceEchoForTest(source.text).catch(e => {}); return
diff --git a/test/cli/cli-codegen.spec.ts b/test/cli/cli-codegen-1.spec.ts similarity index 53% rename from test/cli/cli-codegen.spec.ts rename to test/cli/cli-codegen-1.spec.ts index 384714ac69..8e4f67355d 100644 --- a/test/cli/cli-codegen.spec.ts +++ b/test/cli/cli-codegen-1.spec.ts @@ -16,12 +16,12 @@ import { folio } from './cli.fixtures'; import * as http from 'http'; -import * as url from 'url'; const { it, describe, expect } = folio; -describe('cli codegen', (test, { browserName, headful }) => { - test.fixme(browserName === 'firefox' && headful, 'Focus is off'); +describe('cli codegen', (suite, { mode, browserName, headful }) => { + suite.fixme(browserName === 'firefox' && headful, 'Focus is off'); + suite.skip(mode !== 'default'); }, () => { it('should click', async ({ page, recorder }) => { await recorder.setContentAndWait(``); @@ -351,309 +351,4 @@ describe('cli codegen', (test, { browserName, headful }) => { ]);`); expect(page.url()).toContain('about:blank#foo'); }); - - it('should contain open page', async ({ recorder }) => { - await recorder.setContentAndWait(``); - expect(recorder.output()).toContain(`const page = await context.newPage();`); - }); - - it('should contain second page', async ({ contextWrapper, recorder }) => { - await recorder.setContentAndWait(``); - await contextWrapper.context.newPage(); - await recorder.waitForOutput('page1'); - expect(recorder.output()).toContain('const page1 = await context.newPage();'); - }); - - it('should contain close page', async ({ contextWrapper, recorder }) => { - await recorder.setContentAndWait(``); - await contextWrapper.context.newPage(); - await recorder.page.close(); - await recorder.waitForOutput('page.close();'); - }); - - it('should not lead to an error if html gets clicked', async ({ contextWrapper, recorder }) => { - await recorder.setContentAndWait(''); - await contextWrapper.context.newPage(); - const errors: any[] = []; - recorder.page.on('pageerror', e => errors.push(e)); - await recorder.page.evaluate(() => document.querySelector('body').remove()); - const selector = await recorder.hoverOverElement('html'); - expect(selector).toBe('html'); - await recorder.page.close(); - await recorder.waitForOutput('page.close();'); - expect(errors.length).toBe(0); - }); - - it('should upload a single file', async ({ page, recorder }) => { - await recorder.setContentAndWait(` -
- -
- `); - - await page.focus('input[type=file]'); - await page.setInputFiles('input[type=file]', 'test/assets/file-to-upload.txt'); - await page.click('input[type=file]'); - - await recorder.waitForOutput(` - // Upload file-to-upload.txt - await page.setInputFiles('input[type="file"]', 'file-to-upload.txt');`); - }); - - it('should upload multiple files', async ({ page, recorder }) => { - await recorder.setContentAndWait(` -
- -
- `); - - await page.focus('input[type=file]'); - 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(` - // 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']);`); - }); - - it('should clear files', async ({ page, recorder }) => { - await recorder.setContentAndWait(` -
- -
- `); - await page.focus('input[type=file]'); - await page.setInputFiles('input[type=file]', 'test/assets/file-to-upload.txt'); - await page.setInputFiles('input[type=file]', []); - await page.click('input[type=file]'); - - await recorder.waitForOutput(` - // Clear selected files - await page.setInputFiles('input[type="file"]', []);`); - }); - - 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') { - res.setHeader('Content-Type', 'application/octet-stream'); - res.setHeader('Content-Disposition', 'attachment; filename=file.txt'); - res.end(`Hello world`); - } else { - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - res.end(''); - } - }); - await recorder.setContentAndWait(` - Download - `, httpServer.PREFIX); - await recorder.hoverOverElement('text=Download'); - await Promise.all([ - page.waitForEvent('download'), - page.click('text=Download') - ]); - await recorder.waitForOutput(` - // Click text=Download - const [download] = await Promise.all([ - page.waitForEvent('download'), - page.click('text=Download') - ]);`); - }); - - it('should handle dialogs', async ({ page, recorder }) => { - await recorder.setContentAndWait(` - - `); - await recorder.hoverOverElement('button'); - page.once('dialog', async dialog => { - await dialog.dismiss(); - }); - await page.click('text=click me'); - await recorder.waitForOutput(` - // Click text=click me - page.once('dialog', dialog => { - console.log(\`Dialog message: $\{dialog.message()}\`); - dialog.dismiss().catch(() => {}); - }); - await page.click('text=click me')`); - }); - - it('should handle history.postData', async ({ page, recorder, httpServer }) => { - httpServer.setHandler((req: http.IncomingMessage, res: http.ServerResponse) => { - res.setHeader('Content-Type', 'text/html; charset=utf-8'); - res.end('Hello world'); - }); - await recorder.setContentAndWait(` - `, httpServer.PREFIX); - for (let i = 1; i < 3; ++i) { - await page.evaluate('pushState()'); - await recorder.waitForOutput(`await page.goto('${httpServer.PREFIX}/#seqNum=${i}');`); - } - }); - - it('should record open in a new tab with url', (test, { browserName }) => { - test.fixme(browserName === 'webkit', 'Ctrl+click does not open in new tab on WebKit'); - }, async ({ page, recorder, browserName, platform }) => { - await recorder.setContentAndWait(`link`); - - const selector = await recorder.hoverOverElement('a'); - expect(selector).toBe('text=link'); - - await page.click('a', { modifiers: [ platform === 'darwin' ? 'Meta' : 'Control'] }); - await recorder.waitForOutput('page1'); - if (browserName === 'chromium') { - expect(recorder.output()).toContain(` - // Open new page - const page1 = await context.newPage(); - page1.goto('about:blank?foo');`); - } else if (browserName === 'firefox') { - expect(recorder.output()).toContain(` - // Click text=link - const [page1] = await Promise.all([ - page.waitForEvent('popup'), - page.click('text=link', { - modifiers: ['${platform === 'darwin' ? 'Meta' : 'Control'}'] - }) - ]);`); - } - }); - - it('should not clash pages', (test, { browserName }) => { - test.fixme(browserName === 'firefox', 'Times out on Firefox, maybe the focus issue'); - }, async ({ page, recorder }) => { - const [popup1] = await Promise.all([ - page.context().waitForEvent('page'), - page.evaluate(`window.open('about:blank')`) - ]); - await recorder.setPageContentAndWait(popup1, ''); - - const [popup2] = await Promise.all([ - page.context().waitForEvent('page'), - page.evaluate(`window.open('about:blank')`) - ]); - await recorder.setPageContentAndWait(popup2, ''); - - await popup1.type('input', 'TextA'); - await recorder.waitForOutput('TextA'); - - await popup2.type('input', 'TextB'); - await recorder.waitForOutput('TextB'); - - expect(recorder.output()).toContain(`await page1.fill('input', 'TextA');`); - expect(recorder.output()).toContain(`await page2.fill('input', 'TextB');`); - }); - - it('click should emit events in order', async ({ page, recorder }) => { - await recorder.setContentAndWait(` - + `); + await recorder.hoverOverElement('button'); + page.once('dialog', async dialog => { + await dialog.dismiss(); + }); + await page.click('text=click me'); + await recorder.waitForOutput(` + // Click text=click me + page.once('dialog', dialog => { + console.log(\`Dialog message: $\{dialog.message()}\`); + dialog.dismiss().catch(() => {}); + }); + await page.click('text=click me')`); + }); + + it('should handle history.postData', async ({ page, recorder, httpServer }) => { + httpServer.setHandler((req: http.IncomingMessage, res: http.ServerResponse) => { + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.end('Hello world'); + }); + await recorder.setContentAndWait(` + `, httpServer.PREFIX); + for (let i = 1; i < 3; ++i) { + await page.evaluate('pushState()'); + await recorder.waitForOutput(`await page.goto('${httpServer.PREFIX}/#seqNum=${i}');`); + } + }); + + it('should record open in a new tab with url', (test, { browserName }) => { + test.fixme(browserName === 'webkit', 'Ctrl+click does not open in new tab on WebKit'); + }, async ({ page, recorder, browserName, platform }) => { + await recorder.setContentAndWait(`link`); + + const selector = await recorder.hoverOverElement('a'); + expect(selector).toBe('text=link'); + + await page.click('a', { modifiers: [ platform === 'darwin' ? 'Meta' : 'Control'] }); + await recorder.waitForOutput('page1'); + if (browserName === 'chromium') { + expect(recorder.output()).toContain(` + // Open new page + const page1 = await context.newPage(); + page1.goto('about:blank?foo');`); + } else if (browserName === 'firefox') { + expect(recorder.output()).toContain(` + // Click text=link + const [page1] = await Promise.all([ + page.waitForEvent('popup'), + page.click('text=link', { + modifiers: ['${platform === 'darwin' ? 'Meta' : 'Control'}'] + }) + ]);`); + } + }); + + it('should not clash pages', (test, { browserName }) => { + test.fixme(browserName === 'firefox', 'Times out on Firefox, maybe the focus issue'); + }, async ({ page, recorder }) => { + const [popup1] = await Promise.all([ + page.context().waitForEvent('page'), + page.evaluate(`window.open('about:blank')`) + ]); + await recorder.setPageContentAndWait(popup1, ''); + + const [popup2] = await Promise.all([ + page.context().waitForEvent('page'), + page.evaluate(`window.open('about:blank')`) + ]); + await recorder.setPageContentAndWait(popup2, ''); + + await popup1.type('input', 'TextA'); + await recorder.waitForOutput('TextA'); + + await popup2.type('input', 'TextB'); + await recorder.waitForOutput('TextB'); + + expect(recorder.output()).toContain(`await page1.fill('input', 'TextA');`); + expect(recorder.output()).toContain(`await page2.fill('input', 'TextB');`); + }); + + it('click should emit events in order', async ({ page, recorder }) => { + await recorder.setContentAndWait(` +