chore: iterate towards recording into trace (2) (#32693)
This commit is contained in:
parent
f9d9ad25de
commit
427eca6f7e
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue