mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: introduce debug toolbar (#5145)
This commit is contained in:
parent
894abbfe28
commit
01d6f83597
@ -30,9 +30,6 @@ import { Browser } from '../client/browser';
|
|||||||
import { Page } from '../client/page';
|
import { Page } from '../client/page';
|
||||||
import { BrowserType } from '../client/browserType';
|
import { BrowserType } from '../client/browserType';
|
||||||
import { BrowserContextOptions, LaunchOptions } from '../client/types';
|
import { BrowserContextOptions, LaunchOptions } from '../client/types';
|
||||||
import { RecorderOutput, RecorderSupplement } from '../client/supplements/recorderSupplement';
|
|
||||||
import { ConsoleApiSupplement } from '../client/supplements/consoleApiSupplement';
|
|
||||||
import { FileOutput, OutputMultiplexer, TerminalOutput } from '../client/supplements/recorderOutputs';
|
|
||||||
|
|
||||||
program
|
program
|
||||||
.version('Version ' + require('../../package.json').version)
|
.version('Version ' + require('../../package.json').version)
|
||||||
@ -318,7 +315,7 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi
|
|||||||
|
|
||||||
async function open(options: Options, url: string | undefined) {
|
async function open(options: Options, url: string | undefined) {
|
||||||
const { context } = await launchContext(options, false);
|
const { context } = await launchContext(options, false);
|
||||||
new ConsoleApiSupplement(context);
|
await context._enableConsoleApi();
|
||||||
await openPage(context, url);
|
await openPage(context, url);
|
||||||
if (process.env.PWCLI_EXIT_FOR_TEST)
|
if (process.env.PWCLI_EXIT_FOR_TEST)
|
||||||
await Promise.all(context.pages().map(p => p.close()));
|
await Promise.all(context.pages().map(p => p.close()));
|
||||||
@ -326,23 +323,9 @@ async function open(options: Options, url: string | undefined) {
|
|||||||
|
|
||||||
async function codegen(options: Options, url: string | undefined, language: string, outputFile?: string) {
|
async function codegen(options: Options, url: string | undefined, language: string, outputFile?: string) {
|
||||||
const { context, launchOptions, contextOptions } = await launchContext(options, false);
|
const { context, launchOptions, contextOptions } = await launchContext(options, false);
|
||||||
let highlighterType = language;
|
if (process.env.PWTRACE)
|
||||||
if (highlighterType === 'python-async')
|
contextOptions._traceDir = path.join(process.cwd(), '.trace');
|
||||||
highlighterType = 'python';
|
await context._enableRecorder(language, launchOptions, contextOptions, options.device, options.saveStorage, !!process.stdout.columns, outputFile ? path.resolve(outputFile) : undefined);
|
||||||
const outputs: RecorderOutput[] = [TerminalOutput.create(process.stdout, highlighterType)];
|
|
||||||
if (outputFile)
|
|
||||||
outputs.push(new FileOutput(outputFile));
|
|
||||||
const output = new OutputMultiplexer(outputs);
|
|
||||||
|
|
||||||
new ConsoleApiSupplement(context);
|
|
||||||
new RecorderSupplement(context,
|
|
||||||
language,
|
|
||||||
launchOptions,
|
|
||||||
contextOptions,
|
|
||||||
options.device,
|
|
||||||
options.saveStorage,
|
|
||||||
output);
|
|
||||||
|
|
||||||
await openPage(context, url);
|
await openPage(context, url);
|
||||||
if (process.env.PWCLI_EXIT_FOR_TEST)
|
if (process.env.PWCLI_EXIT_FOR_TEST)
|
||||||
await Promise.all(context.pages().map(p => p.close()));
|
await Promise.all(context.pages().map(p => p.close()));
|
||||||
|
@ -26,7 +26,7 @@ import { Browser } from './browser';
|
|||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { TimeoutSettings } from '../utils/timeoutSettings';
|
import { TimeoutSettings } from '../utils/timeoutSettings';
|
||||||
import { Waiter } from './waiter';
|
import { Waiter } from './waiter';
|
||||||
import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState } from './types';
|
import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types';
|
||||||
import { isUnderTest, headersObjectToArray, mkdirIfNeeded } from '../utils/utils';
|
import { isUnderTest, headersObjectToArray, mkdirIfNeeded } from '../utils/utils';
|
||||||
import { isSafeCloseError } from '../utils/errors';
|
import { isSafeCloseError } from '../utils/errors';
|
||||||
import * as api from '../../types/types';
|
import * as api from '../../types/types';
|
||||||
@ -44,6 +44,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
|||||||
_ownerPage: Page | undefined;
|
_ownerPage: Page | undefined;
|
||||||
private _closedPromise: Promise<void>;
|
private _closedPromise: Promise<void>;
|
||||||
_options: channels.BrowserNewContextParams = {};
|
_options: channels.BrowserNewContextParams = {};
|
||||||
|
private _stdout: NodeJS.WriteStream;
|
||||||
|
private _stderr: NodeJS.WriteStream;
|
||||||
|
|
||||||
static from(context: channels.BrowserContextChannel): BrowserContext {
|
static from(context: channels.BrowserContextChannel): BrowserContext {
|
||||||
return (context as any)._object;
|
return (context as any)._object;
|
||||||
@ -62,6 +64,10 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
|||||||
this._channel.on('close', () => this._onClose());
|
this._channel.on('close', () => this._onClose());
|
||||||
this._channel.on('page', ({page}) => this._onPage(Page.from(page)));
|
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._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._channel.on('stderr', ({ data }) => this._stderr.write(Buffer.from(data, 'base64')));
|
||||||
this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f));
|
this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,6 +259,35 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _pause() {
|
||||||
|
return this._wrapApiCall('browserContext.pause', async () => {
|
||||||
|
await this._channel.pause();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _enableConsoleApi() {
|
||||||
|
await this._channel.consoleSupplementExpose();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _enableRecorder(
|
||||||
|
language: string,
|
||||||
|
launchOptions?: LaunchOptions,
|
||||||
|
contextOptions?: BrowserContextOptions,
|
||||||
|
device?: string,
|
||||||
|
saveStorage?: string,
|
||||||
|
terminal?: boolean,
|
||||||
|
outputFile?: string) {
|
||||||
|
await this._channel.recorderSupplementEnable({
|
||||||
|
language,
|
||||||
|
launchOptions,
|
||||||
|
contextOptions,
|
||||||
|
device,
|
||||||
|
saveStorage,
|
||||||
|
terminal,
|
||||||
|
outputFile,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareBrowserContextOptions(options: BrowserContextOptions): Promise<channels.BrowserNewContextOptions> {
|
export async function prepareBrowserContextOptions(options: BrowserContextOptions): Promise<channels.BrowserNewContextOptions> {
|
||||||
|
@ -640,9 +640,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
|
|||||||
}
|
}
|
||||||
|
|
||||||
async _pause() {
|
async _pause() {
|
||||||
return this._wrapApiCall('page.pause', async () => {
|
await this.context()._pause();
|
||||||
await this._channel.pause();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _pdf(options: PDFOptions = {}): Promise<Buffer> {
|
async _pdf(options: PDFOptions = {}): Promise<Buffer> {
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { BrowserContext } from '../browserContext';
|
|
||||||
|
|
||||||
export class ConsoleApiSupplement {
|
|
||||||
constructor(context: BrowserContext) {
|
|
||||||
context._channel.consoleSupplementExpose().catch(e => {});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
import { BrowserContext } from '../browserContext';
|
|
||||||
import { BrowserContextOptions, LaunchOptions } from '../types';
|
|
||||||
|
|
||||||
export class RecorderSupplement {
|
|
||||||
constructor(context: BrowserContext,
|
|
||||||
language: string,
|
|
||||||
launchOptions: LaunchOptions,
|
|
||||||
contextOptions: BrowserContextOptions,
|
|
||||||
device: string | undefined,
|
|
||||||
saveStorage: string | undefined,
|
|
||||||
output: RecorderOutput) {
|
|
||||||
|
|
||||||
if (process.env.PWTRACE)
|
|
||||||
contextOptions._traceDir = path.join(process.cwd(), '.trace');
|
|
||||||
|
|
||||||
context._channel.on('recorderSupplementPrintLn', event => output.printLn(event.text));
|
|
||||||
context._channel.on('recorderSupplementPopLn', event => output.popLn(event.text));
|
|
||||||
context.on('close', () => output.flush());
|
|
||||||
context._channel.recorderSupplementEnable({
|
|
||||||
language,
|
|
||||||
launchOptions,
|
|
||||||
contextOptions,
|
|
||||||
device,
|
|
||||||
saveStorage,
|
|
||||||
}).catch(e => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RecorderOutput {
|
|
||||||
printLn(text: string): void;
|
|
||||||
popLn(text: string): void;
|
|
||||||
flush(): void;
|
|
||||||
}
|
|
@ -38,6 +38,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
|||||||
this._dispatchEvent('close');
|
this._dispatchEvent('close');
|
||||||
this._dispose();
|
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') {
|
if (context._browser._options.name === 'chromium') {
|
||||||
for (const page of (context as CRBrowserContext).backgroundPages())
|
for (const page of (context as CRBrowserContext).backgroundPages())
|
||||||
@ -133,11 +135,17 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
|||||||
}
|
}
|
||||||
|
|
||||||
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
|
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
|
||||||
const recorder = new RecorderSupplement(this._context, params, {
|
await RecorderSupplement.getOrCreate(this._context, 'codegen', params);
|
||||||
printLn: text => this._dispatchEvent('recorderSupplementPrintLn', { text }),
|
}
|
||||||
popLn: text => this._dispatchEvent('recorderSupplementPopLn', { text }),
|
|
||||||
|
async pause() {
|
||||||
|
if (!this._context._browser._options.headful)
|
||||||
|
return;
|
||||||
|
const recorder = await RecorderSupplement.getOrCreate(this._context, 'pause', {
|
||||||
|
language: 'javascript',
|
||||||
|
terminal: true
|
||||||
});
|
});
|
||||||
await recorder.install();
|
await recorder.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise<channels.BrowserContextCrNewCDPSessionResult> {
|
async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise<channels.BrowserContextCrNewCDPSessionResult> {
|
||||||
|
@ -237,10 +237,6 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
|
|||||||
return { entries: await coverage.stopCSSCoverage() };
|
return { entries: await coverage.stopCSSCoverage() };
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause() {
|
|
||||||
await this._page.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
_onFrameAttached(frame: Frame) {
|
_onFrameAttached(frame: Frame) {
|
||||||
this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this._scope, frame) });
|
this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this._scope, frame) });
|
||||||
}
|
}
|
||||||
|
@ -533,10 +533,10 @@ export interface BrowserContextChannel extends Channel {
|
|||||||
on(event: 'close', callback: (params: BrowserContextCloseEvent) => void): this;
|
on(event: 'close', callback: (params: BrowserContextCloseEvent) => void): this;
|
||||||
on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this;
|
on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this;
|
||||||
on(event: 'route', callback: (params: BrowserContextRouteEvent) => 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: 'crBackgroundPage', callback: (params: BrowserContextCrBackgroundPageEvent) => void): this;
|
||||||
on(event: 'crServiceWorker', callback: (params: BrowserContextCrServiceWorkerEvent) => void): this;
|
on(event: 'crServiceWorker', callback: (params: BrowserContextCrServiceWorkerEvent) => void): this;
|
||||||
on(event: 'recorderSupplementPrintLn', callback: (params: BrowserContextRecorderSupplementPrintLnEvent) => void): this;
|
|
||||||
on(event: 'recorderSupplementPopLn', callback: (params: BrowserContextRecorderSupplementPopLnEvent) => void): this;
|
|
||||||
addCookies(params: BrowserContextAddCookiesParams, metadata?: Metadata): Promise<BrowserContextAddCookiesResult>;
|
addCookies(params: BrowserContextAddCookiesParams, metadata?: Metadata): Promise<BrowserContextAddCookiesResult>;
|
||||||
addInitScript(params: BrowserContextAddInitScriptParams, metadata?: Metadata): Promise<BrowserContextAddInitScriptResult>;
|
addInitScript(params: BrowserContextAddInitScriptParams, metadata?: Metadata): Promise<BrowserContextAddInitScriptResult>;
|
||||||
clearCookies(params?: BrowserContextClearCookiesParams, metadata?: Metadata): Promise<BrowserContextClearCookiesResult>;
|
clearCookies(params?: BrowserContextClearCookiesParams, metadata?: Metadata): Promise<BrowserContextClearCookiesResult>;
|
||||||
@ -555,6 +555,7 @@ export interface BrowserContextChannel extends Channel {
|
|||||||
setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise<BrowserContextSetOfflineResult>;
|
setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise<BrowserContextSetOfflineResult>;
|
||||||
storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise<BrowserContextStorageStateResult>;
|
storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise<BrowserContextStorageStateResult>;
|
||||||
consoleSupplementExpose(params?: BrowserContextConsoleSupplementExposeParams, metadata?: Metadata): Promise<BrowserContextConsoleSupplementExposeResult>;
|
consoleSupplementExpose(params?: BrowserContextConsoleSupplementExposeParams, metadata?: Metadata): Promise<BrowserContextConsoleSupplementExposeResult>;
|
||||||
|
pause(params?: BrowserContextPauseParams, metadata?: Metadata): Promise<BrowserContextPauseResult>;
|
||||||
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise<BrowserContextRecorderSupplementEnableResult>;
|
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise<BrowserContextRecorderSupplementEnableResult>;
|
||||||
crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise<BrowserContextCrNewCDPSessionResult>;
|
crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise<BrowserContextCrNewCDPSessionResult>;
|
||||||
}
|
}
|
||||||
@ -569,18 +570,18 @@ export type BrowserContextRouteEvent = {
|
|||||||
route: RouteChannel,
|
route: RouteChannel,
|
||||||
request: RequestChannel,
|
request: RequestChannel,
|
||||||
};
|
};
|
||||||
|
export type BrowserContextStdoutEvent = {
|
||||||
|
data: Binary,
|
||||||
|
};
|
||||||
|
export type BrowserContextStderrEvent = {
|
||||||
|
data: Binary,
|
||||||
|
};
|
||||||
export type BrowserContextCrBackgroundPageEvent = {
|
export type BrowserContextCrBackgroundPageEvent = {
|
||||||
page: PageChannel,
|
page: PageChannel,
|
||||||
};
|
};
|
||||||
export type BrowserContextCrServiceWorkerEvent = {
|
export type BrowserContextCrServiceWorkerEvent = {
|
||||||
worker: WorkerChannel,
|
worker: WorkerChannel,
|
||||||
};
|
};
|
||||||
export type BrowserContextRecorderSupplementPrintLnEvent = {
|
|
||||||
text: string,
|
|
||||||
};
|
|
||||||
export type BrowserContextRecorderSupplementPopLnEvent = {
|
|
||||||
text: string,
|
|
||||||
};
|
|
||||||
export type BrowserContextAddCookiesParams = {
|
export type BrowserContextAddCookiesParams = {
|
||||||
cookies: SetNetworkCookie[],
|
cookies: SetNetworkCookie[],
|
||||||
};
|
};
|
||||||
@ -706,16 +707,25 @@ export type BrowserContextStorageStateResult = {
|
|||||||
export type BrowserContextConsoleSupplementExposeParams = {};
|
export type BrowserContextConsoleSupplementExposeParams = {};
|
||||||
export type BrowserContextConsoleSupplementExposeOptions = {};
|
export type BrowserContextConsoleSupplementExposeOptions = {};
|
||||||
export type BrowserContextConsoleSupplementExposeResult = void;
|
export type BrowserContextConsoleSupplementExposeResult = void;
|
||||||
|
export type BrowserContextPauseParams = {};
|
||||||
|
export type BrowserContextPauseOptions = {};
|
||||||
|
export type BrowserContextPauseResult = void;
|
||||||
export type BrowserContextRecorderSupplementEnableParams = {
|
export type BrowserContextRecorderSupplementEnableParams = {
|
||||||
language: string,
|
language: string,
|
||||||
launchOptions: any,
|
launchOptions?: any,
|
||||||
contextOptions: any,
|
contextOptions?: any,
|
||||||
device?: string,
|
device?: string,
|
||||||
saveStorage?: string,
|
saveStorage?: string,
|
||||||
|
terminal?: boolean,
|
||||||
|
outputFile?: string,
|
||||||
};
|
};
|
||||||
export type BrowserContextRecorderSupplementEnableOptions = {
|
export type BrowserContextRecorderSupplementEnableOptions = {
|
||||||
|
launchOptions?: any,
|
||||||
|
contextOptions?: any,
|
||||||
device?: string,
|
device?: string,
|
||||||
saveStorage?: string,
|
saveStorage?: string,
|
||||||
|
terminal?: boolean,
|
||||||
|
outputFile?: string,
|
||||||
};
|
};
|
||||||
export type BrowserContextRecorderSupplementEnableResult = void;
|
export type BrowserContextRecorderSupplementEnableResult = void;
|
||||||
export type BrowserContextCrNewCDPSessionParams = {
|
export type BrowserContextCrNewCDPSessionParams = {
|
||||||
@ -786,7 +796,6 @@ export interface PageChannel extends Channel {
|
|||||||
mouseClick(params: PageMouseClickParams, metadata?: Metadata): Promise<PageMouseClickResult>;
|
mouseClick(params: PageMouseClickParams, metadata?: Metadata): Promise<PageMouseClickResult>;
|
||||||
touchscreenTap(params: PageTouchscreenTapParams, metadata?: Metadata): Promise<PageTouchscreenTapResult>;
|
touchscreenTap(params: PageTouchscreenTapParams, metadata?: Metadata): Promise<PageTouchscreenTapResult>;
|
||||||
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: Metadata): Promise<PageAccessibilitySnapshotResult>;
|
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: Metadata): Promise<PageAccessibilitySnapshotResult>;
|
||||||
pause(params?: PagePauseParams, metadata?: Metadata): Promise<PagePauseResult>;
|
|
||||||
pdf(params: PagePdfParams, metadata?: Metadata): Promise<PagePdfResult>;
|
pdf(params: PagePdfParams, metadata?: Metadata): Promise<PagePdfResult>;
|
||||||
crStartJSCoverage(params: PageCrStartJSCoverageParams, metadata?: Metadata): Promise<PageCrStartJSCoverageResult>;
|
crStartJSCoverage(params: PageCrStartJSCoverageParams, metadata?: Metadata): Promise<PageCrStartJSCoverageResult>;
|
||||||
crStopJSCoverage(params?: PageCrStopJSCoverageParams, metadata?: Metadata): Promise<PageCrStopJSCoverageResult>;
|
crStopJSCoverage(params?: PageCrStopJSCoverageParams, metadata?: Metadata): Promise<PageCrStopJSCoverageResult>;
|
||||||
@ -1083,9 +1092,6 @@ export type PageAccessibilitySnapshotOptions = {
|
|||||||
export type PageAccessibilitySnapshotResult = {
|
export type PageAccessibilitySnapshotResult = {
|
||||||
rootAXNode?: AXNode,
|
rootAXNode?: AXNode,
|
||||||
};
|
};
|
||||||
export type PagePauseParams = {};
|
|
||||||
export type PagePauseOptions = {};
|
|
||||||
export type PagePauseResult = void;
|
|
||||||
export type PagePdfParams = {
|
export type PagePdfParams = {
|
||||||
scale?: number,
|
scale?: number,
|
||||||
displayHeaderFooter?: boolean,
|
displayHeaderFooter?: boolean,
|
||||||
|
@ -602,14 +602,19 @@ BrowserContext:
|
|||||||
consoleSupplementExpose:
|
consoleSupplementExpose:
|
||||||
experimental: True
|
experimental: True
|
||||||
|
|
||||||
|
pause:
|
||||||
|
experimental: True
|
||||||
|
|
||||||
recorderSupplementEnable:
|
recorderSupplementEnable:
|
||||||
experimental: True
|
experimental: True
|
||||||
parameters:
|
parameters:
|
||||||
language: string
|
language: string
|
||||||
launchOptions: json
|
launchOptions: json?
|
||||||
contextOptions: json
|
contextOptions: json?
|
||||||
device: string?
|
device: string?
|
||||||
saveStorage: string?
|
saveStorage: string?
|
||||||
|
terminal: boolean?
|
||||||
|
outputFile: string?
|
||||||
|
|
||||||
crNewCDPSession:
|
crNewCDPSession:
|
||||||
parameters:
|
parameters:
|
||||||
@ -634,6 +639,14 @@ BrowserContext:
|
|||||||
route: Route
|
route: Route
|
||||||
request: Request
|
request: Request
|
||||||
|
|
||||||
|
stdout:
|
||||||
|
parameters:
|
||||||
|
data: binary
|
||||||
|
|
||||||
|
stderr:
|
||||||
|
parameters:
|
||||||
|
data: binary
|
||||||
|
|
||||||
crBackgroundPage:
|
crBackgroundPage:
|
||||||
parameters:
|
parameters:
|
||||||
page: Page
|
page: Page
|
||||||
@ -642,14 +655,6 @@ BrowserContext:
|
|||||||
parameters:
|
parameters:
|
||||||
worker: Worker
|
worker: Worker
|
||||||
|
|
||||||
recorderSupplementPrintLn:
|
|
||||||
parameters:
|
|
||||||
text: string
|
|
||||||
|
|
||||||
recorderSupplementPopLn:
|
|
||||||
parameters:
|
|
||||||
text: string
|
|
||||||
|
|
||||||
Page:
|
Page:
|
||||||
type: interface
|
type: interface
|
||||||
|
|
||||||
@ -854,8 +859,6 @@ Page:
|
|||||||
returns:
|
returns:
|
||||||
rootAXNode: AXNode?
|
rootAXNode: AXNode?
|
||||||
|
|
||||||
pause:
|
|
||||||
|
|
||||||
pdf:
|
pdf:
|
||||||
parameters:
|
parameters:
|
||||||
scale: number?
|
scale: number?
|
||||||
|
@ -336,12 +336,15 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||||||
});
|
});
|
||||||
scheme.BrowserContextStorageStateParams = tOptional(tObject({}));
|
scheme.BrowserContextStorageStateParams = tOptional(tObject({}));
|
||||||
scheme.BrowserContextConsoleSupplementExposeParams = tOptional(tObject({}));
|
scheme.BrowserContextConsoleSupplementExposeParams = tOptional(tObject({}));
|
||||||
|
scheme.BrowserContextPauseParams = tOptional(tObject({}));
|
||||||
scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
||||||
language: tString,
|
language: tString,
|
||||||
launchOptions: tAny,
|
launchOptions: tOptional(tAny),
|
||||||
contextOptions: tAny,
|
contextOptions: tOptional(tAny),
|
||||||
device: tOptional(tString),
|
device: tOptional(tString),
|
||||||
saveStorage: tOptional(tString),
|
saveStorage: tOptional(tString),
|
||||||
|
terminal: tOptional(tBoolean),
|
||||||
|
outputFile: tOptional(tString),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextCrNewCDPSessionParams = tObject({
|
scheme.BrowserContextCrNewCDPSessionParams = tObject({
|
||||||
page: tChannel('Page'),
|
page: tChannel('Page'),
|
||||||
@ -447,7 +450,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||||||
interestingOnly: tOptional(tBoolean),
|
interestingOnly: tOptional(tBoolean),
|
||||||
root: tOptional(tChannel('ElementHandle')),
|
root: tOptional(tChannel('ElementHandle')),
|
||||||
});
|
});
|
||||||
scheme.PagePauseParams = tOptional(tObject({}));
|
|
||||||
scheme.PagePdfParams = tObject({
|
scheme.PagePdfParams = tObject({
|
||||||
scale: tOptional(tNumber),
|
scale: tOptional(tNumber),
|
||||||
displayHeaderFooter: tOptional(tBoolean),
|
displayHeaderFooter: tOptional(tBoolean),
|
||||||
|
@ -93,6 +93,9 @@ export abstract class BrowserContext extends EventEmitter {
|
|||||||
Close: 'close',
|
Close: 'close',
|
||||||
Page: 'page',
|
Page: 'page',
|
||||||
VideoStarted: 'videostarted',
|
VideoStarted: 'videostarted',
|
||||||
|
BeforeClose: 'beforeclose',
|
||||||
|
StdOut: 'stdout',
|
||||||
|
StdErr: 'stderr',
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly _timeoutSettings = new TimeoutSettings();
|
readonly _timeoutSettings = new TimeoutSettings();
|
||||||
@ -280,6 +283,7 @@ export abstract class BrowserContext extends EventEmitter {
|
|||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
if (this._closedStatus === 'open') {
|
if (this._closedStatus === 'open') {
|
||||||
|
this.emit(BrowserContext.Events.BeforeClose);
|
||||||
this._closedStatus = 'closing';
|
this._closedStatus = 'closing';
|
||||||
|
|
||||||
for (const listener of contextListeners)
|
for (const listener of contextListeners)
|
||||||
|
@ -32,10 +32,7 @@ type ContextData = {
|
|||||||
contextPromise: Promise<dom.FrameExecutionContext>;
|
contextPromise: Promise<dom.FrameExecutionContext>;
|
||||||
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
|
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
|
||||||
context: dom.FrameExecutionContext | null;
|
context: dom.FrameExecutionContext | null;
|
||||||
rerunnableTasks: Set<{
|
rerunnableTasks: Set<RerunnableTask>;
|
||||||
rerun(context: dom.FrameExecutionContext): Promise<void>;
|
|
||||||
terminate(error: Error): void;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type DocumentInfo = {
|
type DocumentInfo = {
|
||||||
@ -1049,24 +1046,6 @@ export class Frame extends EventEmitter {
|
|||||||
this._parentFrame = null;
|
this._parentFrame = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateSurvivingNavigations<T>(callback: (context: dom.FrameExecutionContext) => Promise<T>, world: types.World) {
|
|
||||||
return new Promise<T>((resolve, terminate) => {
|
|
||||||
const data = this._contextData.get(world)!;
|
|
||||||
const task = {
|
|
||||||
terminate,
|
|
||||||
async rerun(context: dom.FrameExecutionContext) {
|
|
||||||
try {
|
|
||||||
resolve(await callback(context));
|
|
||||||
data.rerunnableTasks.delete(task);
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
data.rerunnableTasks.add(task);
|
|
||||||
if (data.context)
|
|
||||||
task.rerun(data.context);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _scheduleRerunnableTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<T> {
|
private _scheduleRerunnableTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<T> {
|
||||||
const data = this._contextData.get(world)!;
|
const data = this._contextData.get(world)!;
|
||||||
const rerunnableTask = new RerunnableTask(data, progress, task, true /* returnByValue */);
|
const rerunnableTask = new RerunnableTask(data, progress, task, true /* returnByValue */);
|
||||||
|
@ -492,31 +492,6 @@ export class Page extends EventEmitter {
|
|||||||
const identifier = PageBinding.identifier(name, world);
|
const identifier = PageBinding.identifier(name, world);
|
||||||
return this._pageBindings.get(identifier) || this._browserContext._pageBindings.get(identifier);
|
return this._pageBindings.get(identifier) || this._browserContext._pageBindings.get(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause() {
|
|
||||||
if (!this._browserContext._browser._options.headful)
|
|
||||||
throw new Error('Cannot pause in headless mode.');
|
|
||||||
await this.mainFrame().evaluateSurvivingNavigations(async context => {
|
|
||||||
await context.evaluateInternal(async () => {
|
|
||||||
const element = document.createElement('playwright-resume');
|
|
||||||
element.style.position = 'absolute';
|
|
||||||
element.style.top = '10px';
|
|
||||||
element.style.left = '10px';
|
|
||||||
element.style.zIndex = '2147483646';
|
|
||||||
element.style.opacity = '0.9';
|
|
||||||
element.setAttribute('role', 'button');
|
|
||||||
element.tabIndex = 0;
|
|
||||||
element.style.fontSize = '50px';
|
|
||||||
element.textContent = '▶️';
|
|
||||||
element.title = 'Resume script';
|
|
||||||
document.body.appendChild(element);
|
|
||||||
await new Promise(x => {
|
|
||||||
element.onclick = x;
|
|
||||||
});
|
|
||||||
element.remove();
|
|
||||||
});
|
|
||||||
}, 'utility');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Worker extends EventEmitter {
|
export class Worker extends EventEmitter {
|
||||||
|
@ -24,6 +24,9 @@ declare global {
|
|||||||
playwrightRecorderPerformAction: (action: actions.Action) => Promise<void>;
|
playwrightRecorderPerformAction: (action: actions.Action) => Promise<void>;
|
||||||
playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
|
playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
|
||||||
playwrightRecorderCommitAction: () => Promise<void>;
|
playwrightRecorderCommitAction: () => Promise<void>;
|
||||||
|
playwrightRecorderState: () => Promise<{ state: any, paused: boolean, tool: 'codegen' | 'pause' }>;
|
||||||
|
playwrightRecorderSetState: (state: any) => Promise<void>;
|
||||||
|
playwrightRecorderResume: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,11 +40,19 @@ export class Recorder {
|
|||||||
private _innerGlassPaneElement: HTMLElement;
|
private _innerGlassPaneElement: HTMLElement;
|
||||||
private _highlightElements: HTMLElement[] = [];
|
private _highlightElements: HTMLElement[] = [];
|
||||||
private _tooltipElement: HTMLElement;
|
private _tooltipElement: HTMLElement;
|
||||||
private _listeners: RegisteredListener[] = [];
|
private _listeners: (() => void)[] = [];
|
||||||
private _hoveredModel: HighlightModel | null = null;
|
private _hoveredModel: HighlightModel | null = null;
|
||||||
private _hoveredElement: HTMLElement | null = null;
|
private _hoveredElement: HTMLElement | null = null;
|
||||||
private _activeModel: HighlightModel | null = null;
|
private _activeModel: HighlightModel | null = null;
|
||||||
private _expectProgrammaticKeyUp = false;
|
private _expectProgrammaticKeyUp = false;
|
||||||
|
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
||||||
|
private _toolbarElement: HTMLElement;
|
||||||
|
private _inspectElement: HTMLElement;
|
||||||
|
private _recordElement: HTMLElement;
|
||||||
|
private _resumeElement: HTMLElement;
|
||||||
|
private _mode: 'inspecting' | 'recording' | 'none' = 'none';
|
||||||
|
private _tool: 'codegen' | 'pause' = 'pause';
|
||||||
|
private _paused = false;
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript) {
|
constructor(injectedScript: InjectedScript) {
|
||||||
this._injectedScript = injectedScript;
|
this._injectedScript = injectedScript;
|
||||||
@ -53,7 +64,7 @@ export class Recorder {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 2147483647;
|
z-index: 2147483646;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
">
|
">
|
||||||
@ -96,9 +107,106 @@ export class Recorder {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
this._toolbarElement = html`
|
||||||
|
<x-pw-toolbar style="
|
||||||
|
position: fixed;
|
||||||
|
top: 100px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 2147483647;
|
||||||
|
background-color: #ffffffe6;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 22px;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.1) 0px 0.25em 0.5em;
|
||||||
|
flex-direction: column;"
|
||||||
|
></x-pw-toolbar>`;
|
||||||
|
|
||||||
|
this._inspectElement = html`
|
||||||
|
<x-pw-button tabIndex=0>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46 4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/></svg>
|
||||||
|
</x-pw-button>`;
|
||||||
|
this._recordElement = html`
|
||||||
|
<x-pw-button class="record" tabIndex=0>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M24 24H0V0h24v24z" fill="none"/><circle cx="12" cy="12" r="8"/></svg>
|
||||||
|
</x-pw-button>`;
|
||||||
|
this._resumeElement = html`
|
||||||
|
<x-pw-button tabIndex=0 class="playwright-resume">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></svg>
|
||||||
|
</x-pw-button>`;
|
||||||
|
|
||||||
|
this._populateToolbar();
|
||||||
|
this._pollRecorderMode();
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
this._refreshListenersIfNeeded();
|
this._refreshListenersIfNeeded();
|
||||||
}, 100);
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _populateToolbar() {
|
||||||
|
const toolbarShadow = this._toolbarElement.attachShadow({ mode: 'open' });
|
||||||
|
toolbarShadow.appendChild(html`
|
||||||
|
<style>
|
||||||
|
x-pw-button {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
border-radius: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 2px;
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
x-pw-button.logo {
|
||||||
|
cursor: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
x-pw-button.toggled {
|
||||||
|
fill: #468fd2;
|
||||||
|
}
|
||||||
|
x-pw-button:hover:not(.logo):not(.disabled) {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
x-pw-button.record.toggled {
|
||||||
|
fill: red;
|
||||||
|
}
|
||||||
|
x-pw-button.disabled {
|
||||||
|
fill: #777777 !important;
|
||||||
|
cursor: inherit;
|
||||||
|
}
|
||||||
|
x-pw-button.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>`);
|
||||||
|
|
||||||
|
const iconElement = html`<x-pw-button class="logo" tabIndex=0 style="background-size: 32px 32px;"></x-pw-button>`;
|
||||||
|
iconElement.style.backgroundImage = `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTM2IDIyMmMtMTIgMy0yMSAxMC0yNiAxNiA1LTUgMTItOSAyMi0xMiAxMC0yIDE4LTIgMjUtMXYtNmMtNiAwLTEzIDAtMjEgM3ptLTI3LTQ2bC00OCAxMiAzIDMgNDAtMTBzMCA3LTUgMTRjOS03IDEwLTE5IDEwLTE5em00MCAxMTJDODIgMzA2IDQ2IDIyOCAzNSAxODhhMjI3IDIyNyAwIDAxLTctNDVjLTQgMS02IDItNSA4IDAgOSAyIDIzIDcgNDIgMTEgNDAgNDcgMTE4IDExNCAxMDAgMTUtNCAyNi0xMSAzNC0yMC03IDctMTcgMTItMjkgMTV6bTEzLTE2MHY1aDI2bC0yLTVoLTI0eiIgZmlsbD0iIzJENDU1MiIvPjxwYXRoIGQ9Ik0xOTQgMTY4YzEyIDMgMTggMTEgMjEgMTlsMTQgM3MtMi0yNS0yNS0zMmMtMjItNi0zNiAxMi0zNyAxNCA2LTQgMTUtOCAyNy00em0xMDUgMTljLTIxLTYtMzUgMTItMzYgMTQgNi00IDE1LTggMjctNSAxMiA0IDE4IDEyIDIxIDE5bDE0IDRzLTItMjYtMjYtMzJ6bS0xMyA2OGwtMTEwLTMxczEgNiA2IDE0bDkzIDI2IDExLTl6bS03NiA2NmMtODctMjMtNzctMTM0LTYzLTE4NyA2LTIyIDEyLTM4IDE3LTQ5LTMgMC01IDEtOCA2LTUgMTEtMTIgMjgtMTggNTItMTQgNTMtMjUgMTY0IDYyIDE4OCA0MSAxMSA3My02IDk3LTMyYTkwIDkwIDAgMDEtODcgMjJ6IiBmaWxsPSIjMkQ0NTUyIi8+PHBhdGggZD0iTTE2MiAyNjJ2LTIybC02MyAxOHM1LTI3IDM3LTM2YzEwLTMgMTktMyAyNi0ydi05MmgzMWwtMTAtMjRjLTQtOS05LTMtMTkgNi04IDYtMjcgMTktNTUgMjctMjkgOC01MiA2LTYxIDQtMTQtMi0yMS01LTIwIDUgMCA5IDIgMjMgNyA0MiAxMSA0MCA0NyAxMTggMTE0IDEwMCAxOC00IDMwLTE0IDM5LTI2aC0yNnpNNjEgMTg4bDQ4LTEycy0xIDE4LTE5IDIzLTI5LTExLTI5LTExeiIgZmlsbD0iI0UyNTc0QyIvPjxwYXRoIGQ9Ik0zNDIgMTI5Yy0xMyAyLTQzIDUtNzktNS0zNy0xMC02Mi0yNy03MS0zNS0xNC0xMi0yMC0yMC0yNi04LTUgMTEtMTIgMjktMTkgNTMtMTQgNTMtMjQgMTY0IDYzIDE4N3MxMzQtNzggMTQ4LTEzMWM2LTI0IDktNDIgMTAtNTQgMS0xNC05LTEwLTI2LTd6bS0xNzYgNDRzMTQtMjIgMzgtMTVjMjMgNyAyNSAzMiAyNSAzMmwtNjMtMTd6bTU3IDk2Yy00MS0xMi00Ny00NS00Ny00NWwxMTAgMzFzLTIyIDI2LTYzIDE0em0zOS02OHMxNC0yMSAzNy0xNGMyNCA2IDI2IDMyIDI2IDMybC02My0xOHoiIGZpbGw9IiMyRUFEMzMiLz48cGF0aCBkPSJNMTQwIDI0NmwtNDEgMTJzNS0yNiAzNS0zNmwtMjMtODYtMiAxYy0yOSA4LTUyIDYtNjEgNC0xNC0yLTIxLTUtMjAgNSAwIDkgMiAyMyA3IDQyIDExIDQwIDQ3IDExOCAxMTQgMTAwaDJsLTExLTQyem0tNzktNThsNDgtMTJzLTEgMTgtMTkgMjMtMjktMTEtMjktMTF6IiBmaWxsPSIjRDY1MzQ4Ii8+PHBhdGggZD0iTTIyNSAyNjloLTJjLTQxLTEyLTQ3LTQ1LTQ3LTQ1bDU3IDE2IDMwLTExNmMtMzctMTAtNjItMjctNzEtMzUtMTQtMTItMjAtMjAtMjYtOC01IDExLTEyIDI5LTE5IDUzLTE0IDUzLTI0IDE2NCA2MyAxODdsMiAxIDEzLTUzem0tNTktOTZzMTQtMjIgMzgtMTVjMjMgNyAyNSAzMiAyNSAzMmwtNjMtMTd6IiBmaWxsPSIjMUQ4RDIyIi8+PHBhdGggZD0iTTE0MiAyNDVsLTExIDRjMyAxNCA3IDI4IDE0IDQwbDQtMSA5LTNjLTgtMTItMTMtMjUtMTYtNDB6bS00LTEwMmMtNiAyMS0xMSA1MS0xMCA4MWw4LTIgMi0xYTI3MyAyNzMgMCAwMTE0LTEwM2wtOCA1LTYgMjB6IiBmaWxsPSIjQzA0QjQxIi8+PC9zdmc+')`;
|
||||||
|
toolbarShadow.appendChild(iconElement);
|
||||||
|
toolbarShadow.appendChild(this._inspectElement);
|
||||||
|
toolbarShadow.appendChild(this._recordElement);
|
||||||
|
toolbarShadow.appendChild(this._resumeElement);
|
||||||
|
|
||||||
|
this._inspectElement.addEventListener('click', () => {
|
||||||
|
if (this._inspectElement.classList.contains('disabled'))
|
||||||
|
return;
|
||||||
|
this._inspectElement.classList.toggle('toggled');
|
||||||
|
this._setMode(this._inspectElement.classList.contains('toggled') ? 'inspecting' : 'none');
|
||||||
|
});
|
||||||
|
this._recordElement.addEventListener('click', () => {
|
||||||
|
if (this._recordElement.classList.contains('disabled'))
|
||||||
|
return;
|
||||||
|
this._recordElement.classList.toggle('toggled');
|
||||||
|
this._setMode(this._recordElement.classList.contains('toggled') ? 'recording' : 'none');
|
||||||
|
});
|
||||||
|
this._resumeElement.addEventListener('click', () => {
|
||||||
|
if (!this._resumeElement.classList.contains('disabled')) {
|
||||||
|
this._setMode('none');
|
||||||
|
window.playwrightRecorderResume().catch(e => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _refreshListenersIfNeeded() {
|
private _refreshListenersIfNeeded() {
|
||||||
@ -122,10 +230,47 @@ export class Recorder {
|
|||||||
}, true),
|
}, true),
|
||||||
];
|
];
|
||||||
document.documentElement.appendChild(this._outerGlassPaneElement);
|
document.documentElement.appendChild(this._outerGlassPaneElement);
|
||||||
|
document.documentElement.appendChild(this._toolbarElement);
|
||||||
if ((window as any)._recorderScriptReadyForTest)
|
if ((window as any)._recorderScriptReadyForTest)
|
||||||
(window as any)._recorderScriptReadyForTest();
|
(window as any)._recorderScriptReadyForTest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _setMode(mode: 'inspecting' | 'recording' | 'paused' | 'none') {
|
||||||
|
window.playwrightRecorderSetState({ mode }).then(() => this._pollRecorderMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _pollRecorderMode() {
|
||||||
|
if (this._pollRecorderModeTimer)
|
||||||
|
clearTimeout(this._pollRecorderModeTimer);
|
||||||
|
const result = await window.playwrightRecorderState().catch(e => null);
|
||||||
|
if (result) {
|
||||||
|
const { state, paused, tool } = result;
|
||||||
|
if (state && state.mode !== this._mode) {
|
||||||
|
this._mode = state.mode as any;
|
||||||
|
this._inspectElement.classList.toggle('toggled', this._mode === 'inspecting');
|
||||||
|
this._recordElement.classList.toggle('toggled', this._mode === 'recording');
|
||||||
|
this._inspectElement.classList.toggle('disabled', this._mode === 'recording');
|
||||||
|
this._resumeElement.classList.toggle('disabled', this._mode === 'recording');
|
||||||
|
this._clearHighlight();
|
||||||
|
}
|
||||||
|
if (paused !== this._paused) {
|
||||||
|
this._paused = paused;
|
||||||
|
this._resumeElement.classList.toggle('disabled', !this._paused);
|
||||||
|
}
|
||||||
|
if (tool !== this._tool) {
|
||||||
|
this._tool = tool;
|
||||||
|
this._resumeElement.classList.toggle('hidden', this._tool !== 'pause');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearHighlight() {
|
||||||
|
this._hoveredModel = null;
|
||||||
|
this._activeModel = null;
|
||||||
|
this._updateHighlight();
|
||||||
|
}
|
||||||
|
|
||||||
private _actionInProgress(event: Event): boolean {
|
private _actionInProgress(event: Event): boolean {
|
||||||
// If Playwright is performing action for us, bail.
|
// If Playwright is performing action for us, bail.
|
||||||
if (this._performingAction)
|
if (this._performingAction)
|
||||||
@ -143,7 +288,7 @@ export class Recorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _consumedDueWrongTarget(event: Event): boolean {
|
private _consumedDueWrongTarget(event: Event): boolean {
|
||||||
if (this._activeModel && this._activeModel.elements[0] === deepEventTarget(event))
|
if (this._activeModel && this._activeModel.elements[0] === this._deepEventTarget(event))
|
||||||
return false;
|
return false;
|
||||||
consumeEvent(event);
|
consumeEvent(event);
|
||||||
return true;
|
return true;
|
||||||
@ -157,7 +302,7 @@ export class Recorder {
|
|||||||
if (this._consumedDueToNoModel(event, this._hoveredModel))
|
if (this._consumedDueToNoModel(event, this._hoveredModel))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const checkbox = asCheckbox(deepEventTarget(event));
|
const checkbox = asCheckbox(this._deepEventTarget(event));
|
||||||
if (checkbox) {
|
if (checkbox) {
|
||||||
// Interestingly, inputElement.checked is reversed inside this event handler.
|
// Interestingly, inputElement.checked is reversed inside this event handler.
|
||||||
this._performAction({
|
this._performAction({
|
||||||
@ -178,8 +323,20 @@ export class Recorder {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _isInToolbar(element: Element | undefined | null): boolean {
|
||||||
|
return !!element && element.nodeName.toLowerCase().startsWith('x-pw-');
|
||||||
|
}
|
||||||
|
|
||||||
private _shouldIgnoreMouseEvent(event: MouseEvent): boolean {
|
private _shouldIgnoreMouseEvent(event: MouseEvent): boolean {
|
||||||
const target = deepEventTarget(event);
|
const target = this._deepEventTarget(event);
|
||||||
|
if (this._isInToolbar(target))
|
||||||
|
return true;
|
||||||
|
if (this._mode === 'none')
|
||||||
|
return true;
|
||||||
|
if (this._mode === 'inspecting') {
|
||||||
|
consumeEvent(event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const nodeName = target.nodeName;
|
const nodeName = target.nodeName;
|
||||||
if (nodeName === 'SELECT')
|
if (nodeName === 'SELECT')
|
||||||
return true;
|
return true;
|
||||||
@ -204,7 +361,11 @@ export class Recorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _onMouseMove(event: MouseEvent) {
|
private _onMouseMove(event: MouseEvent) {
|
||||||
const target = deepEventTarget(event);
|
if (this._mode === 'none')
|
||||||
|
return;
|
||||||
|
const target = this._deepEventTarget(event);
|
||||||
|
if (this._isInToolbar(target))
|
||||||
|
return;
|
||||||
if (this._hoveredElement === target)
|
if (this._hoveredElement === target)
|
||||||
return;
|
return;
|
||||||
this._hoveredElement = target;
|
this._hoveredElement = target;
|
||||||
@ -214,14 +375,14 @@ export class Recorder {
|
|||||||
|
|
||||||
private _onMouseLeave(event: MouseEvent) {
|
private _onMouseLeave(event: MouseEvent) {
|
||||||
// Leaving iframe.
|
// Leaving iframe.
|
||||||
if (deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
|
if (this._deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
|
||||||
this._hoveredElement = null;
|
this._hoveredElement = null;
|
||||||
this._commitActionAndUpdateModelForHoveredElement();
|
this._commitActionAndUpdateModelForHoveredElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onFocus() {
|
private _onFocus() {
|
||||||
const activeElement = deepActiveElement(document);
|
const activeElement = this._deepActiveElement(document);
|
||||||
const result = activeElement ? generateSelector(this._injectedScript, activeElement) : null;
|
const result = activeElement ? generateSelector(this._injectedScript, activeElement) : null;
|
||||||
this._activeModel = result && result.selector ? result : null;
|
this._activeModel = result && result.selector ? result : null;
|
||||||
if ((window as any)._highlightUpdatedForTest)
|
if ((window as any)._highlightUpdatedForTest)
|
||||||
@ -290,7 +451,7 @@ export class Recorder {
|
|||||||
this._highlightElements = [];
|
this._highlightElements = [];
|
||||||
for (const box of boxes) {
|
for (const box of boxes) {
|
||||||
const highlightElement = pool.length ? pool.shift()! : this._createHighlightElement();
|
const highlightElement = pool.length ? pool.shift()! : this._createHighlightElement();
|
||||||
highlightElement.style.borderColor = this._highlightElements.length ? 'hotpink' : '#8929ff';
|
highlightElement.style.backgroundColor = this._highlightElements.length ? '#f6b26b7f' : '#6fa8dc7f';
|
||||||
highlightElement.style.left = box.x + 'px';
|
highlightElement.style.left = box.x + 'px';
|
||||||
highlightElement.style.top = box.y + 'px';
|
highlightElement.style.top = box.y + 'px';
|
||||||
highlightElement.style.width = box.width + 'px';
|
highlightElement.style.width = box.width + 'px';
|
||||||
@ -313,7 +474,6 @@ export class Recorder {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
border: 1px solid;
|
|
||||||
box-sizing: border-box;">
|
box-sizing: border-box;">
|
||||||
</x-pw-highlight>`;
|
</x-pw-highlight>`;
|
||||||
this._glassPaneShadow.appendChild(highlightElement);
|
this._glassPaneShadow.appendChild(highlightElement);
|
||||||
@ -321,7 +481,9 @@ export class Recorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _onInput(event: Event) {
|
private _onInput(event: Event) {
|
||||||
const target = deepEventTarget(event);
|
if (this._mode !== 'recording')
|
||||||
|
return true;
|
||||||
|
const target = this._deepEventTarget(event);
|
||||||
if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) {
|
if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) {
|
||||||
const inputElement = target as HTMLInputElement;
|
const inputElement = target as HTMLInputElement;
|
||||||
const elementType = (inputElement.type || '').toLowerCase();
|
const elementType = (inputElement.type || '').toLowerCase();
|
||||||
@ -385,11 +547,17 @@ export class Recorder {
|
|||||||
return false;
|
return false;
|
||||||
const hasModifier = event.ctrlKey || event.altKey || event.metaKey;
|
const hasModifier = event.ctrlKey || event.altKey || event.metaKey;
|
||||||
if (event.key.length === 1 && !hasModifier)
|
if (event.key.length === 1 && !hasModifier)
|
||||||
return !!asCheckbox(deepEventTarget(event));
|
return !!asCheckbox(this._deepEventTarget(event));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onKeyDown(event: KeyboardEvent) {
|
private _onKeyDown(event: KeyboardEvent) {
|
||||||
|
if (this._mode === 'inspecting') {
|
||||||
|
consumeEvent(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this._mode !== 'recording')
|
||||||
|
return true;
|
||||||
if (!this._shouldGenerateKeyPressFor(event))
|
if (!this._shouldGenerateKeyPressFor(event))
|
||||||
return;
|
return;
|
||||||
if (this._actionInProgress(event)) {
|
if (this._actionInProgress(event)) {
|
||||||
@ -400,7 +568,7 @@ export class Recorder {
|
|||||||
return;
|
return;
|
||||||
// Similarly to click, trigger checkbox on key event, not input.
|
// Similarly to click, trigger checkbox on key event, not input.
|
||||||
if (event.key === ' ') {
|
if (event.key === ' ') {
|
||||||
const checkbox = asCheckbox(deepEventTarget(event));
|
const checkbox = asCheckbox(this._deepEventTarget(event));
|
||||||
if (checkbox) {
|
if (checkbox) {
|
||||||
this._performAction({
|
this._performAction({
|
||||||
name: checkbox.checked ? 'uncheck' : 'check',
|
name: checkbox.checked ? 'uncheck' : 'check',
|
||||||
@ -449,18 +617,18 @@ export class Recorder {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function deepEventTarget(event: Event): HTMLElement {
|
private _deepEventTarget(event: Event): HTMLElement {
|
||||||
return event.composedPath()[0] as HTMLElement;
|
return event.composedPath()[0] as HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deepActiveElement(document: Document): Element | null {
|
private _deepActiveElement(document: Document): Element | null {
|
||||||
let activeElement = document.activeElement;
|
let activeElement = document.activeElement;
|
||||||
while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)
|
while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)
|
||||||
activeElement = activeElement.shadowRoot.activeElement;
|
activeElement = activeElement.shadowRoot.activeElement;
|
||||||
return activeElement;
|
return activeElement;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function modifiersForEvent(event: MouseEvent | KeyboardEvent): number {
|
function modifiersForEvent(event: MouseEvent | KeyboardEvent): number {
|
||||||
return (event.altKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.metaKey ? 4 : 0) | (event.shiftKey ? 8 : 0);
|
return (event.altKey ? 1 : 0) | (event.ctrlKey ? 2 : 0) | (event.metaKey ? 4 : 0) | (event.shiftKey ? 8 : 0);
|
||||||
@ -500,14 +668,17 @@ type RegisteredListener = {
|
|||||||
useCapture?: boolean;
|
useCapture?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function addEventListener(target: EventTarget, eventName: string, listener: EventListener, useCapture?: boolean): RegisteredListener {
|
function addEventListener(target: EventTarget, eventName: string, listener: EventListener, useCapture?: boolean): () => void {
|
||||||
target.addEventListener(eventName, listener, useCapture);
|
target.addEventListener(eventName, listener, useCapture);
|
||||||
return { target, eventName, listener, useCapture };
|
const remove = () => {
|
||||||
|
target.removeEventListener(eventName, listener, useCapture);
|
||||||
|
};
|
||||||
|
return remove;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEventListeners(listeners: RegisteredListener[]) {
|
function removeEventListeners(listeners: (() => void)[]) {
|
||||||
for (const listener of listeners)
|
for (const listener of listeners)
|
||||||
listener.target.removeEventListener(listener.eventName, listener.listener, listener.useCapture);
|
listener();
|
||||||
listeners.splice(0, listeners.length);
|
listeners.splice(0, listeners.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,18 +37,20 @@ export class CodeGenerator {
|
|||||||
private _lastActionText: string | undefined;
|
private _lastActionText: string | undefined;
|
||||||
private _languageGenerator: LanguageGenerator;
|
private _languageGenerator: LanguageGenerator;
|
||||||
private _output: CodeGeneratorOutput;
|
private _output: CodeGeneratorOutput;
|
||||||
private _footerText: string;
|
private _footerText = '';
|
||||||
|
|
||||||
constructor(browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, output: CodeGeneratorOutput, languageGenerator: LanguageGenerator, deviceName: string | undefined, saveStorage: string | undefined) {
|
constructor(browserName: string, generateHeaders: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, output: CodeGeneratorOutput, languageGenerator: LanguageGenerator, deviceName: string | undefined, saveStorage: string | undefined) {
|
||||||
this._output = output;
|
this._output = output;
|
||||||
this._languageGenerator = languageGenerator;
|
this._languageGenerator = languageGenerator;
|
||||||
|
|
||||||
launchOptions = { headless: false, ...launchOptions };
|
launchOptions = { headless: false, ...launchOptions };
|
||||||
|
if (generateHeaders) {
|
||||||
const header = this._languageGenerator.generateHeader(browserName, launchOptions, contextOptions, deviceName);
|
const header = this._languageGenerator.generateHeader(browserName, launchOptions, contextOptions, deviceName);
|
||||||
this._output.printLn(header);
|
this._output.printLn(header);
|
||||||
this._footerText = '\n' + this._languageGenerator.generateFooter(saveStorage);
|
this._footerText = '\n' + this._languageGenerator.generateFooter(saveStorage);
|
||||||
this._output.printLn(this._footerText);
|
this._output.printLn(this._footerText);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addAction(action: ActionInContext) {
|
addAction(action: ActionInContext) {
|
||||||
this.willPerformAction(action);
|
this.willPerformAction(action);
|
||||||
@ -94,6 +96,7 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_printAction(actionInContext: ActionInContext, eraseLastAction: boolean) {
|
_printAction(actionInContext: ActionInContext, eraseLastAction: boolean) {
|
||||||
|
if (this._footerText)
|
||||||
this._output.popLn(this._footerText);
|
this._output.popLn(this._footerText);
|
||||||
if (eraseLastAction && this._lastActionText)
|
if (eraseLastAction && this._lastActionText)
|
||||||
this._output.popLn(this._lastActionText);
|
this._output.popLn(this._lastActionText);
|
||||||
@ -102,6 +105,7 @@ export class CodeGenerator {
|
|||||||
this._lastAction = actionInContext;
|
this._lastAction = actionInContext;
|
||||||
this._lastActionText = this._languageGenerator.generateAction(actionInContext, performingAction);
|
this._lastActionText = this._languageGenerator.generateAction(actionInContext, performingAction);
|
||||||
this._output.printLn(this._lastActionText);
|
this._output.printLn(this._lastActionText);
|
||||||
|
if (this._footerText)
|
||||||
this._output.printLn(this._footerText);
|
this._output.printLn(this._footerText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,27 +16,46 @@
|
|||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as querystring from 'querystring';
|
import * as querystring from 'querystring';
|
||||||
import { Writable } from 'stream';
|
import * as hljs from '../../../third_party/highlightjs/highlightjs';
|
||||||
import * as hljs from '../../third_party/highlightjs/highlightjs';
|
|
||||||
import { RecorderOutput } from './recorderSupplement';
|
export interface RecorderOutput {
|
||||||
|
printLn(text: string): void;
|
||||||
|
popLn(text: string): void;
|
||||||
|
flush(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Writable {
|
||||||
|
write(data: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
export class OutputMultiplexer implements RecorderOutput {
|
export class OutputMultiplexer implements RecorderOutput {
|
||||||
private _outputs: RecorderOutput[]
|
private _outputs: RecorderOutput[]
|
||||||
|
private _enabled = true;
|
||||||
constructor(outputs: RecorderOutput[]) {
|
constructor(outputs: RecorderOutput[]) {
|
||||||
this._outputs = outputs;
|
this._outputs = outputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setEnabled(enabled: boolean) {
|
||||||
|
this._enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
printLn(text: string) {
|
printLn(text: string) {
|
||||||
|
if (!this._enabled)
|
||||||
|
return;
|
||||||
for (const output of this._outputs)
|
for (const output of this._outputs)
|
||||||
output.printLn(text);
|
output.printLn(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
popLn(text: string) {
|
popLn(text: string) {
|
||||||
|
if (!this._enabled)
|
||||||
|
return;
|
||||||
for (const output of this._outputs)
|
for (const output of this._outputs)
|
||||||
output.popLn(text);
|
output.popLn(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
flush() {
|
flush() {
|
||||||
|
if (!this._enabled)
|
||||||
|
return;
|
||||||
for (const output of this._outputs)
|
for (const output of this._outputs)
|
||||||
output.flush();
|
output.flush();
|
||||||
}
|
}
|
||||||
@ -64,6 +83,7 @@ export class FileOutput extends BufferOutput implements RecorderOutput {
|
|||||||
constructor(fileName: string) {
|
constructor(fileName: string) {
|
||||||
super();
|
super();
|
||||||
this._fileName = fileName;
|
this._fileName = fileName;
|
||||||
|
process.on('exit', () => this.flush());
|
||||||
}
|
}
|
||||||
|
|
||||||
flush() {
|
flush() {
|
||||||
@ -72,7 +92,7 @@ export class FileOutput extends BufferOutput implements RecorderOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class TerminalOutput implements RecorderOutput {
|
export class TerminalOutput implements RecorderOutput {
|
||||||
private _output: Writable
|
private _output: Writable;
|
||||||
private _language: string;
|
private _language: string;
|
||||||
|
|
||||||
static create(output: Writable, language: string) {
|
static create(output: Writable, language: string) {
|
@ -15,7 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as actions from './recorder/recorderActions';
|
import * as actions from './recorder/recorderActions';
|
||||||
import { CodeGenerator, ActionInContext, CodeGeneratorOutput } from './recorder/codeGenerator';
|
import type * as channels from '../../protocol/channels';
|
||||||
|
import { CodeGenerator, ActionInContext } from './recorder/codeGenerator';
|
||||||
import { toClickOptions, toModifiers } from './recorder/utils';
|
import { toClickOptions, toModifiers } from './recorder/utils';
|
||||||
import { Page } from '../page';
|
import { Page } from '../page';
|
||||||
import { Frame } from '../frames';
|
import { Frame } from '../frames';
|
||||||
@ -26,8 +27,15 @@ import { CSharpLanguageGenerator } from './recorder/csharp';
|
|||||||
import { PythonLanguageGenerator } from './recorder/python';
|
import { PythonLanguageGenerator } from './recorder/python';
|
||||||
import { ProgressController } from '../progress';
|
import { ProgressController } from '../progress';
|
||||||
import * as recorderSource from '../../generated/recorderSource';
|
import * as recorderSource from '../../generated/recorderSource';
|
||||||
|
import * as consoleApiSource from '../../generated/consoleApiSource';
|
||||||
|
import { FileOutput, FlushingTerminalOutput, OutputMultiplexer, RecorderOutput, TerminalOutput, Writable } from './recorder/outputs';
|
||||||
|
|
||||||
type BindingSource = { frame: Frame, page: Page };
|
type BindingSource = { frame: Frame, page: Page };
|
||||||
|
type Tool = 'codegen' | 'pause';
|
||||||
|
type Mode = 'inspecting' | 'recording' | 'none';
|
||||||
|
|
||||||
|
const symbol = Symbol('RecorderSupplement');
|
||||||
|
|
||||||
|
|
||||||
export class RecorderSupplement {
|
export class RecorderSupplement {
|
||||||
private _generator: CodeGenerator;
|
private _generator: CodeGenerator;
|
||||||
@ -36,11 +44,27 @@ export class RecorderSupplement {
|
|||||||
private _lastDialogOrdinal = 0;
|
private _lastDialogOrdinal = 0;
|
||||||
private _timers = new Set<NodeJS.Timeout>();
|
private _timers = new Set<NodeJS.Timeout>();
|
||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
|
private _resumeCallback: (() => void) | null = null;
|
||||||
|
private _recorderState: { mode: Mode };
|
||||||
|
private _paused = false;
|
||||||
|
private _tool: Tool;
|
||||||
|
private _output: OutputMultiplexer;
|
||||||
|
|
||||||
constructor(context: BrowserContext, params: { language: string, launchOptions: any, contextOptions: any, device?: string, saveStorage?: string}, output: CodeGeneratorOutput) {
|
static getOrCreate(context: BrowserContext, tool: Tool, params: channels.BrowserContextRecorderSupplementEnableParams): Promise<RecorderSupplement> {
|
||||||
|
let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
|
||||||
|
if (!recorderPromise) {
|
||||||
|
const recorder = new RecorderSupplement(context, tool, params);
|
||||||
|
recorderPromise = recorder.install().then(() => recorder);
|
||||||
|
(context as any)[symbol] = recorderPromise;
|
||||||
|
}
|
||||||
|
return recorderPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: BrowserContext, tool: Tool, params: channels.BrowserContextRecorderSupplementEnableParams) {
|
||||||
this._context = context;
|
this._context = context;
|
||||||
|
this._tool = tool;
|
||||||
|
this._recorderState = { mode: tool === 'codegen' ? 'recording' : 'none' };
|
||||||
let languageGenerator: LanguageGenerator;
|
let languageGenerator: LanguageGenerator;
|
||||||
|
|
||||||
switch (params.language) {
|
switch (params.language) {
|
||||||
case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break;
|
case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break;
|
||||||
case 'csharp': languageGenerator = new CSharpLanguageGenerator(); break;
|
case 'csharp': languageGenerator = new CSharpLanguageGenerator(); break;
|
||||||
@ -48,7 +72,21 @@ export class RecorderSupplement {
|
|||||||
case 'python-async': languageGenerator = new PythonLanguageGenerator(params.language === 'python-async'); break;
|
case 'python-async': languageGenerator = new PythonLanguageGenerator(params.language === 'python-async'); break;
|
||||||
default: throw new Error(`Invalid target: '${params.language}'`);
|
default: throw new Error(`Invalid target: '${params.language}'`);
|
||||||
}
|
}
|
||||||
const generator = new CodeGenerator(context._browser._options.name, params.launchOptions, params.contextOptions, output, languageGenerator, params.device, params.saveStorage);
|
let highlighterType = params.language;
|
||||||
|
if (highlighterType === 'python-async')
|
||||||
|
highlighterType = 'python';
|
||||||
|
|
||||||
|
const writable: Writable = {
|
||||||
|
write: (text: string) => context.emit(BrowserContext.Events.StdOut, text)
|
||||||
|
};
|
||||||
|
const outputs: RecorderOutput[] = [params.terminal ? new TerminalOutput(writable, highlighterType) : new FlushingTerminalOutput(writable)];
|
||||||
|
if (params.outputFile)
|
||||||
|
outputs.push(new FileOutput(params.outputFile));
|
||||||
|
this._output = new OutputMultiplexer(outputs);
|
||||||
|
this._output.setEnabled(tool === 'codegen');
|
||||||
|
context.on(BrowserContext.Events.BeforeClose, () => this._output.flush());
|
||||||
|
|
||||||
|
const generator = new CodeGenerator(context._browser._options.name, tool === 'codegen', params.launchOptions || {}, params.contextOptions || {}, this._output, languageGenerator, params.device, params.saveStorage);
|
||||||
this._generator = generator;
|
this._generator = generator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,7 +114,34 @@ export class RecorderSupplement {
|
|||||||
await this._context.exposeBinding('playwrightRecorderCommitAction', false,
|
await this._context.exposeBinding('playwrightRecorderCommitAction', false,
|
||||||
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());
|
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());
|
||||||
|
|
||||||
|
await this._context.exposeBinding('playwrightRecorderState', false, () => {
|
||||||
|
return {
|
||||||
|
state: this._recorderState,
|
||||||
|
tool: this._tool,
|
||||||
|
paused: this._paused
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._context.exposeBinding('playwrightRecorderSetState', false, (source, state) => {
|
||||||
|
this._recorderState = state;
|
||||||
|
this._output.setEnabled(state.mode === 'recording');
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._context.exposeBinding('playwrightRecorderResume', false, () => {
|
||||||
|
if (this._resumeCallback) {
|
||||||
|
this._resumeCallback();
|
||||||
|
this._resumeCallback = null;
|
||||||
|
}
|
||||||
|
this._paused = false;
|
||||||
|
});
|
||||||
|
|
||||||
await this._context.extendInjectedScript(recorderSource.source);
|
await this._context.extendInjectedScript(recorderSource.source);
|
||||||
|
await this._context.extendInjectedScript(consoleApiSource.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
async pause() {
|
||||||
|
this._paused = true;
|
||||||
|
return new Promise(f => this._resumeCallback = f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _onPage(page: Page) {
|
private async _onPage(page: Page) {
|
||||||
|
@ -15,14 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import { Writable } from 'stream';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { ChildProcess, spawn } from 'child_process';
|
import { ChildProcess, spawn } from 'child_process';
|
||||||
import { folio as baseFolio } from '../fixtures';
|
import { folio as baseFolio } from '../fixtures';
|
||||||
import type { Page, BrowserType, Browser, BrowserContext } from '../..';
|
import type { Page, BrowserType, Browser, BrowserContext } from '../..';
|
||||||
export { config } from 'folio';
|
export { config } from 'folio';
|
||||||
import { FlushingTerminalOutput } from '../../lib/client/supplements/recorderOutputs';
|
|
||||||
import { RecorderSupplement } from '../../lib/client/supplements/recorderSupplement';
|
|
||||||
|
|
||||||
type WorkerFixtures = {
|
type WorkerFixtures = {
|
||||||
browserType: BrowserType<Browser>;
|
browserType: BrowserType<Browser>;
|
||||||
@ -41,8 +38,8 @@ export const fixtures = baseFolio.extend<TestFixtures, WorkerFixtures>();
|
|||||||
fixtures.contextWrapper.init(async ({ browser }, runTest) => {
|
fixtures.contextWrapper.init(async ({ browser }, runTest) => {
|
||||||
const context = await browser.newContext() as BrowserContext;
|
const context = await browser.newContext() as BrowserContext;
|
||||||
const outputBuffer = new WritableBuffer();
|
const outputBuffer = new WritableBuffer();
|
||||||
const output = new FlushingTerminalOutput(outputBuffer as any as Writable);
|
(context as any)._stdout = outputBuffer;
|
||||||
new RecorderSupplement(context, 'javascript', {}, {}, undefined, undefined, output);
|
await (context as any)._enableRecorder('javascript');
|
||||||
await runTest({ context, output: outputBuffer });
|
await runTest({ context, output: outputBuffer });
|
||||||
await context.close();
|
await context.close();
|
||||||
});
|
});
|
||||||
@ -88,14 +85,10 @@ class WritableBuffer {
|
|||||||
this._data = '';
|
this._data = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
write(chunk: string) {
|
write(data: Buffer) {
|
||||||
if (!chunk)
|
if (!data)
|
||||||
return;
|
return;
|
||||||
if (chunk === '\u001B[F\u001B[2K') {
|
const chunk = data.toString('utf8');
|
||||||
const index = this._data.lastIndexOf('\n');
|
|
||||||
this._data = this._data.substring(0, index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._data += chunk;
|
this._data += chunk;
|
||||||
if (this._callback && chunk.includes(this._text))
|
if (this._callback && chunk.includes(this._text))
|
||||||
this._callback();
|
this._callback();
|
||||||
|
@ -27,7 +27,7 @@ it('should pause and resume the script', async ({page}) => {
|
|||||||
const resumePromise = (page as any)._pause().then(() => resolved = true);
|
const resumePromise = (page as any)._pause().then(() => resolved = true);
|
||||||
await new Promise(x => setTimeout(x, 0));
|
await new Promise(x => setTimeout(x, 0));
|
||||||
expect(resolved).toBe(false);
|
expect(resolved).toBe(false);
|
||||||
await page.click('playwright-resume');
|
await page.click('.playwright-resume');
|
||||||
await resumePromise;
|
await resumePromise;
|
||||||
expect(resolved).toBe(true);
|
expect(resolved).toBe(true);
|
||||||
});
|
});
|
||||||
@ -38,7 +38,7 @@ it('should pause through a navigation', async ({page, server}) => {
|
|||||||
await new Promise(x => setTimeout(x, 0));
|
await new Promise(x => setTimeout(x, 0));
|
||||||
expect(resolved).toBe(false);
|
expect(resolved).toBe(false);
|
||||||
await page.goto(server.EMPTY_PAGE);
|
await page.goto(server.EMPTY_PAGE);
|
||||||
await page.click('playwright-resume');
|
await page.click('.playwright-resume');
|
||||||
await resumePromise;
|
await resumePromise;
|
||||||
expect(resolved).toBe(true);
|
expect(resolved).toBe(true);
|
||||||
});
|
});
|
||||||
@ -50,7 +50,7 @@ it('should pause after a navigation', async ({page, server}) => {
|
|||||||
const resumePromise = (page as any)._pause().then(() => resolved = true);
|
const resumePromise = (page as any)._pause().then(() => resolved = true);
|
||||||
await new Promise(x => setTimeout(x, 0));
|
await new Promise(x => setTimeout(x, 0));
|
||||||
expect(resolved).toBe(false);
|
expect(resolved).toBe(false);
|
||||||
await page.click('playwright-resume');
|
await page.click('.playwright-resume');
|
||||||
await resumePromise;
|
await resumePromise;
|
||||||
expect(resolved).toBe(true);
|
expect(resolved).toBe(true);
|
||||||
});
|
});
|
||||||
|
@ -16,11 +16,10 @@
|
|||||||
|
|
||||||
import { folio } from './fixtures';
|
import { folio } from './fixtures';
|
||||||
import type { Page, Frame } from '..';
|
import type { Page, Frame } from '..';
|
||||||
import { ConsoleApiSupplement } from '../lib/client/supplements/consoleApiSupplement';
|
|
||||||
|
|
||||||
const fixtures = folio.extend();
|
const fixtures = folio.extend();
|
||||||
fixtures.context.override(async ({ context }, run) => {
|
fixtures.context.override(async ({ context }, run) => {
|
||||||
new ConsoleApiSupplement(context);
|
await (context as any)._enableConsoleApi();
|
||||||
await run(context);
|
await run(context);
|
||||||
});
|
});
|
||||||
const { describe, it, expect } = fixtures.build();
|
const { describe, it, expect } = fixtures.build();
|
||||||
|
@ -126,6 +126,7 @@ DEPS['src/server/'] = [
|
|||||||
'src/server/common/**',
|
'src/server/common/**',
|
||||||
'src/server/injected/**',
|
'src/server/injected/**',
|
||||||
'src/server/supplements/**',
|
'src/server/supplements/**',
|
||||||
|
'src/protocol/**',
|
||||||
];
|
];
|
||||||
|
|
||||||
// No dependencies for code shared between node and page.
|
// No dependencies for code shared between node and page.
|
||||||
@ -133,8 +134,6 @@ DEPS['src/server/common/'] = [];
|
|||||||
// Strict dependencies for injected code.
|
// Strict dependencies for injected code.
|
||||||
DEPS['src/server/injected/'] = ['src/server/common/'];
|
DEPS['src/server/injected/'] = ['src/server/common/'];
|
||||||
|
|
||||||
DEPS['src/client/supplements/'] = ['src/client/'];
|
|
||||||
|
|
||||||
// Electron and Clank use chromium internally.
|
// Electron and Clank use chromium internally.
|
||||||
DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/'];
|
DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/'];
|
||||||
DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
|
DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user