chore(recorder): move recording output into the gui app (#5425)

This commit is contained in:
Pavel Feldman 2021-02-11 17:46:54 -08:00 committed by GitHub
parent a42c46b986
commit 449adfd3ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 583 additions and 698 deletions

View file

@ -15,5 +15,4 @@
*/
const { setUnderTest } = require('./lib/utils/utils');
setUnderTest(); // Note: we must call setUnderTest before initializing.
module.exports = require('./lib/inprocess');

View file

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

View file

@ -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<channels.AndroidDeviceChannel, c
async launchBrowser(options: types.BrowserContextOptions & { pkg?: string } = {}): Promise<ChromiumBrowserContext> {
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<Page> {
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];
});
}

View file

@ -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<channels.BrowserChannel, channels.Brow
return this._wrapApiCall('browser.newContext', async () => {
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);

View file

@ -43,9 +43,9 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
_timeoutSettings = new TimeoutSettings();
_ownerPage: Page | undefined;
private _closedPromise: Promise<void>;
_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<channels.BrowserContextChannel,
this._channel.on('close', () => 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<channels.BrowserContextChannel,
device?: string,
saveStorage?: string,
startRecording?: boolean,
terminal?: boolean,
outputFile?: string
}) {
this._pushTerminalSize();
await this._channel.recorderSupplementEnable(params);
}
}
export async function prepareBrowserContextOptions(options: BrowserContextOptions): Promise<channels.BrowserNewContextOptions> {
export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
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;
}

View file

@ -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<channels.BrowserTypeChannel, chann
async launchPersistentContext(userDataDir: string, options: LaunchPersistentContextOptions = {}): Promise<BrowserContext> {
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<channels.BrowserTypeChannel, chann
const logger = params.logger;
return this._wrapApiCall('browserType.connectOverCDP', async () => {
const result = await this._channel.connectOverCDP({
sdkLanguage: 'javascript',
wsEndpoint: params.wsEndpoint,
slowMo: params.slowMo,
timeout: params.timeout

View file

@ -46,6 +46,7 @@ export class Electron extends ChannelOwner<channels.ElectronChannel, channels.El
async launch(options: ElectronOptions = {}): Promise<ElectronApplication> {
return this._wrapApiCall('electron.launch', async () => {
const params: channels.ElectronLaunchParams = {
sdkLanguage: 'javascript',
...options,
env: envObjectToArray(options.env ? options.env : process.env),
};

View file

@ -165,7 +165,7 @@ export class AndroidDeviceDispatcher extends Dispatcher<AndroidDevice, channels.
}
async connectToWebView(params: channels.AndroidDeviceConnectToWebViewParams): Promise<channels.AndroidDeviceConnectToWebViewResult> {
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)) };
}
}

View file

@ -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<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel {
private _context: BrowserContext;
@ -38,8 +39,6 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
this._dispatchEvent('close');
this._dispose();
});
context.on(BrowserContext.Events.StdOut, data => 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<BrowserContext, channel
}
async pause() {
if (!this._context._browser.options.headful)
if (!this._context._browser.options.headful && !isUnderTest())
return;
const recorder = await RecorderSupplement.getOrCreate(this._context, {
language: 'javascript',
terminal: true
});
const recorder = await RecorderSupplement.getOrCreate(this._context);
await recorder.pause();
}
@ -149,8 +145,4 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
const crBrowserContext = this._object as CRBrowserContext;
return { session: new CDPSessionDispatcher(this._scope, await crBrowserContext.newCDPSession((params.page as PageDispatcher)._object)) };
}
async setTerminalSizeNoReply(params: channels.BrowserContextSetTerminalSizeNoReplyParams): Promise<void> {
this._context.terminalSize = params;
}
}

View file

@ -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<BrowserContextAddCookiesResult>;
@ -580,7 +581,6 @@ export interface BrowserContextChannel extends Channel {
pause(params?: BrowserContextPauseParams, metadata?: Metadata): Promise<BrowserContextPauseResult>;
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise<BrowserContextRecorderSupplementEnableResult>;
crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise<BrowserContextCrNewCDPSessionResult>;
setTerminalSizeNoReply(params: BrowserContextSetTerminalSizeNoReplyParams, metadata?: Metadata): Promise<BrowserContextSetTerminalSizeNoReplyResult>;
}
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<ElectronLaunchResult>;
}
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 = {

View file

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

View file

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

View file

@ -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<BrowserContext> {
async launchBrowser(pkg: string = 'com.android.chrome', options: types.BrowserContextOptions): Promise<BrowserContext> {
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<BrowserContext> {
async connectToWebView(pid: number, sdkLanguage: string): Promise<BrowserContext> {
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<BrowserContext> {
private async _connectToBrowser(socketName: string, options: types.BrowserContextOptions): Promise<BrowserContext> {
const socket = await this._waitForLocalAbstract(socketName);
const androidBrowser = new AndroidBrowser(this, socket);
await androidBrowser._init();

View file

@ -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<BrowserContext>;
abstract newContext(options: types.BrowserContextOptions): Promise<BrowserContext>;
abstract contexts(): BrowserContext[];
abstract isConnected(): boolean;
abstract version(): string;
async newPage(options?: types.BrowserContextOptions): Promise<Page> {
async newPage(options: types.BrowserContextOptions): Promise<Page> {
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));
}
}

View file

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

View file

@ -69,7 +69,7 @@ export abstract class BrowserType extends SdkObject {
return browser;
}
async launchPersistentContext(metadata: CallMetadata, userDataDir?: string, options: types.LaunchPersistentOptions = {}): Promise<BrowserContext> {
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: types.LaunchPersistentOptions): Promise<BrowserContext> {
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<Browser> {
async connectOverCDP(metadata: CallMetadata, wsEndpoint: string, options: { slowMo?: number, sdkLanguage: string }, timeout?: number): Promise<Browser> {
throw new Error('CDP connections are only supported by Chromium');
}

View file

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

View file

@ -97,7 +97,7 @@ export class CRBrowser extends Browser {
this._session.on('Target.detachedFromTarget', this._onDetachedFromTarget.bind(this));
}
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
async newContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
validateBrowserContextOptions(options, this.options);
const { browserContextId } = await this._session.send('Target.createBrowserContext', {
disposeOnDetach: true,

View file

@ -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<ElectronApplication> {
async launch(options: ElectronLaunchOptionsBase): Promise<ElectronApplication> {
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,

View file

@ -71,7 +71,7 @@ export class FFBrowser extends Browser {
return !this._connection._closed;
}
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
async newContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
validateBrowserContextOptions(options, this.options);
if (options.isMobile)
throw new Error('options.isMobile is not supported in Firefox');

View file

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

View file

@ -22,12 +22,8 @@ import { debugLogger } from '../../utils/debugLogger';
export class InspectorController implements InstrumentationListener {
async onContextCreated(context: BrowserContext): Promise<void> {
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 {

View file

@ -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(/<span class="hljs-keyword">/g, '\x1b[38;5;205m');
highlightedCode = highlightedCode.replace(/<span class="hljs-built_in">/g, '\x1b[38;5;220m');
highlightedCode = highlightedCode.replace(/<span class="hljs-literal">/g, '\x1b[38;5;159m');
highlightedCode = highlightedCode.replace(/<span class="hljs-title">/g, '');
highlightedCode = highlightedCode.replace(/<span class="hljs-number">/g, '\x1b[38;5;78m');
highlightedCode = highlightedCode.replace(/<span class="hljs-string">/g, '\x1b[38;5;130m');
highlightedCode = highlightedCode.replace(/<span class="hljs-comment">/g, '\x1b[38;5;23m');
highlightedCode = highlightedCode.replace(/<span class="hljs-subst">/g, '\x1b[38;5;242m');
highlightedCode = highlightedCode.replace(/<span class="hljs-function">/g, '');
highlightedCode = highlightedCode.replace(/<span class="hljs-params">/g, '');
highlightedCode = highlightedCode.replace(/<span class="hljs-attr">/g, '');
highlightedCode = highlightedCode.replace(/<\/span>/g, '\x1b[0m');
highlightedCode = highlightedCode.replace(/&#x27;/g, "'");
highlightedCode = highlightedCode.replace(/&quot;/g, '"');
highlightedCode = highlightedCode.replace(/&gt;/g, '>');
highlightedCode = highlightedCode.replace(/&lt;/g, '<');
highlightedCode = highlightedCode.replace(/&amp;/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');
}
}

View file

@ -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<RecorderApp> {
static async open(inspectedContext: BrowserContext): Promise<RecorderApp> {
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() {

View file

@ -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<RecorderSupplement> {
static getOrCreate(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<RecorderSupplement> {
let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
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 };
});

View file

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

View file

@ -73,7 +73,7 @@ export class WKBrowser extends Browser {
this._didClose();
}
async newContext(options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
async newContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
validateBrowserContextOptions(options, this.options);
const createOptions = options.proxy ? {
proxyServer: options.proxy.server,

View file

@ -28,6 +28,7 @@ declare global {
playwrightSetPaused: (paused: boolean) => void;
playwrightSetSource: (params: { text: string, language: string }) => void;
dispatch(data: any): Promise<void>;
playwrightSourceEchoForTest?: (text: string) => Promise<void>;
}
}
@ -43,6 +44,8 @@ export const Recorder: React.FC<RecorderProps> = ({
window.playwrightSetMode = setMode;
window.playwrightSetSource = setSource;
window.playwrightSetPaused = setPaused;
if (window.playwrightSourceEchoForTest)
window.playwrightSourceEchoForTest(source.text).catch(e => {});
return <div className="recorder">
<Toolbar>

View file

@ -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(`<button onclick="console.log('click')">Submit</button>`);
@ -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(`
<form>
<input type="file">
</form>
`);
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(`
<form>
<input type="file" multiple>
</form>
`);
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(`
<form>
<input type="file" multiple>
</form>
`);
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(`
<a href="${httpServer.PREFIX}/download" download>Download</a>
`, 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(`
<button onclick="alert()">click me</button>
`);
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(`
<script>
let seqNum = 0;
function pushState() {
history.pushState({}, 'title', '${httpServer.PREFIX}/#seqNum=' + (++seqNum));
}
</script>`, 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(`<a href="about:blank?foo">link</a>`);
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, '<input id=name>');
const [popup2] = await Promise.all([
page.context().waitForEvent('page'),
page.evaluate(`window.open('about:blank')`)
]);
await recorder.setPageContentAndWait(popup2, '<input id=name>');
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(`
<button id=button>
<script>
button.addEventListener('mousedown', e => console.log(e.type));
button.addEventListener('mouseup', e => console.log(e.type));
button.addEventListener('click', e => console.log(e.type));
</script>
`);
const messages: any[] = [];
page.on('console', message => messages.push(message.text()));
await Promise.all([
page.click('button'),
recorder.waitForOutput('page.click')
]);
expect(messages).toEqual(['mousedown', 'mouseup', 'click']);
});
it('should update hover model on action', async ({ page, recorder }) => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
const [ models ] = await Promise.all([
recorder.waitForActionPerformed(),
page.click('input')
]);
expect(models.hovered).toBe('input[name="updated"]');
});
it('should update active model on action', (test, { browserName, headful }) => {
test.fixme(browserName === 'webkit' && !headful);
test.fixme(browserName === 'firefox' && !headful);
}, async ({ page, recorder }) => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
const [ models ] = await Promise.all([
recorder.waitForActionPerformed(),
page.click('input')
]);
expect(models.active).toBe('input[name="updated"]');
});
it('should check input with chaning id', async ({ page, recorder }) => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name = 'updated'"></input>`);
await Promise.all([
recorder.waitForActionPerformed(),
page.click('input[id=checkbox]')
]);
});
it('should prefer frame name', async ({ page, recorder, server }) => {
await recorder.setContentAndWait(`
<iframe src='./frames/frame.html' name='one'></iframe>
<iframe src='./frames/frame.html' name='two'></iframe>
<iframe src='./frames/frame.html'></iframe>
`, server.EMPTY_PAGE, 4);
const frameOne = page.frame({ name: 'one' });
const frameTwo = page.frame({ name: 'two' });
const otherFrame = page.frames().find(f => f !== page.mainFrame() && !f.name());
await Promise.all([
recorder.waitForOutput('one'),
frameOne.click('div'),
]);
expect(recorder.output()).toContain(`
// Click text=Hi, I'm frame
await page.frame({
name: 'one'
}).click('text=Hi, I\\'m frame');`);
await Promise.all([
recorder.waitForOutput('two'),
frameTwo.click('div'),
]);
expect(recorder.output()).toContain(`
// Click text=Hi, I'm frame
await page.frame({
name: 'two'
}).click('text=Hi, I\\'m frame');`);
await Promise.all([
recorder.waitForOutput('url: \''),
otherFrame.click('div'),
]);
expect(recorder.output()).toContain(`
// Click text=Hi, I'm frame
await page.frame({
url: '${otherFrame.url()}'
}).click('text=Hi, I\\'m frame');`);
});
it('should record navigations after identical pushState', 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(`
<script>
function pushState() {
history.pushState({}, 'title', '${httpServer.PREFIX}');
}
</script>`, httpServer.PREFIX);
for (let i = 1; i < 3; ++i)
await page.evaluate('pushState()');
await page.goto(httpServer.PREFIX + '/page2.html');
await recorder.waitForOutput(`await page.goto('${httpServer.PREFIX}/page2.html');`);
});
});

View file

@ -0,0 +1,337 @@
/**
* 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 { folio } from './cli.fixtures';
import * as http from 'http';
import * as url from 'url';
const { it, describe, expect } = folio;
describe('cli codegen', (suite, { mode, browserName, headful }) => {
suite.fixme(browserName === 'firefox' && headful, 'Focus is off');
suite.skip(mode !== 'default');
}, () => {
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 ({ context, recorder }) => {
await recorder.setContentAndWait(``);
await context.newPage();
await recorder.waitForOutput('page1');
expect(recorder.output()).toContain('const page1 = await context.newPage();');
});
it('should contain close page', async ({ context, recorder }) => {
await recorder.setContentAndWait(``);
await context.newPage();
await recorder.page.close();
await recorder.waitForOutput('page.close();');
});
it('should not lead to an error if html gets clicked', async ({ context, recorder }) => {
await recorder.setContentAndWait('');
await 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', (test, { browserName }) => {
test.fixme(browserName === 'firefox', 'Hangs');
}, async ({ page, recorder }) => {
await recorder.setContentAndWait(`
<form>
<input type="file">
</form>
`);
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', (test, { browserName }) => {
test.fixme(browserName === 'firefox', 'Hangs');
}, async ({ page, recorder }) => {
await recorder.setContentAndWait(`
<form>
<input type="file" multiple>
</form>
`);
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', (test, { browserName }) => {
test.fixme(browserName === 'firefox', 'Hangs');
}, async ({ page, recorder }) => {
await recorder.setContentAndWait(`
<form>
<input type="file" multiple>
</form>
`);
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, headful}) => {
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(`
<a href="${httpServer.PREFIX}/download" download>Download</a>
`, 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(`
<button onclick="alert()">click me</button>
`);
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(`
<script>
let seqNum = 0;
function pushState() {
history.pushState({}, 'title', '${httpServer.PREFIX}/#seqNum=' + (++seqNum));
}
</script>`, 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(`<a href="about:blank?foo">link</a>`);
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, '<input id=name>');
const [popup2] = await Promise.all([
page.context().waitForEvent('page'),
page.evaluate(`window.open('about:blank')`)
]);
await recorder.setPageContentAndWait(popup2, '<input id=name>');
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(`
<button id=button>
<script>
button.addEventListener('mousedown', e => console.log(e.type));
button.addEventListener('mouseup', e => console.log(e.type));
button.addEventListener('click', e => console.log(e.type));
</script>
`);
const messages: any[] = [];
page.on('console', message => messages.push(message.text()));
await Promise.all([
page.click('button'),
recorder.waitForOutput('page.click')
]);
expect(messages).toEqual(['mousedown', 'mouseup', 'click']);
});
it('should update hover model on action', async ({ page, recorder }) => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
const [ models ] = await Promise.all([
recorder.waitForActionPerformed(),
page.click('input')
]);
expect(models.hovered).toBe('input[name="updated"]');
});
it('should update active model on action', (test, { browserName, headful }) => {
test.fixme(browserName === 'webkit' && !headful);
test.fixme(browserName === 'firefox' && !headful);
}, async ({ page, recorder }) => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name='updated'"></input>`);
const [ models ] = await Promise.all([
recorder.waitForActionPerformed(),
page.click('input')
]);
expect(models.active).toBe('input[name="updated"]');
});
it('should check input with chaning id', async ({ page, recorder }) => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="checkbox.name = 'updated'"></input>`);
await Promise.all([
recorder.waitForActionPerformed(),
page.click('input[id=checkbox]')
]);
});
it('should prefer frame name', async ({ page, recorder, server }) => {
await recorder.setContentAndWait(`
<iframe src='./frames/frame.html' name='one'></iframe>
<iframe src='./frames/frame.html' name='two'></iframe>
<iframe src='./frames/frame.html'></iframe>
`, server.EMPTY_PAGE, 4);
const frameOne = page.frame({ name: 'one' });
const frameTwo = page.frame({ name: 'two' });
const otherFrame = page.frames().find(f => f !== page.mainFrame() && !f.name());
await Promise.all([
recorder.waitForOutput('one'),
frameOne.click('div'),
]);
expect(recorder.output()).toContain(`
// Click text=Hi, I'm frame
await page.frame({
name: 'one'
}).click('text=Hi, I\\'m frame');`);
await Promise.all([
recorder.waitForOutput('two'),
frameTwo.click('div'),
]);
expect(recorder.output()).toContain(`
// Click text=Hi, I'm frame
await page.frame({
name: 'two'
}).click('text=Hi, I\\'m frame');`);
await Promise.all([
recorder.waitForOutput('url: \''),
otherFrame.click('div'),
]);
expect(recorder.output()).toContain(`
// Click text=Hi, I'm frame
await page.frame({
url: '${otherFrame.url()}'
}).click('text=Hi, I\\'m frame');`);
});
it('should record navigations after identical pushState', 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(`
<script>
function pushState() {
history.pushState({}, 'title', '${httpServer.PREFIX}');
}
</script>`, httpServer.PREFIX);
for (let i = 1; i < 3; ++i)
await page.evaluate('pushState()');
await page.goto(httpServer.PREFIX + '/page2.html');
await recorder.waitForOutput(`await page.goto('${httpServer.PREFIX}/page2.html');`);
});
});

View file

@ -22,17 +22,21 @@ const { it, expect } = folio;
const emptyHTML = new URL('file://' + path.join(__dirname, '..', 'assets', 'empty.html')).toString();
it('should print the correct imports and context options', async ({ runCLI }) => {
function capitalize(browserName: string): string {
return browserName[0].toUpperCase() + browserName.slice(1);
}
it('should print the correct imports and context options', async ({ browserName, runCLI }) => {
const cli = runCLI(['codegen', '--target=csharp', emptyHTML]);
const expectedResult = `await Playwright.InstallAsync();
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(headless: false);
await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync(headless: false);
var context = await browser.NewContextAsync();`;
await cli.waitFor(expectedResult).catch(e => e);
expect(cli.text()).toContain(expectedResult);
});
it('should print the correct context options for custom settings', async ({ runCLI }) => {
it('should print the correct context options for custom settings', async ({ browserName, runCLI }) => {
const cli = runCLI([
'--color-scheme=dark',
'--geolocation=37.819722,-122.478611',
@ -47,7 +51,7 @@ it('should print the correct context options for custom settings', async ({ runC
emptyHTML]);
const expectedResult = `await Playwright.InstallAsync();
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(
await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync(
headless: false,
proxy: new ProxySettings
{
@ -79,14 +83,13 @@ it('should print the correct context options when using a device', async ({ runC
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(headless: false);
var context = await browser.NewContextAsync(playwright.Devices["Pixel 2"]);`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
it('should print the correct context options when using a device and additional options', async ({ runCLI }) => {
const cli = runCLI([
'--device=Pixel 2',
'--device=iPhone 11',
'--color-scheme=dark',
'--geolocation=37.819722,-122.478611',
'--lang=es',
@ -100,13 +103,13 @@ it('should print the correct context options when using a device and additional
emptyHTML]);
const expectedResult = `await Playwright.InstallAsync();
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(
await using var browser = await playwright.Webkit.LaunchAsync(
headless: false,
proxy: new ProxySettings
{
Server = "http://myproxy:3128",
});
var context = await browser.NewContextAsync(new BrowserContextOptions(playwright.Devices["Pixel 2"])
var context = await browser.NewContextAsync(new BrowserContextOptions(playwright.Devices["iPhone 11"])
{
UserAgent = "hardkodemium",
Viewport = new ViewportSize
@ -129,21 +132,20 @@ var context = await browser.NewContextAsync(new BrowserContextOptions(playwright
expect(cli.text()).toContain(expectedResult);
});
it('should print load/save storageState', async ({ runCLI, testInfo }) => {
it('should print load/save storageState', async ({ browserName, runCLI, testInfo }) => {
const loadFileName = testInfo.outputPath('load.json');
const saveFileName = testInfo.outputPath('save.json');
await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8');
const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, 'codegen', '--target=csharp', emptyHTML]);
const expectedResult = `await Playwright.InstallAsync();
const expectedResult1 = `await Playwright.InstallAsync();
using var playwright = await Playwright.CreateAsync();
await using var browser = await playwright.Chromium.LaunchAsync(headless: false);
var context = await browser.NewContextAsync(storageState: "${loadFileName}");
// Open new page
var page = await context.NewPageAsync();
await using var browser = await playwright.${capitalize(browserName)}.LaunchAsync(headless: false);
var context = await browser.NewContextAsync(storageState: "${loadFileName}");`;
await cli.waitFor(expectedResult1);
const expectedResult2 = `
// ---------------------
await context.StorageStateAsync(path: "${saveFileName}");
`;
await cli.waitFor(expectedResult);
await cli.waitFor(expectedResult2);
});

View file

@ -22,12 +22,12 @@ const { it, expect } = folio;
const emptyHTML = new URL('file://' + path.join(__dirname, '..', 'assets', 'empty.html')).toString();
it('should print the correct imports and context options', async ({ runCLI }) => {
it('should print the correct imports and context options', async ({ browserName, runCLI }) => {
const cli = runCLI(['codegen', emptyHTML]);
const expectedResult = `const { chromium } = require('playwright');
const expectedResult = `const { ${browserName} } = require('playwright');
(async () => {
const browser = await chromium.launch({
const browser = await ${browserName}.launch({
headless: false
});
const context = await browser.newContext();`;
@ -35,12 +35,12 @@ it('should print the correct imports and context options', async ({ runCLI }) =>
expect(cli.text()).toContain(expectedResult);
});
it('should print the correct context options for custom settings', async ({ runCLI }) => {
it('should print the correct context options for custom settings', async ({ browserName, runCLI }) => {
const cli = runCLI(['--color-scheme=light', 'codegen', emptyHTML]);
const expectedResult = `const { chromium } = require('playwright');
const expectedResult = `const { ${browserName} } = require('playwright');
(async () => {
const browser = await chromium.launch({
const browser = await ${browserName}.launch({
headless: false
});
const context = await browser.newContext({
@ -67,30 +67,30 @@ it('should print the correct context options when using a device', async ({ runC
});
it('should print the correct context options when using a device and additional options', async ({ runCLI }) => {
const cli = runCLI(['--color-scheme=light', '--device=Pixel 2', 'codegen', emptyHTML]);
const expectedResult = `const { chromium, devices } = require('playwright');
const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', 'codegen', emptyHTML]);
const expectedResult = `const { webkit, devices } = require('playwright');
(async () => {
const browser = await chromium.launch({
const browser = await webkit.launch({
headless: false
});
const context = await browser.newContext({
...devices['Pixel 2'],
...devices['iPhone 11'],
colorScheme: 'light'
});`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
it('should save the codegen output to a file if specified', async ({ runCLI, testInfo }) => {
it('should save the codegen output to a file if specified', async ({ browserName, runCLI, testInfo }) => {
const tmpFile = testInfo.outputPath('script.js');
const cli = runCLI(['codegen', '--output', tmpFile, emptyHTML]);
await cli.exited;
const content = fs.readFileSync(tmpFile);
expect(content.toString()).toBe(`const { chromium } = require('playwright');
expect(content.toString()).toBe(`const { ${browserName} } = require('playwright');
(async () => {
const browser = await chromium.launch({
const browser = await ${browserName}.launch({
headless: false
});
const context = await browser.newContext();
@ -110,25 +110,27 @@ it('should save the codegen output to a file if specified', async ({ runCLI, tes
})();`);
});
it('should print load/save storageState', async ({ runCLI, testInfo }) => {
it('should print load/save storageState', async ({ browserName, runCLI, testInfo }) => {
const loadFileName = testInfo.outputPath('load.json');
const saveFileName = testInfo.outputPath('save.json');
await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8');
const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, 'codegen', emptyHTML]);
const expectedResult = `const { chromium } = require('playwright');
const expectedResult1 = `const { ${browserName} } = require('playwright');
(async () => {
const browser = await chromium.launch({
const browser = await ${browserName}.launch({
headless: false
});
const context = await browser.newContext({
storageState: '${loadFileName}'
});
});`;
await cli.waitFor(expectedResult1);
const expectedResult2 = `
// ---------------------
await context.storageState({ path: '${saveFileName}' });
await context.close();
await browser.close();
})();`;
await cli.waitFor(expectedResult);
await cli.waitFor(expectedResult2);
});

View file

@ -22,25 +22,25 @@ const { it, expect } = folio;
const emptyHTML = new URL('file://' + path.join(__dirname, '..', 'assets', 'empty.html')).toString();
it('should print the correct imports and context options', async ({ runCLI }) => {
it('should print the correct imports and context options', async ({ browserName, runCLI }) => {
const cli = runCLI(['codegen', '--target=python-async', emptyHTML]);
const expectedResult = `import asyncio
from playwright.async_api import async_playwright
async def run(playwright):
browser = await playwright.chromium.launch(headless=False)
browser = await playwright.${browserName}.launch(headless=False)
context = await browser.new_context()`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
it('should print the correct context options for custom settings', async ({ runCLI }) => {
it('should print the correct context options for custom settings', async ({ browserName, runCLI }) => {
const cli = runCLI(['--color-scheme=light', 'codegen', '--target=python-async', emptyHTML]);
const expectedResult = `import asyncio
from playwright.async_api import async_playwright
async def run(playwright):
browser = await playwright.chromium.launch(headless=False)
browser = await playwright.${browserName}.launch(headless=False)
context = await browser.new_context(color_scheme="light")`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
@ -59,18 +59,18 @@ async def run(playwright):
});
it('should print the correct context options when using a device and additional options', async ({ runCLI }) => {
const cli = runCLI(['--color-scheme=light', '--device=Pixel 2', 'codegen', '--target=python-async', emptyHTML]);
const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', 'codegen', '--target=python-async', emptyHTML]);
const expectedResult = `import asyncio
from playwright.async_api import async_playwright
async def run(playwright):
browser = await playwright.chromium.launch(headless=False)
context = await browser.new_context(**playwright.devices["Pixel 2"], color_scheme="light")`;
browser = await playwright.webkit.launch(headless=False)
context = await browser.new_context(**playwright.devices["iPhone 11"], color_scheme="light")`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
it('should save the codegen output to a file if specified', async ({ runCLI, testInfo }) => {
it('should save the codegen output to a file if specified', async ({ browserName, runCLI, testInfo }) => {
const tmpFile = testInfo.outputPath('script.js');
const cli = runCLI(['codegen', '--target=python-async', '--output', tmpFile, emptyHTML]);
await cli.exited;
@ -79,7 +79,7 @@ it('should save the codegen output to a file if specified', async ({ runCLI, tes
from playwright.async_api import async_playwright
async def run(playwright):
browser = await playwright.chromium.launch(headless=False)
browser = await playwright.${browserName}.launch(headless=False)
context = await browser.new_context()
# Open new page
@ -101,21 +101,20 @@ async def main():
asyncio.run(main())`);
});
it('should print load/save storage_state', async ({ runCLI, testInfo }) => {
it('should print load/save storage_state', async ({ browserName, runCLI, testInfo }) => {
const loadFileName = testInfo.outputPath('load.json');
const saveFileName = testInfo.outputPath('save.json');
await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8');
const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, 'codegen', '--target=python-async', emptyHTML]);
const expectedResult = `import asyncio
const expectedResult1 = `import asyncio
from playwright.async_api import async_playwright
async def run(playwright):
browser = await playwright.chromium.launch(headless=False)
context = await browser.new_context(storage_state="${loadFileName}")
# Open new page
page = await context.new_page()
browser = await playwright.${browserName}.launch(headless=False)
context = await browser.new_context(storage_state="${loadFileName}")`;
await cli.waitFor(expectedResult1);
const expectedResult2 = `
# ---------------------
await context.storage_state(path="${saveFileName}")
await context.close()
@ -125,5 +124,5 @@ async def main():
async with async_playwright() as playwright:
await run(playwright)
asyncio.run(main())`;
await cli.waitFor(expectedResult);
await cli.waitFor(expectedResult2);
});

View file

@ -22,23 +22,23 @@ const { it, expect } = folio;
const emptyHTML = new URL('file://' + path.join(__dirname, '..', 'assets', 'empty.html')).toString();
it('should print the correct imports and context options', async ({ runCLI }) => {
it('should print the correct imports and context options', async ({ runCLI, browserName }) => {
const cli = runCLI(['codegen', '--target=python', emptyHTML]);
const expectedResult = `from playwright.sync_api import sync_playwright
def run(playwright):
browser = playwright.chromium.launch(headless=False)
browser = playwright.${browserName}.launch(headless=False)
context = browser.new_context()`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
it('should print the correct context options for custom settings', async ({ runCLI }) => {
it('should print the correct context options for custom settings', async ({ runCLI, browserName }) => {
const cli = runCLI(['--color-scheme=light', 'codegen', '--target=python', emptyHTML]);
const expectedResult = `from playwright.sync_api import sync_playwright
def run(playwright):
browser = playwright.chromium.launch(headless=False)
browser = playwright.${browserName}.launch(headless=False)
context = browser.new_context(color_scheme="light")`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
@ -56,17 +56,17 @@ def run(playwright):
});
it('should print the correct context options when using a device and additional options', async ({ runCLI }) => {
const cli = runCLI(['--color-scheme=light', '--device=Pixel 2', 'codegen', '--target=python', emptyHTML]);
const cli = runCLI(['--color-scheme=light', '--device=iPhone 11', 'codegen', '--target=python', emptyHTML]);
const expectedResult = `from playwright.sync_api import sync_playwright
def run(playwright):
browser = playwright.chromium.launch(headless=False)
context = browser.new_context(**playwright.devices["Pixel 2"], color_scheme="light")`;
browser = playwright.webkit.launch(headless=False)
context = browser.new_context(**playwright.devices["iPhone 11"], color_scheme="light")`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
});
it('should save the codegen output to a file if specified', async ({ runCLI, testInfo }) => {
it('should save the codegen output to a file if specified', async ({ runCLI, browserName, testInfo }) => {
const tmpFile = testInfo.outputPath('script.js');
const cli = runCLI(['codegen', '--target=python', '--output', tmpFile, emptyHTML]);
await cli.exited;
@ -74,7 +74,7 @@ it('should save the codegen output to a file if specified', async ({ runCLI, tes
expect(content.toString()).toBe(`from playwright.sync_api import sync_playwright
def run(playwright):
browser = playwright.chromium.launch(headless=False)
browser = playwright.${browserName}.launch(headless=False)
context = browser.new_context()
# Open new page
@ -94,20 +94,19 @@ with sync_playwright() as playwright:
run(playwright)`);
});
it('should print load/save storage_state', async ({ runCLI, testInfo }) => {
it('should print load/save storage_state', async ({ runCLI, browserName, testInfo }) => {
const loadFileName = testInfo.outputPath('load.json');
const saveFileName = testInfo.outputPath('save.json');
await fs.promises.writeFile(loadFileName, JSON.stringify({ cookies: [], origins: [] }), 'utf8');
const cli = runCLI([`--load-storage=${loadFileName}`, `--save-storage=${saveFileName}`, 'codegen', '--target=python', emptyHTML]);
const expectedResult = `from playwright.sync_api import sync_playwright
const expectedResult1 = `from playwright.sync_api import sync_playwright
def run(playwright):
browser = playwright.chromium.launch(headless=False)
context = browser.new_context(storage_state="${loadFileName}")
# Open new page
page = context.new_page()
browser = playwright.${browserName}.launch(headless=False)
context = browser.new_context(storage_state="${loadFileName}")`;
await cli.waitFor(expectedResult1);
const expectedResult2 = `
# ---------------------
context.storage_state(path="${saveFileName}")
context.close()
@ -115,5 +114,5 @@ def run(playwright):
with sync_playwright() as playwright:
run(playwright)`;
await cli.waitFor(expectedResult);
await cli.waitFor(expectedResult2);
});

View file

@ -17,8 +17,8 @@
import * as http from 'http';
import path from 'path';
import { ChildProcess, spawn } from 'child_process';
import { folio as baseFolio } from '../fixtures';
import type { Page, BrowserType, Browser, BrowserContext } from '../..';
import { folio as baseFolio } from '../recorder.fixtures';
import type { BrowserType, Browser, Page } from '../..';
export { config } from 'folio';
type WorkerFixtures = {
@ -28,28 +28,19 @@ type WorkerFixtures = {
};
type TestFixtures = {
contextWrapper: { context: BrowserContext, output: WritableBuffer };
recorder: Recorder;
runCLI: (args: string[]) => CLIMock;
};
export const fixtures = baseFolio.extend<TestFixtures, WorkerFixtures>();
fixtures.contextWrapper.init(async ({ browser }, runTest) => {
const context = await browser.newContext() as BrowserContext;
const outputBuffer = new WritableBuffer();
(context as any)._stdout = outputBuffer;
await (context as any)._enableRecorder({ language: 'javascript', startRecording: true });
await runTest({ context, output: outputBuffer });
await context.close();
});
fixtures.recorder.init(async ({ contextWrapper }, runTest) => {
const page = await contextWrapper.context.newPage();
if (process.env.PWCONSOLE)
page.on('console', console.log);
await runTest(new Recorder(page, contextWrapper.output));
await page.close();
fixtures.recorder.init(async ({ page, recorderFrame }, runTest) => {
await (page.context() as any)._enableRecorder({ language: 'javascript', startRecording: true });
const recorderFrameInstance = await recorderFrame();
const recorder = new Recorder(page, recorderFrameInstance);
await recorderFrameInstance._page.context().exposeBinding('playwrightSourceEchoForTest', false,
(_: any, text: string) => recorder.setText(text));
await runTest(recorder);
});
fixtures.httpServer.init(async ({testWorkerIndex}, runTest) => {
@ -63,11 +54,6 @@ fixtures.httpServer.init(async ({testWorkerIndex}, runTest) => {
server.close();
}, { scope: 'worker' });
fixtures.page.override(async ({ recorder }, runTest) => {
await runTest(recorder.page);
});
function removeAnsiColors(input: string): string {
const pattern = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
@ -76,51 +62,19 @@ function removeAnsiColors(input: string): string {
return input.replace(new RegExp(pattern, 'g'), '');
}
class WritableBuffer {
_data: string;
private _callback: () => void;
_text: string;
constructor() {
this._data = '';
}
write(data: Buffer) {
if (!data)
return;
const chunk = data.toString('utf8');
this._data += chunk;
if (this._callback && chunk.includes(this._text))
this._callback();
}
_waitFor(text: string): Promise<void> {
if (this._data.includes(text))
return Promise.resolve();
this._text = text;
return new Promise(f => this._callback = f);
}
data() {
return this._data;
}
text() {
return removeAnsiColors(this.data());
}
}
class Recorder {
page: Page;
_output: WritableBuffer;
_highlightCallback: Function
_highlightInstalled: boolean
_actionReporterInstalled: boolean
_actionPerformedCallback: Function
recorderFrame: any;
private _text: string;
private _waiters = [];
constructor(page: Page, output: WritableBuffer) {
constructor(page: Page, recorderFrame: any) {
this.page = page;
this._output = output;
this.recorderFrame = recorderFrame;
this._highlightCallback = () => { };
this._highlightInstalled = false;
this._actionReporterInstalled = false;
@ -147,12 +101,20 @@ class Recorder {
]);
}
setText(text: string) {
this._text = text;
for (const waiter of this._waiters) {
if (text.includes(waiter.text))
waiter.fulfill();
}
}
async waitForOutput(text: string): Promise<void> {
await this._output._waitFor(text);
return new Promise(fulfill => this._waiters.push({ text, fulfill }));
}
output(): string {
return this._output.text();
return this._text;
}
async waitForHighlight(action: () => Promise<void>): Promise<string> {
@ -184,10 +146,10 @@ class Recorder {
}
}
fixtures.runCLI.init(async ({ }, runTest) => {
fixtures.runCLI.init(async ({ browserName }, runTest) => {
let cli: CLIMock;
const cliFactory = (args: string[]) => {
cli = new CLIMock(args);
cli = new CLIMock(browserName, args);
return cli;
};
await runTest(cliFactory);
@ -201,10 +163,11 @@ class CLIMock {
private waitForCallback: () => void;
exited: Promise<void>;
constructor(args: string[]) {
constructor(browserName, args: string[]) {
this.data = '';
this.process = spawn('node', [
path.join(__dirname, '..', '..', 'lib', 'cli', 'cli.js'),
`--browser=${browserName}`,
...args
], {
env: {

View file

@ -103,8 +103,8 @@ fixtures.browserOptions.override(async ({ browserName, headful, slowMo }, run) =
fixtures.playwright.override(async ({ browserName, testWorkerIndex, platform, mode }, run) => {
assert(platform); // Depend on platform to generate all tests.
const { coverage, uninstall } = installCoverageHooks(browserName);
require('../lib/utils/utils').setUnderTest();
if (mode === 'driver') {
require('../lib/utils/utils').setUnderTest();
const connection = new Connection();
const spawnedProcess = childProcess.fork(path.join(__dirname, '..', 'lib', 'cli', 'cli.js'), ['run-driver'], {
stdio: 'pipe',
@ -126,7 +126,6 @@ fixtures.playwright.override(async ({ browserName, testWorkerIndex, platform, mo
spawnedProcess.stderr.destroy();
await teardownCoverage();
} else if (mode === 'service') {
require('../lib/utils/utils').setUnderTest();
const port = 9407 + testWorkerIndex * 2;
const spawnedProcess = childProcess.fork(path.join(__dirname, '..', 'lib', 'service.js'), [String(port)], {
stdio: 'pipe'

View file

@ -14,37 +14,8 @@
* limitations under the License.
*/
import { folio } from './fixtures';
import { internalCallMetadata } from '../lib/server/instrumentation';
const extended = folio.extend<{
recorderFrame: () => Promise<any>,
recorderClick: (selector: string) => Promise<void>
}>();
extended.browserOptions.override(async ({browserOptions}, runTest) => {
await runTest({
...browserOptions,
headless: false,
});
});
extended.recorderFrame.init(async ({context, toImpl}, runTest) => {
await runTest(async () => {
while (!toImpl(context).recorderAppForTest)
await new Promise(f => setTimeout(f, 100));
return toImpl(context).recorderAppForTest._page.mainFrame();
});
});
extended.recorderClick.init(async ({ recorderFrame }, runTest) => {
await runTest(async (selector: string) => {
const frame = await recorderFrame();
frame.click(internalCallMetadata(), selector, {});
});
});
const {it, expect, describe} = extended.build();
import { folio } from './recorder.fixtures';
const { it, expect, describe} = folio;
describe('pause', (suite, { mode }) => {
suite.skip(mode !== 'default');

40
test/recorder.fixtures.ts Normal file
View file

@ -0,0 +1,40 @@
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* 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 { folio as baseFolio } from './fixtures';
import { internalCallMetadata } from '../lib/server/instrumentation';
const fixtures = baseFolio.extend<{
recorderFrame: () => Promise<any>,
recorderClick: (selector: string) => Promise<void>
}>();
fixtures.recorderFrame.init(async ({context, toImpl}, runTest) => {
await runTest(async () => {
while (!toImpl(context).recorderAppForTest)
await new Promise(f => setTimeout(f, 100));
return toImpl(context).recorderAppForTest._page.mainFrame();
});
});
fixtures.recorderClick.init(async ({ recorderFrame }, runTest) => {
await runTest(async (selector: string) => {
const frame = await recorderFrame();
await frame.click(internalCallMetadata(), selector, {});
});
});
export const folio = fixtures.build();

View file

@ -154,7 +154,7 @@ DEPS['src/service.ts'] = ['src/remote/'];
// CLI should only use client-side features.
DEPS['src/cli/'] = ['src/cli/**', 'src/client/**', 'src/install/**', 'src/generated/', 'src/server/injected/', 'src/debug/injected/', 'src/trace/**', 'src/utils/**'];
DEPS['src/server/supplements/recorder/recorderApp.ts'] = ['src/server/', 'src/server/chromium/'];
DEPS['src/server/supplements/recorder/recorderApp.ts'] = ['src/utils/', 'src/server/', 'src/server/chromium/'];
DEPS['src/utils/'] = ['src/common/'];
checkDeps().catch(e => {