chore: update trace event on action merge (#32860)

This commit is contained in:
Pavel Feldman 2024-09-27 21:18:30 -07:00 committed by GitHub
parent 908b0de5d4
commit 11014145ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 67 additions and 49 deletions

View file

@ -139,7 +139,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
});
this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], actions: actions.ActionInContext[] }) => {
this._recorderSources = data.sources;
recorderApp.setActions(data.actions);
recorderApp.setActions(data.actions, data.sources);
this._pushAllSources();
});

View file

@ -172,7 +172,7 @@ export class ContextRecorder extends EventEmitter {
name: 'closePage',
signals: [],
},
timestamp: monotonicTime()
startTime: monotonicTime()
});
this._pageAliases.delete(page);
});
@ -195,7 +195,7 @@ export class ContextRecorder extends EventEmitter {
url: page.mainFrame().url(),
signals: [],
},
timestamp: monotonicTime()
startTime: monotonicTime()
});
}
}
@ -232,7 +232,7 @@ export class ContextRecorder extends EventEmitter {
frame: frameDescription,
action,
description: undefined,
timestamp: monotonicTime()
startTime: monotonicTime()
};
await this._delegate.rewriteActionInContext?.(this._pageAliases, actionInContext);
return actionInContext;

View file

@ -38,7 +38,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp {
async setSelector(selector: string, userGesture?: boolean): Promise<void> {}
async updateCallLogs(callLogs: CallLog[]): Promise<void> {}
async setSources(sources: Source[]): Promise<void> {}
async setActions(actions: actions.ActionInContext[]): Promise<void> {}
async setActions(actions: actions.ActionInContext[], sources: Source[]): Promise<void> {}
}
export class RecorderApp extends EventEmitter implements IRecorderApp {
@ -155,7 +155,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
}
}
async setActions(actions: actions.ActionInContext[]): Promise<void> {
async setActions(actions: actions.ActionInContext[], sources: Source[]): Promise<void> {
}
async setSelector(selector: string, userGesture?: boolean): Promise<void> {

View file

@ -75,9 +75,12 @@ export class RecorderCollection extends EventEmitter {
this._fireChange();
const error = await callback?.(callMetadata).catch((e: Error) => e);
callMetadata.endTime = monotonicTime();
actionInContext.endTime = callMetadata.endTime;
callMetadata.error = error ? serializeError(error) : undefined;
// Do not wait for onAfterCall so that performAction returned immediately after the action.
mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata).catch(() => {});
mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata).then(() => {
this._fireChange();
}).catch(() => {});
}
signal(pageAlias: string, frame: Frame, signal: Signal) {
@ -94,7 +97,7 @@ export class RecorderCollection extends EventEmitter {
generateGoto = true;
else if (lastAction.action.name !== 'click' && lastAction.action.name !== 'press')
generateGoto = true;
else if (timestamp - lastAction.timestamp > signalThreshold)
else if (timestamp - lastAction.startTime > signalThreshold)
generateGoto = true;
if (generateGoto) {
@ -108,7 +111,8 @@ export class RecorderCollection extends EventEmitter {
url: frame.url(),
signals: [],
},
timestamp
startTime: timestamp,
endTime: timestamp,
});
}
return;

View file

@ -32,7 +32,7 @@ export interface IRecorderApp extends EventEmitter {
setSelector(selector: string, userGesture?: boolean): Promise<void>;
updateCallLogs(callLogs: CallLog[]): Promise<void>;
setSources(sources: Source[]): Promise<void>;
setActions(actions: actions.ActionInContext[]): Promise<void>;
setActions(actions: actions.ActionInContext[], sources: Source[]): Promise<void>;
}
export type IRecorderAppFactory = (recorder: IRecorder) => Promise<IRecorderApp>;

View file

@ -86,8 +86,8 @@ export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp
}
}
async setActions(actions: actions.ActionInContext[]): Promise<void> {
this._transport.deliverEvent('setActions', { actions });
async setActions(actions: actions.ActionInContext[], sources: Source[]): Promise<void> {
this._transport.deliverEvent('setActions', { actions, sources });
}
}

View file

@ -76,12 +76,11 @@ export function callMetadataForAction(pageAliases: Map<Page, string>, actionInCo
const callMetadata: CallMetadata = {
id: `call@${createGuid()}`,
stepId: `recorder@${createGuid()}`,
apiName: 'page.' + method,
objectId: mainFrame.guid,
pageId: mainFrame._page.guid,
frameId: mainFrame.guid,
startTime: actionInContext.timestamp,
startTime: actionInContext.startTime,
endTime: 0,
type: 'Frame',
method,
@ -102,7 +101,9 @@ export function collapseActions(actions: actions.ActionInContext[]): actions.Act
result.push(action);
continue;
}
const startTime = result[result.length - 1].startTime;
result[result.length - 1] = action;
result[result.length - 1].startTime = startTime;
}
return result;
}

