chore(recorder): move recording output into the gui app (#5425)
This commit is contained in:
parent
a42c46b986
commit
449adfd3ae
1
index.js
1
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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)) };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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({}));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(/'/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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');`);
|
||||
});
|
||||
});
|
||||
337
test/cli/cli-codegen-2.spec.ts
Normal file
337
test/cli/cli-codegen-2.spec.ts
Normal 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');`);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
40
test/recorder.fixtures.ts
Normal 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();
|
||||
|
|
@ -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 => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue