chore: allow starting recorder in traceviewer (#32741)

This commit is contained in:
Pavel Feldman 2024-09-20 15:25:49 -07:00 committed by GitHub
parent dfb3fdf217
commit 418d1c0c55
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 159 additions and 102 deletions

View file

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

View file

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

View file

@ -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'])),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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[] = [];

View file

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

View file

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

View file

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