View file

@ -153,5 +153,6 @@ export type ActionInContext = {
frame: FrameDescription;
description?: string;
action: Action;
timestamp: number;
startTime: number;
endTime?: number;
};

View file

@ -17,18 +17,17 @@
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
import type { Language } from '@web/components/codeMirrorWrapper';
import { ToolbarButton } from '@web/components/toolbarButton';
import { copy, useSetting } from '@web/uiUtils';
import { copy } from '@web/uiUtils';
import * as React from 'react';
import './sourceTab.css';
export const InspectorTab: React.FunctionComponent<{
showScreenshot: boolean,
sdkLanguage: Language,
setIsInspecting: (isInspecting: boolean) => void,
highlightedLocator: string,
setHighlightedLocator: (locator: string) => void,
}> = ({ sdkLanguage, setIsInspecting, highlightedLocator, setHighlightedLocator }) => {
const [showScreenshot] = useSetting('screenshot-instead-of-snapshot', false);
}> = ({ showScreenshot, sdkLanguage, setIsInspecting, highlightedLocator, setHighlightedLocator }) => {
return <div className='vbox' style={{ backgroundColor: 'var(--vscode-sideBar-background)' }}>
<div style={{ margin: '10px 0px 10px 10px', color: 'var(--vscode-editorCodeLens-foreground)', flex: 'none' }}>Locator</div>
<div style={{ margin: '0 10px 10px', flex: 'auto' }}>

View file

@ -25,9 +25,8 @@ export const BackendProvider: React.FunctionComponent<React.PropsWithChildren<{
}>> = ({ guid, children }) => {
const [connection, setConnection] = React.useState<Connection | undefined>(undefined);
const [mode, setMode] = React.useState<Mode>('none');
const [actions, setActions] = React.useState<actionTypes.ActionInContext[]>([]);
const [sources, setSources] = React.useState<Source[]>([]);
const callbacks = React.useRef({ setMode, setActions, setSources });
const [actions, setActions] = React.useState<{ actions: actionTypes.ActionInContext[], sources: Source[] }>({ actions: [], sources: [] });
const callbacks = React.useRef({ setMode, setActions });
React.useEffect(() => {
const wsURL = new URL(`../${guid}`, window.location.toString());
@ -40,8 +39,8 @@ export const BackendProvider: React.FunctionComponent<React.PropsWithChildren<{
}, [guid]);
const backend = React.useMemo(() => {
return connection ? { mode, actions, sources, connection } : undefined;
}, [actions, mode, sources, connection]);
return connection ? { mode, actions: actions.actions, sources: actions.sources, connection } : undefined;
}, [actions, mode, connection]);
return <BackendContext.Provider value={backend}>
{children}
@ -56,8 +55,7 @@ export type Backend = {
type ConnectionCallbacks = {
setMode: (mode: Mode) => void;
setActions: (actions: actionTypes.ActionInContext[]) => void;
setSources: (sources: Source[]) => void;
setActions: (data: { actions: actionTypes.ActionInContext[], sources: Source[] }) => void;
};
class Connection {
@ -111,14 +109,10 @@ class Connection {
const { mode } = params as { mode: Mode };
this._options.setMode(mode);
}
if (method === 'setSources') {
const { sources } = params as { sources: Source[] };
this._options.setSources(sources);
(window as any).playwrightSourcesEchoForTest = sources;
}
if (method === 'setActions') {
const { actions } = params as { actions: actionTypes.ActionInContext[] };
this._options.setActions(actions.filter(a => a.action.name !== 'openPage' && a.action.name !== 'closePage'));
const { actions, sources } = params as { actions: actionTypes.ActionInContext[], sources: Source[] };
this._options.setActions({ actions: actions.filter(a => a.action.name !== 'openPage' && a.action.name !== 'closePage'), sources });
(window as any).playwrightSourcesEchoForTest = sources;
}
}
}

View file

@ -54,17 +54,25 @@ export const Workbench: React.FunctionComponent = () => {
const backend = React.useContext(BackendContext);
const model = React.useContext(ModelContext);
const [fileId, setFileId] = React.useState<string | undefined>();
const [selectedCallTime, setSelectedCallTime] = React.useState<number | undefined>(undefined);
const [selectedStartTime, setSelectedStartTime] = React.useState<number | undefined>(undefined);
const [isInspecting, setIsInspecting] = React.useState(false);
const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
const [highlightedLocatorInProperties, setHighlightedLocatorInProperties] = React.useState<string>('');
const [highlightedLocatorInTrace, setHighlightedLocatorInTrace] = React.useState<string>('');
const [traceCallId, setTraceCallId] = React.useState<string | undefined>();
const setSelectedAction = React.useCallback((action: actionTypes.ActionInContext | undefined) => {
setSelectedCallTime(action?.timestamp);
setSelectedStartTime(action?.startTime);
}, []);
const selectedAction = React.useMemo(() => {
return backend?.actions.find(a => a.timestamp === selectedCallTime);
}, [backend?.actions, selectedCallTime]);
return backend?.actions.find(a => a.startTime === selectedStartTime);
}, [backend?.actions, selectedStartTime]);
React.useEffect(() => {
const callId = model?.actions.find(a => a.endTime && a.endTime === selectedAction?.endTime)?.callId;
if (callId)
setTraceCallId(callId);
}, [model, selectedAction]);
const source = React.useMemo(() => backend?.sources.find(s => s.id === fileId) || backend?.sources[0], [backend?.sources, fileId]);
const sourceLocation = React.useMemo(() => {
@ -95,6 +103,17 @@ export const Workbench: React.FunctionComponent = () => {
return { boundaries };
}, [model]);
const locatorPickedInTrace = React.useCallback((locator: string) => {
setHighlightedLocatorInProperties(locator);
setHighlightedLocatorInTrace('');
setIsInspecting(false);
}, []);
const locatorTypedInProperties = React.useCallback((locator: string) => {
setHighlightedLocatorInTrace(locator);
setHighlightedLocatorInProperties(locator);
}, []);
const actionList = <ActionListView
sdkLanguage={sdkLanguage}
actions={backend?.actions || []}
@ -129,25 +148,23 @@ export const Workbench: React.FunctionComponent = () => {
<SourceChooser fileId={fileId} sources={backend?.sources || []} setFileId={fileId => {
setFileId(fileId);
}} />
<ToolbarButton icon='clear-all' title='Clear' onClick={() => {
}}></ToolbarButton>
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
</Toolbar>;
const sidebarTabbedPane = <TabbedPane tabs={[actionsTab]} />;
const traceView = <TraceView
sdkLanguage={sdkLanguage}
callTime={selectedCallTime || 0}
callId={traceCallId}
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
setHighlightedLocator={setHighlightedLocator} />;
highlightedLocator={highlightedLocatorInTrace}
setHighlightedLocator={locatorPickedInTrace} />;
const propertiesView = <PropertiesView
sdkLanguage={sdkLanguage}
boundaries={boundaries}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
setHighlightedLocator={setHighlightedLocator}
highlightedLocator={highlightedLocatorInProperties}
setHighlightedLocator={locatorTypedInProperties}
sourceLocation={sourceLocation} />;
return <div className='vbox workbench'>
@ -196,6 +213,7 @@ const PropertiesView: React.FunctionComponent<{
id: 'inspector',
title: 'Locator',
render: () => <InspectorTab
showScreenshot={false}
sdkLanguage={sdkLanguage}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
@ -240,14 +258,14 @@ const PropertiesView: React.FunctionComponent<{
const TraceView: React.FunctionComponent<{
sdkLanguage: Language,
callTime: number;
callId: string | undefined,
isInspecting: boolean;
setIsInspecting: (value: boolean) => void;
highlightedLocator: string;
setHighlightedLocator: (locator: string) => void;
}> = ({
sdkLanguage,
callTime,
callId,
isInspecting,
setIsInspecting,
highlightedLocator,
@ -255,8 +273,8 @@ const TraceView: React.FunctionComponent<{
}) => {
const model = React.useContext(ModelContext);
const action = React.useMemo(() => {
return model?.actions.find(a => a.startTime === callTime);
}, [model, callTime]);
return model?.actions.find(a => a.callId === callId);
}, [model, callId]);
const snapshot = React.useMemo(() => {
const snapshot = collectSnapshots(action);

View file

@ -165,6 +165,7 @@ export const Workbench: React.FunctionComponent<{
id: 'inspector',
title: 'Locator',
render: () => <InspectorTab
showScreenshot={showScreenshot}
sdkLanguage={sdkLanguage}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}