Skip to content

Commit

Permalink
chore: render recorded action list in tv mode (#32841)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Sep 26, 2024
1 parent 5b85c71 commit 1a3d3f6
Show file tree
Hide file tree
Showing 16 changed files with 537 additions and 729 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export class ContextRecorder extends EventEmitter {
saveStorage: params.saveStorage,
};

this._collection = new RecorderCollection(codegenMode, context, this._pageAliases);
this._collection = new RecorderCollection(this._pageAliases);
this._collection.on('change', (actions: actions.ActionInContext[]) => {
this._recorderSources = [];
for (const languageGenerator of this._orderedLanguages) {
Expand Down
17 changes: 4 additions & 13 deletions packages/playwright-core/src/server/recorder/recorderCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,20 @@ import type { Page } from '../page';
import type { Signal } from '../../../../recorder/src/actions';
import type * as actions from '@recorder/actions';
import { monotonicTime } from '../../utils/time';
import { callMetadataForAction, collapseActions, traceEventsToAction } from './recorderUtils';
import { callMetadataForAction, collapseActions } 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: actions.ActionInContext[] = [];
private _enabled = false;
private _pageAliases: Map<Page, string>;
private _context: BrowserContext;

constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, pageAliases: Map<Page, string>) {
constructor(pageAliases: Map<Page, string>) {
super();
this._context = context;
this._pageAliases = pageAliases;

if (codegenMode === 'trace-events') {
this._context.tracing.onMemoryEvents(events => {
this._actions = traceEventsToAction(events);
this._fireChange();
});
}
}

restart() {
Expand Down Expand Up @@ -86,7 +76,8 @@ export class RecorderCollection extends EventEmitter {
const error = await callback?.(callMetadata).catch((e: Error) => e);
callMetadata.endTime = monotonicTime();
callMetadata.error = error ? serializeError(error) : undefined;
await mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata);
// Do not wait for onAfterCall so that performAction returned immediately after the action.
mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata).catch(() => {});
}

signal(pageAlias: string, frame: Frame, signal: Signal) {
Expand Down
285 changes: 4 additions & 281 deletions packages/playwright-core/src/server/recorder/recorderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ import type { Page } from '../page';
import type { Frame } from '../frames';
import type * as actions from '@recorder/actions';
import type * as channels from '@protocol/channels';
import type * as trace from '@trace/trace';
import { fromKeyboardModifiers, toKeyboardModifiers } from '../codegen/language';
import { toKeyboardModifiers } from '../codegen/language';
import { serializeExpectedTextValues } from '../../utils/expectUtils';
import { createGuid, monotonicTime } from '../../utils';
import { parseSerializedValue, serializeValue } from '../../protocol/serializers';
import type { SmartKeyboardModifier } from '../types';
import { createGuid } from '../../utils';
import { serializeValue } from '../../protocol/serializers';

export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
let title = metadata.apiName || metadata.method;
Expand Down Expand Up @@ -201,7 +199,7 @@ export function callMetadataForAction(pageAliases: Map<Page, string>, actionInCo
objectId: mainFrame.guid,
pageId: mainFrame._page.guid,
frameId: mainFrame.guid,
startTime: monotonicTime(),
startTime: actionInContext.timestamp,
endTime: 0,
type: 'Frame',
method,
Expand All @@ -211,281 +209,6 @@ export function callMetadataForAction(pageAliases: Map<Page, string>, actionInCo
return { callMetadata, mainFrame };
}

export function traceEventsToAction(events: trace.TraceEvent[]): actions.ActionInContext[] {
const result: actions.ActionInContext[] = [];
const pageAliases = new Map<string, string>();
let lastDownloadOrdinal = 0;
let lastDialogOrdinal = 0;

const addSignal = (signal: actions.Signal) => {
const lastAction = result[result.length - 1];
if (!lastAction)
return;
lastAction.action.signals.push(signal);
};

for (const event of events) {
if (event.type === 'event' && event.class === 'BrowserContext') {
const { method, params } = event;
if (method === 'page') {
const pageAlias = 'page' + (pageAliases.size || '');
pageAliases.set(params.pageId, pageAlias);
addSignal({
name: 'popup',
popupAlias: pageAlias,
});
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'openPage',
url: '',
signals: [],
},
timestamp: event.time,
});
continue;
}

if (method === 'pageClosed') {
const pageAlias = pageAliases.get(event.params.pageId) || 'page';
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'closePage',
signals: [],
},
timestamp: event.time,
});
continue;
}

if (method === 'download') {
const downloadAlias = lastDownloadOrdinal ? String(lastDownloadOrdinal) : '';
++lastDownloadOrdinal;
addSignal({
name: 'download',
downloadAlias,
});
continue;
}

if (method === 'dialog') {
const dialogAlias = lastDialogOrdinal ? String(lastDialogOrdinal) : '';
++lastDialogOrdinal;
addSignal({
name: 'dialog',
dialogAlias,
});
continue;
}
continue;
}

if (event.type !== 'before' || !event.pageId)
continue;
if (!event.stepId?.startsWith('recorder@'))
continue;

const { method, params: untypedParams, pageId } = event;

let pageAlias = pageAliases.get(pageId);
if (!pageAlias) {
pageAlias = 'page';
pageAliases.set(pageId, pageAlias);
result.push({
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: params.url,
signals: [],
},
timestamp: event.startTime,
});
continue;
}

