From 418d1c0c5557c5ccabd87c440146ef9c0c5987aa Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 20 Sep 2024 15:25:49 -0700 Subject: [PATCH] chore: allow starting recorder in traceviewer (#32741) --- packages/playwright-core/src/cli/program.ts | 1 + .../src/client/browserContext.ts | 13 +-- .../playwright-core/src/protocol/validator.ts | 5 +- .../src/server/browserContext.ts | 6 +- .../src/server/debugController.ts | 2 +- .../dispatchers/browserContextDispatcher.ts | 13 +-- .../playwright-core/src/server/recorder.ts | 21 +++-- .../src/server/recorder/contextRecorder.ts | 6 +- .../src/server/recorder/recorderCollection.ts | 4 +- .../src/server/recorder/recorderUtils.ts | 3 +- packages/protocol/src/channels.ts | 10 +- packages/protocol/src/protocol.yml | 7 +- packages/trace-viewer/src/sw.ts | 4 +- packages/trace-viewer/src/traceModel.ts | 23 ++++- tests/config/testModeFixtures.ts | 2 + tests/config/utils.ts | 2 +- tests/library/inspector/cli-codegen-2.spec.ts | 9 +- tests/library/inspector/inspectorTest.ts | 38 +++++--- tests/library/playwright.config.ts | 92 +++++++++++-------- 19 files changed, 159 insertions(+), 102 deletions(-) diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 9d7abf7b9232c..2787bc30b4df1 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -574,6 +574,7 @@ async function codegen(options: Options & { target: string, output?: string, tes device: options.device, saveStorage: options.saveStorage, mode: 'recording', + codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions', testIdAttributeName, outputFile: outputFile ? path.resolve(outputFile) : undefined, }); diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 6b3de08c8e97a..a07accb6d3e13 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -492,17 +492,8 @@ export class BrowserContext extends ChannelOwner await this._closedPromise; } - async _enableRecorder(params: { - language: string, - launchOptions?: LaunchOptions, - contextOptions?: BrowserContextOptions, - device?: string, - saveStorage?: string, - mode?: 'recording' | 'inspecting', - testIdAttributeName?: string, - outputFile?: string, - }) { - await this._channel.recorderSupplementEnable(params); + async _enableRecorder(params: channels.BrowserContextEnableRecorderParams) { + await this._channel.enableRecorder(params); } } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index dcf0433b1cf38..9244213c50ebd 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -965,9 +965,10 @@ scheme.BrowserContextStorageStateResult = tObject({ }); scheme.BrowserContextPauseParams = tOptional(tObject({})); scheme.BrowserContextPauseResult = tOptional(tObject({})); -scheme.BrowserContextRecorderSupplementEnableParams = tObject({ +scheme.BrowserContextEnableRecorderParams = tObject({ language: tOptional(tString), mode: tOptional(tEnum(['inspecting', 'recording'])), + codegenMode: tOptional(tEnum(['actions', 'trace-events'])), pauseOnNextStatement: tOptional(tBoolean), testIdAttributeName: tOptional(tString), launchOptions: tOptional(tAny), @@ -977,7 +978,7 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({ outputFile: tOptional(tString), omitCallTracking: tOptional(tBoolean), }); -scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({})); +scheme.BrowserContextEnableRecorderResult = tOptional(tObject({})); scheme.BrowserContextNewCDPSessionParams = tObject({ page: tOptional(tChannel(['Page'])), frame: tOptional(tChannel(['Frame'])), diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 499356ca49506..c67c4c6db6e90 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -131,15 +131,15 @@ export abstract class BrowserContext extends SdkObject { // When PWDEBUG=1, show inspector for each context. 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. if (this._debugger.isPaused()) - Recorder.showInspector(this, RecorderApp.factory(this)); + Recorder.showInspectorNoReply(this, RecorderApp.factory(this)); this._debugger.on(Debugger.Events.PausedStateChanged, () => { if (this._debugger.isPaused()) - Recorder.showInspector(this, RecorderApp.factory(this)); + Recorder.showInspectorNoReply(this, RecorderApp.factory(this)); }); if (debugMode() === 'console') diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index 2a950d7c6aa64..53c6c3d99e904 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -197,7 +197,7 @@ export class DebugController extends SdkObject { const contexts = new Set(); for (const page of this._playwright.allPages()) 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[]; } diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index ae0b722dfc7c3..9df655e6561f7 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -41,7 +41,6 @@ import { serializeError } from '../errors'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer'; import { RecorderApp } from '../recorder/recorderApp'; -import type { IRecorderAppFactory } from '../recorder/recorderFrontend'; import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { @@ -301,21 +300,19 @@ export class BrowserContextDispatcher extends Dispatcher { - let factory: IRecorderAppFactory; - if (process.env.PW_RECORDER_IS_TRACE_VIEWER) { - factory = RecorderInTraceViewer.factory(this._context); + async enableRecorder(params: channels.BrowserContextEnableRecorderParams): Promise { + if (params.codegenMode === 'trace-events') { await this._context.tracing.start({ name: 'trace', snapshots: true, - screenshots: false, + screenshots: true, live: true, }); await this._context.tracing.startChunk({ name: 'trace', title: 'trace' }); + await Recorder.show('trace-events', this._context, RecorderInTraceViewer.factory(this._context), params); } 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) { diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index ddaa035811e6a..19776c8a298a9 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -45,32 +45,35 @@ export class Recorder implements InstrumentationListener, IRecorder { private _omitCallTracking = false; private _currentLanguage: Language; - static showInspector(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) { - const params: channels.BrowserContextRecorderSupplementEnableParams = {}; + static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) { if (isUnderTest()) 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 { + 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 { let recorderPromise = (context as any)[recorderSymbol] as Promise; if (!recorderPromise) { - recorderPromise = Recorder._create(context, recorderAppFactory, params); + recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params); (context as any)[recorderSymbol] = recorderPromise; } return recorderPromise; } - private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise { - const recorder = new Recorder(context, params); + private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise { + const recorder = new Recorder(codegenMode, context, params); const recorderApp = await recorderAppFactory(recorder); await recorder._install(recorderApp); return recorder; } - constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { + constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) { this._mode = params.mode || 'none'; - this._contextRecorder = new ContextRecorder(context, params, {}); + this._contextRecorder = new ContextRecorder(codegenMode, context, params, {}); this._context = context; this._omitCallTracking = !!params.omitCallTracking; this._debugger = context.debugger(); diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts index 856305f300ba3..db5f38703162b 100644 --- a/packages/playwright-core/src/server/recorder/contextRecorder.ts +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -48,14 +48,14 @@ export class ContextRecorder extends EventEmitter { private _lastDialogOrdinal = -1; private _lastDownloadOrdinal = -1; private _context: BrowserContext; - private _params: channels.BrowserContextRecorderSupplementEnableParams; + private _params: channels.BrowserContextEnableRecorderParams; private _delegate: ContextRecorderDelegate; private _recorderSources: Source[]; private _throttledOutputFile: ThrottledFile | null = null; private _orderedLanguages: LanguageGenerator[] = []; private _listeners: RegisteredListener[] = []; - constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams, delegate: ContextRecorderDelegate) { + constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) { super(); this._context = context; this._params = params; @@ -73,7 +73,7 @@ export class ContextRecorder extends EventEmitter { 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[]) => { this._recorderSources = []; for (const languageGenerator of this._orderedLanguages) { diff --git a/packages/playwright-core/src/server/recorder/recorderCollection.ts b/packages/playwright-core/src/server/recorder/recorderCollection.ts index 5b0d0b5b9e00a..ad00f8306b463 100644 --- a/packages/playwright-core/src/server/recorder/recorderCollection.ts +++ b/packages/playwright-core/src/server/recorder/recorderCollection.ts @@ -33,13 +33,13 @@ export class RecorderCollection extends EventEmitter { private _pageAliases: Map; private _context: BrowserContext; - constructor(context: BrowserContext, pageAliases: Map, enabled: boolean) { + constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, pageAliases: Map, enabled: boolean) { super(); this._context = context; this._enabled = enabled; this._pageAliases = pageAliases; - if (process.env.PW_RECORDER_IS_TRACE_VIEWER) { + if (codegenMode === 'trace-events') { this._context.tracing.onMemoryEvents(events => { this._actions = traceEventsToAction(events); this._fireChange(); diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index d2ed8208769dd..3cde57052a19d 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -193,13 +193,12 @@ export function traceParamsForAction(actionInContext: ActionInContext): { method export function callMetadataForAction(pageAliases: Map, actionInContext: ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } { const mainFrame = mainFrameForAction(pageAliases, actionInContext); - const { action } = actionInContext; const { method, params } = traceParamsForAction(actionInContext); const callMetadata: CallMetadata = { id: `call@${createGuid()}`, stepId: `recorder@${createGuid()}`, - apiName: 'frame.' + action.name, + apiName: 'page.' + method, objectId: mainFrame.guid, pageId: mainFrame._page.guid, frameId: mainFrame.guid, diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index d7df4e01575a2..7a3584a9684ee 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1526,7 +1526,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT setOffline(params: BrowserContextSetOfflineParams, metadata?: CallMetadata): Promise; storageState(params?: BrowserContextStorageStateParams, metadata?: CallMetadata): Promise; pause(params?: BrowserContextPauseParams, metadata?: CallMetadata): Promise; - recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: CallMetadata): Promise; + enableRecorder(params: BrowserContextEnableRecorderParams, metadata?: CallMetadata): Promise; newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise; harStart(params: BrowserContextHarStartParams, metadata?: CallMetadata): Promise; harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise; @@ -1766,9 +1766,10 @@ export type BrowserContextStorageStateResult = { export type BrowserContextPauseParams = {}; export type BrowserContextPauseOptions = {}; export type BrowserContextPauseResult = void; -export type BrowserContextRecorderSupplementEnableParams = { +export type BrowserContextEnableRecorderParams = { language?: string, mode?: 'inspecting' | 'recording', + codegenMode?: 'actions' | 'trace-events', pauseOnNextStatement?: boolean, testIdAttributeName?: string, launchOptions?: any, @@ -1778,9 +1779,10 @@ export type BrowserContextRecorderSupplementEnableParams = { outputFile?: string, omitCallTracking?: boolean, }; -export type BrowserContextRecorderSupplementEnableOptions = { +export type BrowserContextEnableRecorderOptions = { language?: string, mode?: 'inspecting' | 'recording', + codegenMode?: 'actions' | 'trace-events', pauseOnNextStatement?: boolean, testIdAttributeName?: string, launchOptions?: any, @@ -1790,7 +1792,7 @@ export type BrowserContextRecorderSupplementEnableOptions = { outputFile?: string, omitCallTracking?: boolean, }; -export type BrowserContextRecorderSupplementEnableResult = void; +export type BrowserContextEnableRecorderResult = void; export type BrowserContextNewCDPSessionParams = { page?: PageChannel, frame?: FrameChannel, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 9532e9f07ff5a..8c77beee884b1 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1187,7 +1187,7 @@ BrowserContext: pause: experimental: True - recorderSupplementEnable: + enableRecorder: experimental: True parameters: language: string? @@ -1196,6 +1196,11 @@ BrowserContext: literals: - inspecting - recording + codegenMode: + type: enum? + literals: + - actions + - trace-events pauseOnNextStatement: boolean? testIdAttributeName: string? launchOptions: json? diff --git a/packages/trace-viewer/src/sw.ts b/packages/trace-viewer/src/sw.ts index 7888aa6a308e6..f581af8049b03 100644 --- a/packages/trace-viewer/src/sw.ts +++ b/packages/trace-viewer/src/sw.ts @@ -47,12 +47,14 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI } set.add(traceUrl); + const isRecorderMode = traceUrl.includes('/recorder-trace-'); + const traceModel = new TraceModel(); try { // Allow 10% to hop from sw to page. const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]); 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) { // eslint-disable-next-line no-console console.error(error); diff --git a/packages/trace-viewer/src/traceModel.ts b/packages/trace-viewer/src/traceModel.ts index 87a3d42491689..893e928691662 100644 --- a/packages/trace-viewer/src/traceModel.ts +++ b/packages/trace-viewer/src/traceModel.ts @@ -15,7 +15,7 @@ */ 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 { SnapshotStorage } from './snapshotStorage'; import { TraceModernizer } from './traceModernizer'; @@ -38,7 +38,7 @@ export class TraceModel { 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; const ordinals: string[] = []; @@ -72,7 +72,8 @@ export class TraceModel { modernizer.appendTrace(network); 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()) { // Terminate actions w/o after event gracefully. @@ -133,3 +134,19 @@ function stripEncodingFromContentType(contentType: string) { return charset[1]; 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; +} diff --git a/tests/config/testModeFixtures.ts b/tests/config/testModeFixtures.ts index 6b6feff7c2913..1231a78260994 100644 --- a/tests/config/testModeFixtures.ts +++ b/tests/config/testModeFixtures.ts @@ -21,6 +21,7 @@ import * as playwrightLibrary from 'playwright-core'; export type TestModeWorkerOptions = { mode: TestModeName; + codegenMode: 'trace-events' | 'actions'; }; export type TestModeTestFixtures = { @@ -48,6 +49,7 @@ export const testModeTest = test.extend { await use((playwright as any)._toImpl); diff --git a/tests/config/utils.ts b/tests/config/utils.ts index d01d40771f003..52a30f2d25247 100644 --- a/tests/config/utils.ts +++ b/tests/config/utils.ts @@ -160,7 +160,7 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso export async function parseTrace(file: string): Promise<{ resources: Map, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[], errors: string[] }> { const backend = new TraceBackend(file); const traceModel = new TraceModel(); - await traceModel.load(backend, () => {}); + await traceModel.load(backend, false, () => {}); const model = new MultiTraceModel(traceModel.contextEntries); const { rootItem } = buildActionTree(model.actions); const actionTree: string[] = []; diff --git a/tests/library/inspector/cli-codegen-2.spec.ts b/tests/library/inspector/cli-codegen-2.spec.ts index b05e061c93f76..21182353a64df 100644 --- a/tests/library/inspector/cli-codegen-2.spec.ts +++ b/tests/library/inspector/cli-codegen-2.spec.ts @@ -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(); await recorder.setContentAndWait(`link`); @@ -490,7 +491,8 @@ await page1.GotoAsync("about:blank?foo");`); 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 cli = runCLI([`--save-trace=${traceFileName}`], { autoExitWhen: ' ', @@ -499,7 +501,8 @@ await page1.GotoAsync("about:blank?foo");`); 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'); const traceFileName = testInfo.outputPath('trace.zip'); diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index 891b87bcaebcb..9116b6a8605ad 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -67,17 +67,30 @@ export const test = contextTest.extend({ }); }, - 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')); 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 (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(); return { page, recorder: new Recorder(page, await recorderPageGetter()) }; }); @@ -205,23 +218,24 @@ class Recorder { class CLIMock { 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 = [ 'node', path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'cli.js'), 'codegen', - ...args, - `--browser=${browserName}`, + ...options.args, + `--browser=${options.browserName}`, ]; - if (channel) - nodeArgs.push(`--channel=${channel}`); + if (options.channel) + nodeArgs.push(`--channel=${options.channel}`); this.process = childProcess({ command: nodeArgs, 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_HEADLESS: headless ? '1' : undefined, - PWTEST_CLI_EXECUTABLE_PATH: executablePath, + PWTEST_CLI_HEADLESS: options.headless ? '1' : undefined, + PWTEST_CLI_EXECUTABLE_PATH: options.executablePath, DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*', }, }); diff --git a/tests/library/playwright.config.ts b/tests/library/playwright.config.ts index 0d4804ad6da14..9c7ce45ddee0e 100644 --- a/tests/library/playwright.config.ts +++ b/tests/library/playwright.config.ts @@ -107,43 +107,63 @@ for (const browserName of browserNames) { console.error(`Using executable at ${executablePath}`); const devtools = process.env.DEVTOOLS === '1'; const testIgnore: RegExp[] = browserNames.filter(b => b !== browserName).map(b => new RegExp(b)); - for (const folder of ['library', 'page']) { - config.projects.push({ - name: `${browserName}-${folder}`, - testDir: path.join(testDir, folder), - testIgnore, - snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}-${browserName}{ext}`, - use: { - mode, - browserName, - headless: !headed, - channel, - video: video ? 'on' : undefined, - launchOptions: { - executablePath, - devtools - }, - trace: trace ? 'on' : undefined, - }, - metadata: { - platform: process.platform, - docker: !!process.env.INSIDE_DOCKER, - headless: (() => { - if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW) - return 'headless-new'; - if (headed) - return 'headed'; - return 'headless'; - })(), - browserName, - channel, - mode, - video: !!video, - trace: !!trace, - clock: process.env.PW_CLOCK ? 'clock-' + process.env.PW_CLOCK : undefined, + + const projectTemplate: typeof config.projects[0] = { + testIgnore, + snapshotPathTemplate: `{testDir}/{testFileDir}/{testFileName}-snapshots/{arg}-${browserName}{ext}`, + use: { + mode, + browserName, + headless: !headed, + channel, + video: video ? 'on' : undefined, + launchOptions: { + executablePath, + devtools }, - }); - } + trace: trace ? 'on' : undefined, + }, + metadata: { + platform: process.platform, + docker: !!process.env.INSIDE_DOCKER, + headless: (() => { + if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW) + return 'headless-new'; + if (headed) + return 'headed'; + return 'headless'; + })(), + browserName, + channel, + mode, + 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;