chore: allow starting recorder in traceviewer (#32741)
This commit is contained in:
parent
dfb3fdf217
commit
418d1c0c55
|
|
@ -574,6 +574,7 @@ async function codegen(options: Options & { target: string, output?: string, tes
|
||||||
device: options.device,
|
device: options.device,
|
||||||
saveStorage: options.saveStorage,
|
saveStorage: options.saveStorage,
|
||||||
mode: 'recording',
|
mode: 'recording',
|
||||||
|
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions',
|
||||||
testIdAttributeName,
|
testIdAttributeName,
|
||||||
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -492,17 +492,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
||||||
await this._closedPromise;
|
await this._closedPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _enableRecorder(params: {
|
async _enableRecorder(params: channels.BrowserContextEnableRecorderParams) {
|
||||||
language: string,
|
await this._channel.enableRecorder(params);
|
||||||
launchOptions?: LaunchOptions,
|
|
||||||
contextOptions?: BrowserContextOptions,
|
|
||||||
device?: string,
|
|
||||||
saveStorage?: string,
|
|
||||||
mode?: 'recording' | 'inspecting',
|
|
||||||
testIdAttributeName?: string,
|
|
||||||
outputFile?: string,
|
|
||||||
}) {
|
|
||||||
await this._channel.recorderSupplementEnable(params);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -965,9 +965,10 @@ scheme.BrowserContextStorageStateResult = tObject({
|
||||||
});
|
});
|
||||||
scheme.BrowserContextPauseParams = tOptional(tObject({}));
|
scheme.BrowserContextPauseParams = tOptional(tObject({}));
|
||||||
scheme.BrowserContextPauseResult = tOptional(tObject({}));
|
scheme.BrowserContextPauseResult = tOptional(tObject({}));
|
||||||
scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
scheme.BrowserContextEnableRecorderParams = tObject({
|
||||||
language: tOptional(tString),
|
language: tOptional(tString),
|
||||||
mode: tOptional(tEnum(['inspecting', 'recording'])),
|
mode: tOptional(tEnum(['inspecting', 'recording'])),
|
||||||
|
codegenMode: tOptional(tEnum(['actions', 'trace-events'])),
|
||||||
pauseOnNextStatement: tOptional(tBoolean),
|
pauseOnNextStatement: tOptional(tBoolean),
|
||||||
testIdAttributeName: tOptional(tString),
|
testIdAttributeName: tOptional(tString),
|
||||||
launchOptions: tOptional(tAny),
|
launchOptions: tOptional(tAny),
|
||||||
|
|
@ -977,7 +978,7 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
||||||
outputFile: tOptional(tString),
|
outputFile: tOptional(tString),
|
||||||
omitCallTracking: tOptional(tBoolean),
|
omitCallTracking: tOptional(tBoolean),
|
||||||
});
|
});
|
||||||
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({}));
|
scheme.BrowserContextEnableRecorderResult = tOptional(tObject({}));
|
||||||
scheme.BrowserContextNewCDPSessionParams = tObject({
|
scheme.BrowserContextNewCDPSessionParams = tObject({
|
||||||
page: tOptional(tChannel(['Page'])),
|
page: tOptional(tChannel(['Page'])),
|
||||||
frame: tOptional(tChannel(['Frame'])),
|
frame: tOptional(tChannel(['Frame'])),
|
||||||
|
|
|
||||||
|
|
@ -131,15 +131,15 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
|
|
||||||
// When PWDEBUG=1, show inspector for each context.
|
// When PWDEBUG=1, show inspector for each context.
|
||||||
if (debugMode() === 'inspector')
|
if (debugMode() === 'inspector')
|
||||||
await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true });
|
await Recorder.show('actions', this, RecorderApp.factory(this), { pauseOnNextStatement: true });
|
||||||
|
|
||||||
// When paused, show inspector.
|
// When paused, show inspector.
|
||||||
if (this._debugger.isPaused())
|
if (this._debugger.isPaused())
|
||||||
Recorder.showInspector(this, RecorderApp.factory(this));
|
Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
|
||||||
|
|
||||||
this._debugger.on(Debugger.Events.PausedStateChanged, () => {
|
this._debugger.on(Debugger.Events.PausedStateChanged, () => {
|
||||||
if (this._debugger.isPaused())
|
if (this._debugger.isPaused())
|
||||||
Recorder.showInspector(this, RecorderApp.factory(this));
|
Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (debugMode() === 'console')
|
if (debugMode() === 'console')
|
||||||
|
|
|
||||||
|
|
@ -197,7 +197,7 @@ export class DebugController extends SdkObject {
|
||||||
const contexts = new Set<BrowserContext>();
|
const contexts = new Set<BrowserContext>();
|
||||||
for (const page of this._playwright.allPages())
|
for (const page of this._playwright.allPages())
|
||||||
contexts.add(page.context());
|
contexts.add(page.context());
|
||||||
const result = await Promise.all([...contexts].map(c => Recorder.show(c, () => Promise.resolve(new InspectingRecorderApp(this)), { omitCallTracking: true })));
|
const result = await Promise.all([...contexts].map(c => Recorder.showInspector(c, { omitCallTracking: true }, () => Promise.resolve(new InspectingRecorderApp(this)))));
|
||||||
return result.filter(Boolean) as Recorder[];
|
return result.filter(Boolean) as Recorder[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,6 @@ import { serializeError } from '../errors';
|
||||||
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
||||||
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
|
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
|
||||||
import { RecorderApp } from '../recorder/recorderApp';
|
import { RecorderApp } from '../recorder/recorderApp';
|
||||||
import type { IRecorderAppFactory } from '../recorder/recorderFrontend';
|
|
||||||
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
|
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
|
||||||
|
|
||||||
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
|
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
|
||||||
|
|
@ -301,21 +300,19 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||||
await this._context.close(params);
|
await this._context.close(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
|
async enableRecorder(params: channels.BrowserContextEnableRecorderParams): Promise<void> {
|
||||||
let factory: IRecorderAppFactory;
|
if (params.codegenMode === 'trace-events') {
|
||||||
if (process.env.PW_RECORDER_IS_TRACE_VIEWER) {
|
|
||||||
factory = RecorderInTraceViewer.factory(this._context);
|
|
||||||
await this._context.tracing.start({
|
await this._context.tracing.start({
|
||||||
name: 'trace',
|
name: 'trace',
|
||||||
snapshots: true,
|
snapshots: true,
|
||||||
screenshots: false,
|
screenshots: true,
|
||||||
live: true,
|
live: true,
|
||||||
});
|
});
|
||||||
await this._context.tracing.startChunk({ name: 'trace', title: 'trace' });
|
await this._context.tracing.startChunk({ name: 'trace', title: 'trace' });
|
||||||
|
await Recorder.show('trace-events', this._context, RecorderInTraceViewer.factory(this._context), params);
|
||||||
} else {
|
} else {
|
||||||
factory = RecorderApp.factory(this._context);
|
await Recorder.show('actions', this._context, RecorderApp.factory(this._context), params);
|
||||||
}
|
}
|
||||||
await Recorder.show(this._context, factory, params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
|
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {
|
||||||
|
|
|
||||||
|
|
@ -45,32 +45,35 @@ export class Recorder implements InstrumentationListener, IRecorder {
|
||||||
private _omitCallTracking = false;
|
private _omitCallTracking = false;
|
||||||
private _currentLanguage: Language;
|
private _currentLanguage: Language;
|
||||||
|
|
||||||
static showInspector(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) {
|
static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) {
|
||||||
const params: channels.BrowserContextRecorderSupplementEnableParams = {};
|
|
||||||
if (isUnderTest())
|
if (isUnderTest())
|
||||||
params.language = process.env.TEST_INSPECTOR_LANGUAGE;
|
params.language = process.env.TEST_INSPECTOR_LANGUAGE;
|
||||||
Recorder.show(context, recorderAppFactory, params).catch(() => {});
|
return await Recorder.show('actions', context, recorderAppFactory, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
static show(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<Recorder> {
|
static showInspectorNoReply(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) {
|
||||||
|
Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
static show(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise<Recorder> {
|
||||||
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
|
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
|
||||||
if (!recorderPromise) {
|
if (!recorderPromise) {
|
||||||
recorderPromise = Recorder._create(context, recorderAppFactory, params);
|
recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params);
|
||||||
(context as any)[recorderSymbol] = recorderPromise;
|
(context as any)[recorderSymbol] = recorderPromise;
|
||||||
}
|
}
|
||||||
return recorderPromise;
|
return recorderPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<Recorder> {
|
private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
|
||||||
const recorder = new Recorder(context, params);
|
const recorder = new Recorder(codegenMode, context, params);
|
||||||
const recorderApp = await recorderAppFactory(recorder);
|
const recorderApp = await recorderAppFactory(recorder);
|
||||||
await recorder._install(recorderApp);
|
await recorder._install(recorderApp);
|
||||||
return recorder;
|
return recorder;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
|
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
|
||||||
this._mode = params.mode || 'none';
|
this._mode = params.mode || 'none';
|
||||||
this._contextRecorder = new ContextRecorder(context, params, {});
|
this._contextRecorder = new ContextRecorder(codegenMode, context, params, {});
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._omitCallTracking = !!params.omitCallTracking;
|
this._omitCallTracking = !!params.omitCallTracking;
|
||||||
this._debugger = context.debugger();
|
this._debugger = context.debugger();
|
||||||
|
|
|
||||||
|
|
@ -48,14 +48,14 @@ export class ContextRecorder extends EventEmitter {
|
||||||
private _lastDialogOrdinal = -1;
|
private _lastDialogOrdinal = -1;
|
||||||
private _lastDownloadOrdinal = -1;
|
private _lastDownloadOrdinal = -1;
|
||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
private _params: channels.BrowserContextRecorderSupplementEnableParams;
|
private _params: channels.BrowserContextEnableRecorderParams;
|
||||||
private _delegate: ContextRecorderDelegate;
|
private _delegate: ContextRecorderDelegate;
|
||||||
private _recorderSources: Source[];
|
private _recorderSources: Source[];
|
||||||
private _throttledOutputFile: ThrottledFile | null = null;
|
private _throttledOutputFile: ThrottledFile | null = null;
|
||||||
private _orderedLanguages: LanguageGenerator[] = [];
|
private _orderedLanguages: LanguageGenerator[] = [];
|
||||||
private _listeners: RegisteredListener[] = [];
|
private _listeners: RegisteredListener[] = [];
|
||||||
|
|
||||||
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams, delegate: ContextRecorderDelegate) {
|
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) {
|
||||||
super();
|
super();
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._params = params;
|
this._params = params;
|
||||||
|
|
@ -73,7 +73,7 @@ export class ContextRecorder extends EventEmitter {
|
||||||
saveStorage: params.saveStorage,
|
saveStorage: params.saveStorage,
|
||||||
};
|
};
|
||||||
|
|
||||||
const collection = new RecorderCollection(context, this._pageAliases, params.mode === 'recording');
|
const collection = new RecorderCollection(codegenMode, context, this._pageAliases, params.mode === 'recording');
|
||||||
collection.on('change', (actions: ActionInContext[]) => {
|
collection.on('change', (actions: ActionInContext[]) => {
|
||||||
this._recorderSources = [];
|
this._recorderSources = [];
|
||||||
for (const languageGenerator of this._orderedLanguages) {
|
for (const languageGenerator of this._orderedLanguages) {
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,13 @@ export class RecorderCollection extends EventEmitter {
|
||||||
private _pageAliases: Map<Page, string>;
|
private _pageAliases: Map<Page, string>;
|
||||||
private _context: BrowserContext;
|
private _context: BrowserContext;
|
||||||
|
|
||||||
constructor(context: BrowserContext, pageAliases: Map<Page, string>, enabled: boolean) {
|
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, pageAliases: Map<Page, string>, enabled: boolean) {
|
||||||
super();
|
super();
|
||||||
this._context = context;
|
this._context = context;
|
||||||
this._enabled = enabled;
|
this._enabled = enabled;
|
||||||
this._pageAliases = pageAliases;
|
this._pageAliases = pageAliases;
|
||||||
|
|
||||||
if (process.env.PW_RECORDER_IS_TRACE_VIEWER) {
|
if (codegenMode === 'trace-events') {
|
||||||
this._context.tracing.onMemoryEvents(events => {
|
this._context.tracing.onMemoryEvents(events => {
|
||||||
this._actions = traceEventsToAction(events);
|
this._actions = traceEventsToAction(events);
|
||||||
this._fireChange();
|
this._fireChange();
|
||||||
|
|
|
||||||
|
|
@ -193,13 +193,12 @@ export function traceParamsForAction(actionInContext: ActionInContext): { method
|
||||||
|
|
||||||
export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } {
|
export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } {
|
||||||
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
|
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
|
||||||
const { action } = actionInContext;
|
|
||||||
const { method, params } = traceParamsForAction(actionInContext);
|
const { method, params } = traceParamsForAction(actionInContext);
|
||||||
|
|
||||||
const callMetadata: CallMetadata = {
|
const callMetadata: CallMetadata = {
|
||||||
id: `call@${createGuid()}`,
|
id: `call@${createGuid()}`,
|
||||||
stepId: `recorder@${createGuid()}`,
|
stepId: `recorder@${createGuid()}`,
|
||||||
apiName: 'frame.' + action.name,
|
apiName: 'page.' + method,
|
||||||
objectId: mainFrame.guid,
|
objectId: mainFrame.guid,
|
||||||
pageId: mainFrame._page.guid,
|
pageId: mainFrame._page.guid,
|
||||||
frameId: mainFrame.guid,
|
frameId: mainFrame.guid,
|
||||||
|
|
|
||||||
|
|
@ -1526,7 +1526,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT
|
||||||
setOffline(params: BrowserContextSetOfflineParams, metadata?: CallMetadata): Promise<BrowserContextSetOfflineResult>;
|
setOffline(params: BrowserContextSetOfflineParams, metadata?: CallMetadata): Promise<BrowserContextSetOfflineResult>;
|
||||||
storageState(params?: BrowserContextStorageStateParams, metadata?: CallMetadata): Promise<BrowserContextStorageStateResult>;
|
storageState(params?: BrowserContextStorageStateParams, metadata?: CallMetadata): Promise<BrowserContextStorageStateResult>;
|
||||||
pause(params?: BrowserContextPauseParams, metadata?: CallMetadata): Promise<BrowserContextPauseResult>;
|
pause(params?: BrowserContextPauseParams, metadata?: CallMetadata): Promise<BrowserContextPauseResult>;
|
||||||
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: CallMetadata): Promise<BrowserContextRecorderSupplementEnableResult>;
|
enableRecorder(params: BrowserContextEnableRecorderParams, metadata?: CallMetadata): Promise<BrowserContextEnableRecorderResult>;
|
||||||
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise<BrowserContextNewCDPSessionResult>;
|
newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise<BrowserContextNewCDPSessionResult>;
|
||||||
harStart(params: BrowserContextHarStartParams, metadata?: CallMetadata): Promise<BrowserContextHarStartResult>;
|
harStart(params: BrowserContextHarStartParams, metadata?: CallMetadata): Promise<BrowserContextHarStartResult>;
|
||||||
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
|
harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise<BrowserContextHarExportResult>;
|
||||||
|
|
@ -1766,9 +1766,10 @@ export type BrowserContextStorageStateResult = {
|
||||||
export type BrowserContextPauseParams = {};
|
export type BrowserContextPauseParams = {};
|
||||||
export type BrowserContextPauseOptions = {};
|
export type BrowserContextPauseOptions = {};
|
||||||
export type BrowserContextPauseResult = void;
|
export type BrowserContextPauseResult = void;
|
||||||
export type BrowserContextRecorderSupplementEnableParams = {
|
export type BrowserContextEnableRecorderParams = {
|
||||||
language?: string,
|
language?: string,
|
||||||
mode?: 'inspecting' | 'recording',
|
mode?: 'inspecting' | 'recording',
|
||||||
|
codegenMode?: 'actions' | 'trace-events',
|
||||||
pauseOnNextStatement?: boolean,
|
pauseOnNextStatement?: boolean,
|
||||||
testIdAttributeName?: string,
|
testIdAttributeName?: string,
|
||||||
launchOptions?: any,
|
launchOptions?: any,
|
||||||
|
|
@ -1778,9 +1779,10 @@ export type BrowserContextRecorderSupplementEnableParams = {
|
||||||
outputFile?: string,
|
outputFile?: string,
|
||||||
omitCallTracking?: boolean,
|
omitCallTracking?: boolean,
|
||||||
};
|
};
|
||||||
export type BrowserContextRecorderSupplementEnableOptions = {
|
export type BrowserContextEnableRecorderOptions = {
|
||||||
language?: string,
|
language?: string,
|
||||||
mode?: 'inspecting' | 'recording',
|
mode?: 'inspecting' | 'recording',
|
||||||
|
codegenMode?: 'actions' | 'trace-events',
|
||||||
pauseOnNextStatement?: boolean,
|
pauseOnNextStatement?: boolean,
|
||||||
testIdAttributeName?: string,
|
testIdAttributeName?: string,
|
||||||
launchOptions?: any,
|
launchOptions?: any,
|
||||||
|
|
@ -1790,7 +1792,7 @@ export type BrowserContextRecorderSupplementEnableOptions = {
|
||||||
outputFile?: string,
|
outputFile?: string,
|
||||||
omitCallTracking?: boolean,
|
omitCallTracking?: boolean,
|
||||||
};
|
};
|
||||||
export type BrowserContextRecorderSupplementEnableResult = void;
|
export type BrowserContextEnableRecorderResult = void;
|
||||||
export type BrowserContextNewCDPSessionParams = {
|
export type BrowserContextNewCDPSessionParams = {
|
||||||
page?: PageChannel,
|
page?: PageChannel,
|
||||||
frame?: FrameChannel,
|
frame?: FrameChannel,
|
||||||
|
|
|
||||||
|
|
@ -1187,7 +1187,7 @@ BrowserContext:
|
||||||
pause:
|
pause:
|
||||||
experimental: True
|
experimental: True
|
||||||
|
|
||||||
recorderSupplementEnable:
|
enableRecorder:
|
||||||
experimental: True
|
experimental: True
|
||||||
parameters:
|
parameters:
|
||||||
language: string?
|
language: string?
|
||||||
|
|
@ -1196,6 +1196,11 @@ BrowserContext:
|
||||||
literals:
|
literals:
|
||||||
- inspecting
|
- inspecting
|
||||||
- recording
|
- recording
|
||||||
|
codegenMode:
|
||||||
|
type: enum?
|
||||||
|
literals:
|
||||||
|
- actions
|
||||||
|
- trace-events
|
||||||
pauseOnNextStatement: boolean?
|
pauseOnNextStatement: boolean?
|
||||||
testIdAttributeName: string?
|
testIdAttributeName: string?
|
||||||
launchOptions: json?
|
launchOptions: json?
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,14 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI
|
||||||
}
|
}
|
||||||
set.add(traceUrl);
|
set.add(traceUrl);
|
||||||
|
|
||||||
|
const isRecorderMode = traceUrl.includes('/recorder-trace-');
|
||||||
|
|
||||||
const traceModel = new TraceModel();
|
const traceModel = new TraceModel();
|
||||||
try {
|
try {
|
||||||
// Allow 10% to hop from sw to page.
|
// Allow 10% to hop from sw to page.
|
||||||
const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]);
|
const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]);
|
||||||
const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress);
|
const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress);
|
||||||
await traceModel.load(backend, unzipProgress);
|
await traceModel.load(backend, isRecorderMode, unzipProgress);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils';
|
import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils';
|
||||||
import type { ContextEntry } from './entries';
|
import type { ActionEntry, ContextEntry } from './entries';
|
||||||
import { createEmptyContext } from './entries';
|
import { createEmptyContext } from './entries';
|
||||||
import { SnapshotStorage } from './snapshotStorage';
|
import { SnapshotStorage } from './snapshotStorage';
|
||||||
import { TraceModernizer } from './traceModernizer';
|
import { TraceModernizer } from './traceModernizer';
|
||||||
|
|
@ -38,7 +38,7 @@ export class TraceModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(backend: TraceModelBackend, unzipProgress: (done: number, total: number) => void) {
|
async load(backend: TraceModelBackend, isRecorderMode: boolean, unzipProgress: (done: number, total: number) => void) {
|
||||||
this._backend = backend;
|
this._backend = backend;
|
||||||
|
|
||||||
const ordinals: string[] = [];
|
const ordinals: string[] = [];
|
||||||
|
|
@ -72,7 +72,8 @@ export class TraceModel {
|
||||||
modernizer.appendTrace(network);
|
modernizer.appendTrace(network);
|
||||||
unzipProgress(++done, total);
|
unzipProgress(++done, total);
|
||||||
|
|
||||||
contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime);
|
const actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime);
|
||||||
|
contextEntry.actions = isRecorderMode ? collapseActionsForRecorder(actions) : actions;
|
||||||
|
|
||||||
if (!backend.isLive()) {
|
if (!backend.isLive()) {
|
||||||
// Terminate actions w/o after event gracefully.
|
// Terminate actions w/o after event gracefully.
|
||||||
|
|
@ -133,3 +134,19 @@ function stripEncodingFromContentType(contentType: string) {
|
||||||
return charset[1];
|
return charset[1];
|
||||||
return contentType;
|
return contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collapseActionsForRecorder(actions: ActionEntry[]): ActionEntry[] {
|
||||||
|
const result: ActionEntry[] = [];
|
||||||
|
for (const action of actions) {
|
||||||
|
const lastAction = result[result.length - 1];
|
||||||
|
const isSameAction = lastAction && lastAction.method === action.method && lastAction.pageId === action.pageId;
|
||||||
|
const isSameSelector = lastAction && 'selector' in lastAction.params && 'selector' in action.params && action.params.selector === lastAction.params.selector;
|
||||||
|
const shouldMerge = isSameAction && (action.method === 'goto' || (action.method === 'fill' && isSameSelector));
|
||||||
|
if (!shouldMerge) {
|
||||||
|
result.push(action);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result[result.length - 1] = action;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import * as playwrightLibrary from 'playwright-core';
|
||||||
|
|
||||||
export type TestModeWorkerOptions = {
|
export type TestModeWorkerOptions = {
|
||||||
mode: TestModeName;
|
mode: TestModeName;
|
||||||
|
codegenMode: 'trace-events' | 'actions';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestModeTestFixtures = {
|
export type TestModeTestFixtures = {
|
||||||
|
|
@ -48,6 +49,7 @@ export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOpti
|
||||||
await run(playwright);
|
await run(playwright);
|
||||||
await testMode.teardown();
|
await testMode.teardown();
|
||||||
}, { scope: 'worker' }],
|
}, { scope: 'worker' }],
|
||||||
|
codegenMode: ['actions', { scope: 'worker', option: true }],
|
||||||
|
|
||||||
toImplInWorkerScope: [async ({ playwright }, use) => {
|
toImplInWorkerScope: [async ({ playwright }, use) => {
|
||||||
await use((playwright as any)._toImpl);
|
await use((playwright as any)._toImpl);
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
|
||||||
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[], errors: string[] }> {
|
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[], errors: string[] }> {
|
||||||
const backend = new TraceBackend(file);
|
const backend = new TraceBackend(file);
|
||||||
const traceModel = new TraceModel();
|
const traceModel = new TraceModel();
|
||||||
await traceModel.load(backend, () => {});
|
await traceModel.load(backend, false, () => {});
|
||||||
const model = new MultiTraceModel(traceModel.contextEntries);
|
const model = new MultiTraceModel(traceModel.contextEntries);
|
||||||
const { rootItem } = buildActionTree(model.actions);
|
const { rootItem } = buildActionTree(model.actions);
|
||||||
const actionTree: string[] = [];
|
const actionTree: string[] = [];
|
||||||
|
|
|
||||||
|
|
@ -337,7 +337,8 @@ await page.GetByRole(AriaRole.Button, new() { Name = "click me" }).ClickAsync();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should record open in a new tab with url', async ({ openRecorder, browserName }) => {
|
test('should record open in a new tab with url', async ({ openRecorder, browserName, codegenMode }) => {
|
||||||
|
test.skip(codegenMode === 'trace-events');
|
||||||
const { page, recorder } = await openRecorder();
|
const { page, recorder } = await openRecorder();
|
||||||
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
|
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
|
||||||
|
|
||||||
|
|
@ -490,7 +491,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
||||||
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`);
|
await recorder.waitForOutput('JavaScript', `await page.goto('${server.PREFIX}/page2.html');`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should --save-trace', async ({ runCLI }, testInfo) => {
|
test('should --save-trace', async ({ runCLI, codegenMode }, testInfo) => {
|
||||||
|
test.skip(codegenMode === 'trace-events');
|
||||||
const traceFileName = testInfo.outputPath('trace.zip');
|
const traceFileName = testInfo.outputPath('trace.zip');
|
||||||
const cli = runCLI([`--save-trace=${traceFileName}`], {
|
const cli = runCLI([`--save-trace=${traceFileName}`], {
|
||||||
autoExitWhen: ' ',
|
autoExitWhen: ' ',
|
||||||
|
|
@ -499,7 +501,8 @@ await page1.GotoAsync("about:blank?foo");`);
|
||||||
expect(fs.existsSync(traceFileName)).toBeTruthy();
|
expect(fs.existsSync(traceFileName)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => {
|
test('should save assets via SIGINT', async ({ runCLI, platform, codegenMode }, testInfo) => {
|
||||||
|
test.skip(codegenMode === 'trace-events');
|
||||||
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
|
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
|
||||||
|
|
||||||
const traceFileName = testInfo.outputPath('trace.zip');
|
const traceFileName = testInfo.outputPath('trace.zip');
|
||||||
|
|
|
||||||
|
|
@ -67,17 +67,30 @@ export const test = contextTest.extend<CLITestArgs>({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions }, run, testInfo) => {
|
runCLI: async ({ childProcess, browserName, channel, headless, mode, launchOptions, codegenMode }, run, testInfo) => {
|
||||||
testInfo.skip(mode.startsWith('service'));
|
testInfo.skip(mode.startsWith('service'));
|
||||||
|
|
||||||
await run((cliArgs, { autoExitWhen } = {}) => {
|
await run((cliArgs, { autoExitWhen } = {}) => {
|
||||||
return new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath, autoExitWhen);
|
return new CLIMock(childProcess, {
|
||||||
|
browserName,
|
||||||
|
channel,
|
||||||
|
headless,
|
||||||
|
args: cliArgs,
|
||||||
|
executablePath: launchOptions.executablePath,
|
||||||
|
autoExitWhen,
|
||||||
|
codegenMode
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
openRecorder: async ({ context, recorderPageGetter }, run) => {
|
openRecorder: async ({ context, recorderPageGetter, codegenMode }, run) => {
|
||||||
await run(async (options?: { testIdAttributeName?: string }) => {
|
await run(async (options?: { testIdAttributeName?: string }) => {
|
||||||
await (context as any)._enableRecorder({ language: 'javascript', mode: 'recording', ...options });
|
await (context as any)._enableRecorder({
|
||||||
|
language: 'javascript',
|
||||||
|
mode: 'recording',
|
||||||
|
codegenMode,
|
||||||
|
...options
|
||||||
|
});
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
return { page, recorder: new Recorder(page, await recorderPageGetter()) };
|
return { page, recorder: new Recorder(page, await recorderPageGetter()) };
|
||||||
});
|
});
|
||||||
|
|
@ -205,23 +218,24 @@ class Recorder {
|
||||||
class CLIMock {
|
class CLIMock {
|
||||||
process: TestChildProcess;
|
process: TestChildProcess;
|
||||||
|
|
||||||
constructor(childProcess: CommonFixtures['childProcess'], browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined) {
|
constructor(childProcess: CommonFixtures['childProcess'], options: { browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, autoExitWhen: string | undefined, codegenMode?: 'trace-events' | 'actions'}) {
|
||||||
const nodeArgs = [
|
const nodeArgs = [
|
||||||
'node',
|
'node',
|
||||||
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'),
|
path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'),
|
||||||
'codegen',
|
'codegen',
|
||||||
...args,
|
...options.args,
|
||||||
`--browser=${browserName}`,
|
`--browser=${options.browserName}`,
|
||||||
];
|
];
|
||||||
if (channel)
|
if (options.channel)
|
||||||
nodeArgs.push(`--channel=${channel}`);
|
nodeArgs.push(`--channel=${options.channel}`);
|
||||||
this.process = childProcess({
|
this.process = childProcess({
|
||||||
command: nodeArgs,
|
command: nodeArgs,
|
||||||
env: {
|
env: {
|
||||||
PWTEST_CLI_AUTO_EXIT_WHEN: autoExitWhen,
|
PW_RECORDER_IS_TRACE_VIEWER: options.codegenMode === 'trace-events' ? '1' : undefined,
|
||||||
|
PWTEST_CLI_AUTO_EXIT_WHEN: options.autoExitWhen,
|
||||||
PWTEST_CLI_IS_UNDER_TEST: '1',
|
PWTEST_CLI_IS_UNDER_TEST: '1',
|
||||||
PWTEST_CLI_HEADLESS: headless ? '1' : undefined,
|
PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined,
|
||||||
PWTEST_CLI_EXECUTABLE_PATH: executablePath,
|
PWTEST_CLI_EXECUTABLE_PATH: options.executablePath,
|
||||||
DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*',
|
DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -107,43 +107,63 @@ for (const browserName of browserNames) {
|
||||||
console.error(`Using executable at ${executablePath}`);
|
console.error(`Using executable at ${executablePath}`);
|
||||||
const devtools = process.env.DEVTOOLS === '1';
|
const devtools = process.env.DEVTOOLS === '1';
|
||||||
const testIgnore: RegExp[] = browserNames.filter(b => b !== browserName).map(b => new RegExp(b));
|
const testIgnore: RegExp[] = browserNames.filter(b => b !== browserName).map(b => new RegExp(b));
|
||||||
for (const folder of ['library', 'page']) {
|
|
||||||
config.projects.push({
|
const projectTemplate: typeof config.projects[0] = {
|
||||||
name: `${browserName}-${folder}`,
|
testIgnore,
|
||||||
testDir: path.join(testDir, folder),
|
snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}-${browserName}{ext}`,
|
||||||
testIgnore,
|
use: {
|
||||||
snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}-${browserName}{ext}`,
|
mode,
|
||||||
use: {
|
browserName,
|
||||||
mode,
|
headless: !headed,
|
||||||
browserName,
|
channel,
|
||||||
headless: !headed,
|
video: video ? 'on' : undefined,
|
||||||
channel,
|
launchOptions: {
|
||||||
video: video ? 'on' : undefined,
|
executablePath,
|
||||||
launchOptions: {
|
devtools
|
||||||
executablePath,
|
|
||||||
devtools
|
|
||||||
},
|
|
||||||
trace: trace ? 'on' : undefined,
|
|
||||||
},
|
},
|
||||||
metadata: {
|
trace: trace ? 'on' : undefined,
|
||||||
platform: process.platform,
|
},
|
||||||
docker: !!process.env.INSIDE_DOCKER,
|
metadata: {
|
||||||
headless: (() => {
|
platform: process.platform,
|
||||||
if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW)
|
docker: !!process.env.INSIDE_DOCKER,
|
||||||
return 'headless-new';
|
headless: (() => {
|
||||||
if (headed)
|
if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW)
|
||||||
return 'headed';
|
return 'headless-new';
|
||||||
return 'headless';
|
if (headed)
|
||||||
})(),
|
return 'headed';
|
||||||
browserName,
|
return 'headless';
|
||||||
channel,
|
})(),
|
||||||
mode,
|
browserName,
|
||||||
video: !!video,
|
channel,
|
||||||
trace: !!trace,
|
mode,
|
||||||
clock: process.env.PW_CLOCK ? 'clock-' + process.env.PW_CLOCK : undefined,
|
video: !!video,
|
||||||
},
|
trace: !!trace,
|
||||||
});
|
clock: process.env.PW_CLOCK ? 'clock-' + process.env.PW_CLOCK : undefined,
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
config.projects.push({
|
||||||
|
name: `${browserName}-library`,
|
||||||
|
testDir: path.join(testDir, 'library'),
|
||||||
|
...projectTemplate,
|
||||||
|
});
|
||||||
|
|
||||||
|
config.projects.push({
|
||||||
|
name: `${browserName}-page`,
|
||||||
|
testDir: path.join(testDir, 'page'),
|
||||||
|
...projectTemplate,
|
||||||
|
});
|
||||||
|
|
||||||
|
config.projects.push({
|
||||||
|
name: `${browserName}-codegen-mode-trace`,
|
||||||
|
testDir: path.join(testDir, 'library'),
|
||||||
|
testMatch: '**/cli-codegen-*.spec.ts',
|
||||||
|
...projectTemplate,
|
||||||
|
use: {
|
||||||
|
...projectTemplate.use,
|
||||||
|
codegenMode: 'trace-events',
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue