chore: iterate towards recording into trace (2) (#32693)

This commit is contained in:
Pavel Feldman 2024-09-18 14:39:07 -07:00 committed by GitHub
parent f9d9ad25de
commit 427eca6f7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 150 additions and 48 deletions

View file

@ -302,7 +302,6 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
snapshots: true,
screenshots: false,
live: true,
inMemory: true,
});
await this._context.tracing.startChunk({ name: 'trace', title: 'trace' });
} else {

View file

@ -74,10 +74,10 @@ export class ContextRecorder extends EventEmitter {
};
const collection = new RecorderCollection(context, this._pageAliases, params.mode === 'recording');
collection.on('change', () => {
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,

View file

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

View file

@ -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<Page, string>, actionInCo
export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext[] {
const result: ActionInContext[] = [];
const pageAliases = new Map<string, string>();
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;
}

View file

@ -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<har.Entry>();
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) {

View file

@ -27,7 +27,10 @@ export const Main: React.FC = ({
const [mode, setMode] = React.useState<Mode>('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 <Recorder sources={sources} paused={paused} log={log} mode={mode} />;
};