From 355c88f48f31990e83f683df693081035d093d71 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 17 Sep 2024 18:26:44 -0700 Subject: [PATCH] chore: iterate towards recording into trace (#32646) --- packages/playwright-core/src/cli/program.ts | 4 - .../src/server/codegen/language.ts | 34 +-- .../dispatchers/browserContextDispatcher.ts | 16 +- .../src/server/recorder/contextRecorder.ts | 2 +- .../src/server/recorder/recorderApp.ts | 5 +- .../src/server/recorder/recorderCollection.ts | 27 +- .../src/server/recorder/recorderFrontend.ts | 1 + .../server/recorder/recorderInTraceViewer.ts | 11 +- .../src/server/recorder/recorderUtils.ts | 236 ++++++++++++++++-- .../src/server/trace/recorder/tracing.ts | 11 +- .../src/server/trace/viewer/traceViewer.ts | 1 + packages/trace-viewer/src/traceModel.ts | 1 + packages/trace-viewer/src/ui/recorderView.tsx | 2 + tests/library/inspector/inspectorTest.ts | 2 +- 14 files changed, 289 insertions(+), 64 deletions(-) diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index d8fa8230c6..fb27b14231 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -567,10 +567,6 @@ async function codegen(options: Options & { target: string, output?: string, tes tracesDir, }); dotenv.config({ path: 'playwright.env' }); - if (process.env.PW_RECORDER_IS_TRACE_VIEWER) { - await fs.promises.mkdir(tracesDir, { recursive: true }); - await context.tracing.start({ name: 'trace', _live: true }); - } await context._enableRecorder({ language, launchOptions, diff --git a/packages/playwright-core/src/server/codegen/language.ts b/packages/playwright-core/src/server/codegen/language.ts index 4b1ba99b6f..7ee775b18b 100644 --- a/packages/playwright-core/src/server/codegen/language.ts +++ b/packages/playwright-core/src/server/codegen/language.ts @@ -20,7 +20,6 @@ import type * as types from '../types'; import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types'; export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) { - actions = collapseActions(actions); const header = languageGenerator.generateHeader(options); const footer = languageGenerator.generateFooter(options.saveStorage); const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean); @@ -70,6 +69,23 @@ export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModif return result; } +export function fromKeyboardModifiers(modifiers?: types.SmartKeyboardModifier[]): number { + let result = 0; + if (!modifiers) + return result; + if (modifiers.includes('Alt')) + result |= 1; + if (modifiers.includes('Control')) + result |= 2; + if (modifiers.includes('ControlOrMeta')) + result |= 2; + if (modifiers.includes('Meta')) + result |= 4; + if (modifiers.includes('Shift')) + result |= 8; + return result; +} + export function toClickOptionsForSourceCode(action: actions.ClickAction): types.MouseClickOptions { const modifiers = toKeyboardModifiers(action.modifiers); const options: types.MouseClickOptions = {}; @@ -84,19 +100,3 @@ export function toClickOptionsForSourceCode(action: actions.ClickAction): types. options.position = action.position; return options; } - -function collapseActions(actions: ActionInContext[]): ActionInContext[] { - const result: ActionInContext[] = []; - for (const action of actions) { - const lastAction = result[result.length - 1]; - const isSameAction = lastAction && lastAction.action.name === action.action.name && lastAction.frame.pageAlias === action.frame.pageAlias && lastAction.frame.framePath.join('|') === action.frame.framePath.join('|'); - const isSameSelector = lastAction && 'selector' in lastAction.action && 'selector' in action.action && action.action.selector === lastAction.action.selector; - const shouldMerge = isSameAction && (action.action.name === 'navigate' || (action.action.name === 'fill' && isSameSelector)); - if (!shouldMerge) { - result.push(action); - continue; - } - result[result.length - 1] = action; - } - return result; -} diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 5c8fa550a7..c2d5d8e1f6 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -41,6 +41,7 @@ import { serializeError } from '../errors'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer'; import { RecorderApp } from '../recorder/recorderApp'; +import type { IRecorderAppFactory } from '../recorder/recorderFrontend'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { _type_EventTarget = true; @@ -293,7 +294,20 @@ export class BrowserContextDispatcher extends Dispatcher { - const factory = process.env.PW_RECORDER_IS_TRACE_VIEWER ? RecorderInTraceViewer.factory(this._context) : RecorderApp.factory(this._context); + let factory: IRecorderAppFactory; + if (process.env.PW_RECORDER_IS_TRACE_VIEWER) { + factory = RecorderInTraceViewer.factory(this._context); + await this._context.tracing.start({ + name: 'trace', + snapshots: true, + screenshots: false, + live: true, + inMemory: true, + }); + await this._context.tracing.startChunk({ name: 'trace', title: 'trace' }); + } else { + factory = RecorderApp.factory(this._context); + } await Recorder.show(this._context, factory, params); } diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts index 71a1d3ec75..13f3ae1a62 100644 --- a/packages/playwright-core/src/server/recorder/contextRecorder.ts +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -73,7 +73,7 @@ export class ContextRecorder extends EventEmitter { saveStorage: params.saveStorage, }; - const collection = new RecorderCollection(this._pageAliases, params.mode === 'recording'); + const collection = new RecorderCollection(context, this._pageAliases, params.mode === 'recording'); collection.on('change', () => { this._recorderSources = []; for (const languageGenerator of this._orderedLanguages) { diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index 67d8b6e8dc..5e170b2e3e 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -43,6 +43,7 @@ declare global { } export class EmptyRecorderApp extends EventEmitter implements IRecorderApp { + wsEndpointForTest: undefined; async close(): Promise {} async setPaused(paused: boolean): Promise {} async setMode(mode: Mode): Promise {} @@ -54,7 +55,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp { export class RecorderApp extends EventEmitter implements IRecorderApp { private _page: Page; - readonly wsEndpoint: string | undefined; + readonly wsEndpointForTest: string | undefined; private _recorder: IRecorder; constructor(recorder: IRecorder, page: Page, wsEndpoint: string | undefined) { @@ -62,7 +63,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { this.setMaxListeners(0); this._recorder = recorder; this._page = page; - this.wsEndpoint = wsEndpoint; + this.wsEndpointForTest = wsEndpoint; } async close() { diff --git a/packages/playwright-core/src/server/recorder/recorderCollection.ts b/packages/playwright-core/src/server/recorder/recorderCollection.ts index e9c2b31427..e67865a2dc 100644 --- a/packages/playwright-core/src/server/recorder/recorderCollection.ts +++ b/packages/playwright-core/src/server/recorder/recorderCollection.ts @@ -20,31 +20,35 @@ import type { Page } from '../page'; import type { Signal } from './recorderActions'; import type { ActionInContext } from '../codegen/types'; import { monotonicTime } from '../../utils/time'; -import { callMetadataForAction } from './recorderUtils'; +import { callMetadataForAction, collapseActions, traceEventsToAction } from './recorderUtils'; import { serializeError } from '../errors'; import { performAction } from './recorderRunner'; import type { CallMetadata } from '@protocol/callMetadata'; import { isUnderTest } from '../../utils/debug'; +import type { BrowserContext } from '../browserContext'; export class RecorderCollection extends EventEmitter { private _actions: ActionInContext[] = []; private _enabled: boolean; private _pageAliases: Map; + private _context: BrowserContext; - constructor(pageAliases: Map, enabled: boolean) { + constructor(context: BrowserContext, pageAliases: Map, enabled: boolean) { super(); + this._context = context; this._enabled = enabled; this._pageAliases = pageAliases; - this.restart(); } restart() { this._actions = []; - this.emit('change'); + this._fireChange(); } actions() { - return this._actions; + if (!process.env.PW_RECORDER_IS_TRACE_VIEWER) + return collapseActions(this._actions); + return collapseActions(traceEventsToAction(this._context.tracing.inMemoryEvents())); } setEnabled(enabled: boolean) { @@ -60,7 +64,7 @@ export class RecorderCollection extends EventEmitter { addRecordedAction(actionInContext: ActionInContext) { if (['openPage', 'closePage'].includes(actionInContext.action.name)) { this._actions.push(actionInContext); - this.emit('change'); + this._fireChange(); return; } this._addAction(actionInContext).catch(() => {}); @@ -69,11 +73,16 @@ export class RecorderCollection extends EventEmitter { private async _addAction(actionInContext: ActionInContext, callback?: (callMetadata: CallMetadata) => Promise) { if (!this._enabled) return; + if (actionInContext.action.name === 'openPage' || actionInContext.action.name === 'closePage') { + this._actions.push(actionInContext); + this._fireChange(); + return; + } const { callMetadata, mainFrame } = callMetadataForAction(this._pageAliases, actionInContext); await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata); this._actions.push(actionInContext); - this.emit('change'); + this._fireChange(); const error = await callback?.(callMetadata).catch((e: Error) => e); callMetadata.endTime = monotonicTime(); callMetadata.error = error ? serializeError(error) : undefined; @@ -120,4 +129,8 @@ export class RecorderCollection extends EventEmitter { return; } } + + private _fireChange() { + this.emit('change'); + } } diff --git a/packages/playwright-core/src/server/recorder/recorderFrontend.ts b/packages/playwright-core/src/server/recorder/recorderFrontend.ts index 162c9f9964..d2cdffdca4 100644 --- a/packages/playwright-core/src/server/recorder/recorderFrontend.ts +++ b/packages/playwright-core/src/server/recorder/recorderFrontend.ts @@ -23,6 +23,7 @@ export interface IRecorder { } export interface IRecorderApp extends EventEmitter { + readonly wsEndpointForTest: string | undefined; close(): Promise; setPaused(paused: boolean): Promise; setMode(mode: Mode): Promise; diff --git a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts b/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts index a9fd766141..8f08b969e1 100644 --- a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts +++ b/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts @@ -25,6 +25,7 @@ import { gracefullyProcessExitDoNotHang } from '../../utils/processLauncher'; import type { Transport } from '../../utils/httpServer'; export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp { + readonly wsEndpointForTest: string | undefined; private _recorder: IRecorder; private _transport: Transport; @@ -32,15 +33,16 @@ export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp return async (recorder: IRecorder) => { const transport = new RecorderTransport(); const trace = path.join(context._browser.options.tracesDir, 'trace'); - await openApp(trace, { transport }); - return new RecorderInTraceViewer(context, recorder, transport); + const wsEndpointForTest = await openApp(trace, { transport, headless: !context._browser.options.headful }); + return new RecorderInTraceViewer(context, recorder, transport, wsEndpointForTest); }; } - constructor(context: BrowserContext, recorder: IRecorder, transport: Transport) { + constructor(context: BrowserContext, recorder: IRecorder, transport: Transport, wsEndpointForTest: string | undefined) { super(); this._recorder = recorder; this._transport = transport; + this.wsEndpointForTest = wsEndpointForTest; } async close(): Promise { @@ -72,11 +74,12 @@ export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp } } -async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }) { +async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise { const server = await startTraceViewerServer(options); await installRootRedirect(server, [trace], { ...options, webApp: 'recorder.html' }); const page = await openTraceViewerApp(server.urlPrefix('precise'), 'chromium', options); page.on('close', () => gracefullyProcessExitDoNotHang(0)); + return page.context()._browser.options.wsEndpoint; } class RecorderTransport implements Transport { diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index ac6c970489..27c212c6e5 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -20,9 +20,12 @@ import type { Page } from '../page'; import type { ActionInContext } from '../codegen/types'; import type { Frame } from '../frames'; import type * as actions from './recorderActions'; -import { toKeyboardModifiers } from '../codegen/language'; +import type * as channels from '@protocol/channels'; +import type * as trace from '@trace/trace'; +import { fromKeyboardModifiers, toKeyboardModifiers } from '../codegen/language'; import { serializeExpectedTextValues } from '../../utils/expectUtils'; import { createGuid, monotonicTime } from '../../utils'; +import { serializeValue } from '../../protocol/serializers'; export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { let title = metadata.apiName || metadata.method; @@ -76,57 +79,113 @@ export async function frameForAction(pageAliases: Map, actionInCon return result.frame; } -export function traceParamsForAction(actionInContext: ActionInContext) { +export function traceParamsForAction(actionInContext: ActionInContext): { method: string, params: any } { const { action } = actionInContext; switch (action.name) { - case 'navigate': return { url: action.url }; - case 'openPage': return {}; - case 'closePage': return {}; + case 'navigate': { + const params: channels.FrameGotoParams = { + url: action.url, + }; + return { method: 'goto', params }; + } + case 'openPage': + case 'closePage': + throw new Error('Not reached'); } const selector = buildFullSelector(actionInContext.frame.framePath, action.selector); switch (action.name) { - case 'click': return { selector, clickCount: action.clickCount }; - case 'press': { - const modifiers = toKeyboardModifiers(action.modifiers); - const shortcut = [...modifiers, action.key].join('+'); - return { selector, key: shortcut }; - } - case 'fill': return { selector, text: action.text }; - case 'setInputFiles': return { selector, files: action.files }; - case 'check': return { selector }; - case 'uncheck': return { selector }; - case 'select': return { selector, values: action.options.map(value => ({ value })) }; - case 'assertChecked': { - return { + case 'click': { + const params: channels.FrameClickParams = { selector, - expression: 'to.be.checked', - isNot: !action.checked, + strict: true, + modifiers: toKeyboardModifiers(action.modifiers), + button: action.button, + clickCount: action.clickCount, + position: action.position, }; + return { method: 'click', params }; + } + case 'press': { + const params: channels.FramePressParams = { + selector, + strict: true, + key: [...toKeyboardModifiers(action.modifiers), action.key].join('+'), + }; + return { method: 'press', params }; + } + case 'fill': { + const params: channels.FrameFillParams = { + selector, + strict: true, + value: action.text, + }; + return { method: 'fill', params }; + } + case 'setInputFiles': { + const params: channels.FrameSetInputFilesParams = { + selector, + strict: true, + localPaths: action.files, + }; + return { method: 'setInputFiles', params }; + } + case 'check': { + const params: channels.FrameCheckParams = { + selector, + strict: true, + }; + return { method: 'check', params }; + } + case 'uncheck': { + const params: channels.FrameUncheckParams = { + selector, + strict: true, + }; + return { method: 'uncheck', params }; + } + case 'select': { + const params: channels.FrameSelectOptionParams = { + selector, + strict: true, + options: action.options.map(option => ({ value: option })), + }; + return { method: 'selectOption', params }; + } + case 'assertChecked': { + const params: channels.FrameExpectParams = { + selector: action.selector, + expression: 'to.be.checked', + isNot: action.checked, + }; + return { method: 'expect', params }; } case 'assertText': { - return { + const params: channels.FrameExpectParams = { selector, expression: 'to.have.text', expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }), isNot: false, }; + return { method: 'expect', params }; } case 'assertValue': { - return { + const params: channels.FrameExpectParams = { selector, expression: 'to.have.value', - expectedValue: action.value, + expectedValue: { value: serializeValue(action.value, value => ({ fallThrough: value })), handles: [] }, isNot: false, }; + return { method: 'expect', params }; } case 'assertVisible': { - return { + const params: channels.FrameExpectParams = { selector, expression: 'to.be.visible', isNot: false, }; + return { method: 'expect', params }; } } } @@ -134,8 +193,10 @@ export function traceParamsForAction(actionInContext: ActionInContext) { 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, objectId: mainFrame.guid, pageId: mainFrame._page.guid, @@ -143,9 +204,132 @@ export function callMetadataForAction(pageAliases: Map, actionInCo startTime: monotonicTime(), endTime: 0, type: 'Frame', - method: action.name, - params: traceParamsForAction(actionInContext), + method, + params, log: [], }; return { callMetadata, mainFrame }; } + +export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext[] { + const result: ActionInContext[] = []; + for (const event of events) { + if (event.type !== 'before') + continue; + if (!event.stepId?.startsWith('recorder@')) + continue; + + if (event.method === 'goto') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'navigate', + url: event.params.url, + signals: [], + }, + timestamp: event.startTime, + }); + continue; + } + if (event.method === 'click') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'click', + selector: event.params.selector, + signals: [], + button: event.params.button, + modifiers: fromKeyboardModifiers(event.params.modifiers), + clickCount: event.params.clickCount, + position: event.params.position, + }, + timestamp: event.startTime + }); + continue; + } + if (event.method === 'fill') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'fill', + selector: event.params.selector, + signals: [], + text: event.params.value, + }, + timestamp: event.startTime + }); + continue; + } + if (event.method === 'press') { + const tokens = event.params.key.split('+'); + const modifiers = tokens.slice(0, tokens.length - 1); + const key = tokens[tokens.length - 1]; + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'press', + selector: event.params.selector, + signals: [], + key, + modifiers: fromKeyboardModifiers(modifiers), + }, + timestamp: event.startTime + }); + continue; + } + if (event.method === 'check') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'check', + selector: event.params.selector, + signals: [], + }, + timestamp: event.startTime + }); + continue; + } + if (event.method === 'uncheck') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'uncheck', + selector: event.params.selector, + signals: [], + }, + timestamp: event.startTime + }); + continue; + } + if (event.method === 'selectOption') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'select', + selector: event.params.selector, + signals: [], + options: event.params.options.map((option: any) => option.value), + }, + timestamp: event.startTime + }); + continue; + } + } + return result; +} + +export function collapseActions(actions: ActionInContext[]): ActionInContext[] { + const result: ActionInContext[] = []; + for (const action of actions) { + const lastAction = result[result.length - 1]; + const isSameAction = lastAction && lastAction.action.name === action.action.name && lastAction.frame.pageAlias === action.frame.pageAlias && lastAction.frame.framePath.join('|') === action.frame.framePath.join('|'); + const isSameSelector = lastAction && 'selector' in lastAction.action && 'selector' in action.action && action.action.selector === lastAction.action.selector; + const shouldMerge = isSameAction && (action.action.name === 'navigate' || (action.action.name === 'fill' && isSameSelector)); + if (!shouldMerge) { + result.push(action); + continue; + } + result[result.length - 1] = action; + } + return result; +} diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index b09bbe3134..25437e53a2 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -46,6 +46,7 @@ export type TracerOptions = { snapshots?: boolean; screenshots?: boolean; live?: boolean; + inMemory?: boolean; }; type RecordingState = { @@ -79,6 +80,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps private _allResources = new Set(); private _contextCreatedEvent: trace.ContextCreatedTraceEvent; private _pendingHarEntries = new Set(); + private _inMemoryEvents: trace.TraceEvent[] | undefined; constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) { super(context, 'tracing'); @@ -153,6 +155,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps // Tracing is 10x bigger if we include scripts in every trace. if (options.snapshots) this._harTracer.start({ omitScripts: !options.live }); + this._inMemoryEvents = options.inMemory ? [] : undefined; } async startChunk(options: { name?: string, title?: string } = {}): Promise<{ traceName: string }> { @@ -179,7 +182,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps wallTime: Date.now(), monotonicTime: monotonicTime() }; - this._fs.appendFile(this._state.traceFile, JSON.stringify(event) + '\n'); + this._appendTraceEvent(event); this._context.instrumentation.addListener(this, this._context); this._eventListeners.push( @@ -193,6 +196,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps return { traceName: this._state.traceName }; } + inMemoryEvents(): trace.TraceEvent[] { + return this._inMemoryEvents || []; + } + private _startScreencast() { if (!(this._context instanceof BrowserContext)) return; @@ -487,6 +494,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps // Do not flush (console) events, they are too noisy, unless we are in ui mode (live). const flush = this._state!.options.live || (event.type !== 'event' && event.type !== 'console' && event.type !== 'log'); this._fs.appendFile(this._state!.traceFile, JSON.stringify(visited) + '\n', flush); + if (this._inMemoryEvents) + this._inMemoryEvents.push(event); } private _appendResource(sha1: string, buffer: Buffer) { diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 6ca0319aa3..78848142a8 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -178,6 +178,7 @@ export async function openTraceViewerApp(url: string, browserName: string, optio ...options?.persistentContextOptions, useWebSocket: isUnderTest(), headless: !!options?.headless, + args: process.env.PWTEST_RECORDER_PORT ? [`--remote-debugging-port=${process.env.PWTEST_RECORDER_PORT}`] : [], }, }); diff --git a/packages/trace-viewer/src/traceModel.ts b/packages/trace-viewer/src/traceModel.ts index 1248dde967..87a3d42491 100644 --- a/packages/trace-viewer/src/traceModel.ts +++ b/packages/trace-viewer/src/traceModel.ts @@ -73,6 +73,7 @@ export class TraceModel { unzipProgress(++done, total); contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime); + if (!backend.isLive()) { // Terminate actions w/o after event gracefully. // This would close after hooks event that has not been closed because diff --git a/packages/trace-viewer/src/ui/recorderView.tsx b/packages/trace-viewer/src/ui/recorderView.tsx index 940fd146a9..4d8ec8d297 100644 --- a/packages/trace-viewer/src/ui/recorderView.tsx +++ b/packages/trace-viewer/src/ui/recorderView.tsx @@ -45,6 +45,8 @@ export const RecorderView: React.FunctionComponent = () => { connection.setMode('recording'); }, [connection]); + window.playwrightSourcesEchoForTest = sources; + return
({ await run(async () => { while (!toImpl(context).recorderAppForTest) await new Promise(f => setTimeout(f, 100)); - const wsEndpoint = toImpl(context).recorderAppForTest.wsEndpoint; + const wsEndpoint = toImpl(context).recorderAppForTest.wsEndpointForTest; const browser = await playwrightToAutomateInspector.chromium.connectOverCDP({ wsEndpoint }); const c = browser.contexts()[0]; return c.pages()[0] || await c.waitForEvent('page');