From 2af7395d6dd7f6023460a666575bc3914f874fd1 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 18 Sep 2024 10:09:20 -0700 Subject: [PATCH] chore: iterate towards recording into trace (2) --- .../dispatchers/browserContextDispatcher.ts | 1 - .../src/server/recorder/contextRecorder.ts | 4 +- .../src/server/recorder/recorderCollection.ts | 17 +-- .../src/server/recorder/recorderUtils.ts | 136 ++++++++++++++---- .../src/server/trace/recorder/tracing.ts | 34 ++++- packages/recorder/src/main.tsx | 6 +- 6 files changed, 150 insertions(+), 48 deletions(-) diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index c2d5d8e1f604e..4ab4c0779f88c 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -302,7 +302,6 @@ export class BrowserContextDispatcher extends Dispatcher { + collection.on('change', (actions: ActionInContext[]) => { this._recorderSources = []; for (const languageGenerator of this._orderedLanguages) { - const { header, footer, actionTexts, text } = generateCode(collection.actions(), languageGenerator, languageGeneratorOptions); + const { header, footer, actionTexts, text } = generateCode(actions, languageGenerator, languageGeneratorOptions); const source: Source = { isRecorded: true, label: languageGenerator.name, diff --git a/packages/playwright-core/src/server/recorder/recorderCollection.ts b/packages/playwright-core/src/server/recorder/recorderCollection.ts index e67865a2dca24..5b0d0b5b9e00a 100644 --- a/packages/playwright-core/src/server/recorder/recorderCollection.ts +++ b/packages/playwright-core/src/server/recorder/recorderCollection.ts @@ -38,6 +38,13 @@ export class RecorderCollection extends EventEmitter { this._context = context; this._enabled = enabled; this._pageAliases = pageAliases; + + if (process.env.PW_RECORDER_IS_TRACE_VIEWER) { + this._context.tracing.onMemoryEvents(events => { + this._actions = traceEventsToAction(events); + this._fireChange(); + }); + } } restart() { @@ -45,12 +52,6 @@ export class RecorderCollection extends EventEmitter { this._fireChange(); } - actions() { - if (!process.env.PW_RECORDER_IS_TRACE_VIEWER) - return collapseActions(this._actions); - return collapseActions(traceEventsToAction(this._context.tracing.inMemoryEvents())); - } - setEnabled(enabled: boolean) { this._enabled = enabled; } @@ -125,12 +126,12 @@ export class RecorderCollection extends EventEmitter { if (this._actions.length) { this._actions[this._actions.length - 1].action.signals.push(signal); - this.emit('change'); + this._fireChange(); return; } } private _fireChange() { - this.emit('change'); + this.emit('change', collapseActions(this._actions)); } } diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index 27c212c6e5b0e..29186e9f96601 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -26,6 +26,7 @@ import { fromKeyboardModifiers, toKeyboardModifiers } from '../codegen/language' import { serializeExpectedTextValues } from '../../utils/expectUtils'; import { createGuid, monotonicTime } from '../../utils'; import { serializeValue } from '../../protocol/serializers'; +import type { SmartKeyboardModifier } from '../types'; export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { let title = metadata.apiName || metadata.method; @@ -213,62 +214,119 @@ export function callMetadataForAction(pageAliases: Map, actionInCo export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext[] { const result: ActionInContext[] = []; + const pageAliases = new Map(); + for (const event of events) { - if (event.type !== 'before') + if (event.type === 'event' && event.class === 'BrowserContext' && event.method === 'page') { + const pageAlias = 'page' + pageAliases.size; + pageAliases.set(event.params.pageId, pageAlias); + const lastAction = result[result.length - 1]; + lastAction.action.signals.push({ + name: 'popup', + popupAlias: pageAlias, + }); + result.push({ + frame: { pageAlias, framePath: [] }, + action: { + name: 'openPage', + url: '', + signals: [], + }, + timestamp: event.time, + }); + continue; + } + + if (event.type === 'event' && event.class === 'BrowserContext' && event.method === 'pageClosed') { + const pageAlias = pageAliases.get(event.params.pageId) || 'page'; + result.push({ + frame: { pageAlias, framePath: [] }, + action: { + name: 'closePage', + signals: [], + }, + timestamp: event.time, + }); + continue; + } + + if (event.type !== 'before' || !event.pageId) continue; if (!event.stepId?.startsWith('recorder@')) continue; - if (event.method === 'goto') { + const { method, params: untypedParams, pageId } = event; + + let pageAlias = pageAliases.get(pageId); + if (!pageAlias) { + pageAlias = 'page'; + pageAliases.set(pageId, pageAlias); result.push({ - frame: { pageAlias: 'page', framePath: [] }, + frame: { pageAlias, framePath: [] }, + action: { + name: 'openPage', + url: '', + signals: [], + }, + timestamp: event.startTime, + }); + } + + if (method === 'goto') { + const params = untypedParams as channels.FrameGotoParams; + result.push({ + frame: { pageAlias, framePath: [] }, action: { name: 'navigate', - url: event.params.url, + url: params.url, signals: [], }, timestamp: event.startTime, }); continue; } - if (event.method === 'click') { + + if (method === 'click') { + const params = untypedParams as channels.FrameClickParams; result.push({ - frame: { pageAlias: 'page', framePath: [] }, + frame: { pageAlias, framePath: [] }, action: { name: 'click', - selector: event.params.selector, + selector: params.selector, signals: [], - button: event.params.button, - modifiers: fromKeyboardModifiers(event.params.modifiers), - clickCount: event.params.clickCount, - position: event.params.position, + button: params.button || 'left', + modifiers: fromKeyboardModifiers(params.modifiers), + clickCount: params.clickCount || 1, + position: params.position, }, timestamp: event.startTime }); continue; } - if (event.method === 'fill') { + if (method === 'fill') { + const params = untypedParams as channels.FrameFillParams; result.push({ - frame: { pageAlias: 'page', framePath: [] }, + frame: { pageAlias, framePath: [] }, action: { name: 'fill', - selector: event.params.selector, + selector: params.selector, signals: [], - text: event.params.value, + text: params.value, }, timestamp: event.startTime }); continue; } - if (event.method === 'press') { - const tokens = event.params.key.split('+'); - const modifiers = tokens.slice(0, tokens.length - 1); + if (method === 'press') { + const params = untypedParams as channels.FramePressParams; + const tokens = params.key.split('+'); + const modifiers = tokens.slice(0, tokens.length - 1) as SmartKeyboardModifier[]; const key = tokens[tokens.length - 1]; result.push({ - frame: { pageAlias: 'page', framePath: [] }, + frame: { pageAlias, framePath: [] }, action: { name: 'press', - selector: event.params.selector, + selector: params.selector, signals: [], key, modifiers: fromKeyboardModifiers(modifiers), @@ -277,44 +335,62 @@ export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext }); continue; } - if (event.method === 'check') { + if (method === 'check') { + const params = untypedParams as channels.FrameCheckParams; result.push({ - frame: { pageAlias: 'page', framePath: [] }, + frame: { pageAlias, framePath: [] }, action: { name: 'check', - selector: event.params.selector, + selector: params.selector, signals: [], }, timestamp: event.startTime }); continue; } - if (event.method === 'uncheck') { + if (method === 'uncheck') { + const params = untypedParams as channels.FrameUncheckParams; result.push({ - frame: { pageAlias: 'page', framePath: [] }, + frame: { pageAlias, framePath: [] }, action: { name: 'uncheck', - selector: event.params.selector, + selector: params.selector, signals: [], }, timestamp: event.startTime }); continue; } - if (event.method === 'selectOption') { + if (method === 'selectOption') { + const params = untypedParams as channels.FrameSelectOptionParams; result.push({ - frame: { pageAlias: 'page', framePath: [] }, + frame: { pageAlias, framePath: [] }, action: { name: 'select', - selector: event.params.selector, + selector: params.selector, signals: [], - options: event.params.options.map((option: any) => option.value), + options: (params.options || []).map(option => option.value!), + }, + timestamp: event.startTime + }); + continue; + } + if (method === 'setInputFiles') { + const params = untypedParams as channels.FrameSetInputFilesParams; + result.push({ + frame: { pageAlias, framePath: [] }, + action: { + name: 'setInputFiles', + selector: params.selector, + signals: [], + files: params.localPaths || [], }, timestamp: event.startTime }); continue; } } + 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 25437e53a26f4..44e6bd7f1f090 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -46,7 +46,6 @@ export type TracerOptions = { snapshots?: boolean; screenshots?: boolean; live?: boolean; - inMemory?: boolean; }; type RecordingState = { @@ -81,6 +80,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps private _contextCreatedEvent: trace.ContextCreatedTraceEvent; private _pendingHarEntries = new Set(); private _inMemoryEvents: trace.TraceEvent[] | undefined; + private _inMemoryEventsCallback: ((events: trace.TraceEvent[]) => void) | undefined; constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) { super(context, 'tracing'); @@ -155,7 +155,6 @@ 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 }> { @@ -196,8 +195,9 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps return { traceName: this._state.traceName }; } - inMemoryEvents(): trace.TraceEvent[] { - return this._inMemoryEvents || []; + onMemoryEvents(callback: (events: trace.TraceEvent[]) => void) { + this._inMemoryEventsCallback = callback; + this._inMemoryEvents = []; } private _startScreencast() { @@ -454,6 +454,28 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._appendTraceEvent(event); } + onPageOpen(page: Page) { + const event: trace.EventTraceEvent = { + type: 'event', + time: monotonicTime(), + class: 'BrowserContext', + method: 'page', + params: { pageId: page.guid, openerPageId: page.opener()?.guid }, + }; + this._appendTraceEvent(event); + } + + onPageClose(page: Page) { + const event: trace.EventTraceEvent = { + type: 'event', + time: monotonicTime(), + class: 'BrowserContext', + method: 'pageClosed', + params: { pageId: page.guid }, + }; + this._appendTraceEvent(event); + } + private _onPageError(error: Error, page: Page) { const event: trace.EventTraceEvent = { type: 'event', @@ -494,8 +516,10 @@ 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) + if (this._inMemoryEvents) { this._inMemoryEvents.push(event); + this._inMemoryEventsCallback?.(this._inMemoryEvents); + } } private _appendResource(sha1: string, buffer: Buffer) { diff --git a/packages/recorder/src/main.tsx b/packages/recorder/src/main.tsx index 2f7ea3ac4c111..61ac9da67fb7e 100644 --- a/packages/recorder/src/main.tsx +++ b/packages/recorder/src/main.tsx @@ -27,7 +27,10 @@ export const Main: React.FC = ({ const [mode, setMode] = React.useState('none'); window.playwrightSetMode = setMode; - window.playwrightSetSources = setSources; + window.playwrightSetSources = React.useCallback((sources: Source[]) => { + setSources(sources); + window.playwrightSourcesEchoForTest = sources; + }, []); window.playwrightSetPaused = setPaused; window.playwrightUpdateLogs = callLogs => { setLog(log => { @@ -40,6 +43,5 @@ export const Main: React.FC = ({ }); }; - window.playwrightSourcesEchoForTest = sources; return ; };