if (method === 'click') {
const params = untypedParams as channels.FrameClickParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'click',
selector: params.selector,
signals: [],
button: params.button || 'left',
modifiers: fromKeyboardModifiers(params.modifiers),
clickCount: params.clickCount || 1,
position: params.position,
},
timestamp: event.startTime
});
continue;
}
if (method === 'fill') {
const params = untypedParams as channels.FrameFillParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'fill',
selector: params.selector,
signals: [],
text: params.value,
},
timestamp: event.startTime
});
continue;
}
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, framePath: [] },
action: {
name: 'press',
selector: params.selector,
signals: [],
key,
modifiers: fromKeyboardModifiers(modifiers),
},
timestamp: event.startTime
});
continue;
}
if (method === 'check') {
const params = untypedParams as channels.FrameCheckParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'check',
selector: params.selector,
signals: [],
},
timestamp: event.startTime
});
continue;
}
if (method === 'uncheck') {
const params = untypedParams as channels.FrameUncheckParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'uncheck',
selector: params.selector,
signals: [],
},
timestamp: event.startTime
});
continue;
}
if (method === 'selectOption') {
const params = untypedParams as channels.FrameSelectOptionParams;
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'select',
selector: params.selector,
signals: [],
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;
}
if (method === 'expect') {
const params = untypedParams as channels.FrameExpectParams;
if (params.expression === 'to.have.text') {
const entry = params.expectedText?.[0];
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'assertText',
selector: params.selector,
signals: [],
text: entry?.string!,
substring: !!entry?.matchSubstring,
},
timestamp: event.startTime
});
continue;
}

if (params.expression === 'to.have.value') {
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'assertValue',
selector: params.selector,
signals: [],
value: parseSerializedValue(params.expectedValue!.value, params.expectedValue!.handles),
},
timestamp: event.startTime
});
continue;
}

if (params.expression === 'to.be.checked') {
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'assertChecked',
selector: params.selector,
signals: [],
checked: !params.isNot,
},
timestamp: event.startTime
});
continue;
}

if (params.expression === 'to.be.visible') {
result.push({
frame: { pageAlias, framePath: [] },
action: {
name: 'assertVisible',
selector: params.selector,
signals: [],
},
timestamp: event.startTime
});
continue;
}

continue;
}
}

return result;
}

export function collapseActions(actions: actions.ActionInContext[]): actions.ActionInContext[] {
const result: actions.ActionInContext[] = [];
for (const action of actions) {
Expand Down
4 changes: 4 additions & 0 deletions packages/trace-viewer/src/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ ui/

[sw-main.ts]
sw/**


[recorder.tsx]
ui/recorder/**
2 changes: 1 addition & 1 deletion packages/trace-viewer/src/recorder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import '@web/common.css';
import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css';
import * as ReactDOM from 'react-dom/client';
import { RecorderView } from './ui/recorderView';
import { RecorderView } from './ui/recorder/recorderView';

(async () => {
applyTheme();
Expand Down
4 changes: 1 addition & 3 deletions packages/trace-viewer/src/sw/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,12 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI
}
set.add(traceUrl);

const isRecorderMode = traceUrl.includes('/playwright-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, isRecorderMode, unzipProgress);
await traceModel.load(backend, unzipProgress);
} catch (error: any) {
// eslint-disable-next-line no-console
console.error(error);
Expand Down
Loading

0 comments on commit 1a3d3f6

Please sign in to comment.