Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: start building tv-recorder toolbar #32744

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp
constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) {
super();
this._transport = transport;
this._transport.eventSink.resolve(this);
this._tracePage = tracePage;
this._traceServer = traceServer;
this.wsEndpointForTest = wsEndpointForTest;
Expand Down Expand Up @@ -94,6 +95,7 @@ async function openApp(trace: string, options?: TraceViewerServerOptions & { hea

class RecorderTransport implements Transport {
private _connected = new ManualPromise<void>();
readonly eventSink = new ManualPromise<EventEmitter>();

constructor() {
}
Expand All @@ -103,6 +105,8 @@ class RecorderTransport implements Transport {
}

async dispatch(method: string, params: any): Promise<any> {
const eventSink = await this.eventSink;
eventSink.emit('event', { event: method, params });
}

onclose() {
Expand Down
8 changes: 0 additions & 8 deletions packages/recorder/src/recorder.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,6 @@
flex: auto;
}

.recorder-chooser {
border: none;
background: none;
outline: none;
color: var(--vscode-sideBarTitle-foreground);
min-width: 100px;
}

.recorder .codicon {
font-size: 16px;
}
Expand Down
38 changes: 6 additions & 32 deletions packages/recorder/src/recorder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import type { CallLog, Mode, Source } from './recorderTypes';
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
import { SplitView } from '@web/components/splitView';
import { emptySource, SourceChooser } from '@web/components/sourceChooser';
import { TabbedPane } from '@web/components/tabbedPane';
import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton';
Expand Down Expand Up @@ -54,15 +55,7 @@ export const Recorder: React.FC<RecorderProps> = ({
if (source)
return source;
}
const source: Source = {
id: 'default',
isRecorded: false,
text: '',
language: 'javascript',
label: '',
highlight: []
};
return source;
return emptySource();
}, [sources, fileId]);

const [locator, setLocator] = React.useState('');
Expand Down Expand Up @@ -152,10 +145,10 @@ export const Recorder: React.FC<RecorderProps> = ({
}}></ToolbarButton>
<div style={{ flex: 'auto' }}></div>
<div>Target:</div>
<select className='recorder-chooser' hidden={!sources.length} value={fileId} onChange={event => {
setFileId(event.target.selectedOptions[0].value);
window.dispatch({ event: 'fileChanged', params: { file: event.target.selectedOptions[0].value } });
}}>{renderSourceOptions(sources)}</select>
<SourceChooser fileId={fileId} sources={sources} setFileId={fileId => {
setFileId(fileId);
window.dispatch({ event: 'fileChanged', params: { file: fileId } });
}} />
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {
window.dispatch({ event: 'clear' });
}}></ToolbarButton>
Expand Down Expand Up @@ -184,22 +177,3 @@ export const Recorder: React.FC<RecorderProps> = ({
/>
</div>;
};

function renderSourceOptions(sources: Source[]): React.ReactNode {
const transformTitle = (title: string): string => title.replace(/.*[/\\]([^/\\]+)/, '$1');
const renderOption = (source: Source): React.ReactNode => (
<option key={source.id} value={source.id}>{transformTitle(source.label)}</option>
);

const hasGroup = sources.some(s => s.group);
if (hasGroup) {
const groups = new Set(sources.map(s => s.group));
return [...groups].filter(Boolean).map(group => (
<optgroup label={group} key={group}>
{sources.filter(s => s.group === group).map(source => renderOption(source))}
</optgroup>
));
}

return sources.map(source => renderOption(source));
}
96 changes: 80 additions & 16 deletions packages/trace-viewer/src/ui/recorderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import type { SourceLocation } from './modelUtil';
import { Workbench } from './workbench';
import type { Mode, Source } from '@recorder/recorderTypes';
import type { ContextEntry } from '../entries';
import { emptySource, SourceChooser } from '@web/components/sourceChooser';
import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton';
import { toggleTheme } from '@web/theme';

const searchParams = new URLSearchParams(window.location.search);
const guid = searchParams.get('ws');
Expand All @@ -29,33 +33,81 @@ const trace = searchParams.get('trace') + '.json';
export const RecorderView: React.FunctionComponent = () => {
const [connection, setConnection] = React.useState<Connection | null>(null);
const [sources, setSources] = React.useState<Source[]>([]);
const [mode, setMode] = React.useState<Mode>('none');
const [fileId, setFileId] = React.useState<string | undefined>();

React.useEffect(() => {
if (!fileId && sources.length > 0)
setFileId(sources[0].id);
}, [fileId, sources]);

const source = React.useMemo(() => {
if (fileId) {
const source = sources.find(s => s.id === fileId);
if (source)
return source;
}
return emptySource();
}, [sources, fileId]);

React.useEffect(() => {
const wsURL = new URL(`../${guid}`, window.location.toString());
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
const webSocket = new WebSocket(wsURL.toString());
setConnection(new Connection(webSocket, { setSources }));
setConnection(new Connection(webSocket, { setSources, setMode }));
return () => {
webSocket.close();
};
}, []);

React.useEffect(() => {
if (!connection)
return;
connection.setMode('recording');
}, [connection]);

return <div className='vbox workbench-loader'>
<Toolbar>
<ToolbarButton icon='circle-large-filled' title='Record' toggled={mode === 'recording' || mode === 'recording-inspecting' || mode === 'assertingText' || mode === 'assertingVisibility'} onClick={() => {
connection?.setMode(mode === 'none' || mode === 'standby' || mode === 'inspecting' ? 'recording' : 'standby');
}}>Record</ToolbarButton>
<ToolbarSeparator />
<ToolbarButton icon='inspect' title='Pick locator' toggled={mode === 'inspecting' || mode === 'recording-inspecting'} onClick={() => {
const newMode = ({
'inspecting': 'standby',
'none': 'inspecting',
'standby': 'inspecting',
'recording': 'recording-inspecting',
'recording-inspecting': 'recording',
'assertingText': 'recording-inspecting',
'assertingVisibility': 'recording-inspecting',
'assertingValue': 'recording-inspecting',
} as Record<string, Mode>)[mode];
connection?.setMode(newMode);
}}></ToolbarButton>
<ToolbarButton icon='eye' title='Assert visibility' toggled={mode === 'assertingVisibility'} disabled={mode === 'none' || mode === 'standby' || mode === 'inspecting'} onClick={() => {
connection?.setMode(mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility');
}}></ToolbarButton>
<ToolbarButton icon='whole-word' title='Assert text' toggled={mode === 'assertingText'} disabled={mode === 'none' || mode === 'standby' || mode === 'inspecting'} onClick={() => {
connection?.setMode(mode === 'assertingText' ? 'recording' : 'assertingText');
}}></ToolbarButton>
<ToolbarButton icon='symbol-constant' title='Assert value' toggled={mode === 'assertingValue'} disabled={mode === 'none' || mode === 'standby' || mode === 'inspecting'} onClick={() => {
connection?.setMode(mode === 'assertingValue' ? 'recording' : 'assertingValue');
}}></ToolbarButton>
<ToolbarSeparator />
<div style={{ flex: 'auto' }}></div>
<div>Target:</div>
<SourceChooser fileId={fileId} sources={sources} setFileId={fileId => {
setFileId(fileId);
}} />
<ToolbarButton icon='clear-all' title='Clear' disabled={!source || !source.text} onClick={() => {
}}></ToolbarButton>
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
</Toolbar>
<TraceView
traceLocation={trace}
sources={sources} />
source={source} />
</div>;
};

export const TraceView: React.FC<{
traceLocation: string,
sources: Source[],
}> = ({ traceLocation, sources }) => {
source: Source | undefined,
}> = ({ traceLocation, source }) => {
const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>();
const [counter, setCounter] = React.useState(0);
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
Expand All @@ -82,19 +134,19 @@ export const TraceView: React.FC<{
}, [counter, traceLocation]);

const fallbackLocation = React.useMemo(() => {
if (!sources.length)
if (!source)
return undefined;
const fallbackLocation: SourceLocation = {
file: '',
line: 0,
column: 0,
source: {
errors: [],
content: sources[0].text
content: source.text
}
};
return fallbackLocation;
}, [sources]);
}, [source]);

return <Workbench
key='workbench'
Expand All @@ -103,6 +155,7 @@ export const TraceView: React.FC<{
fallbackLocation={fallbackLocation}
isLive={true}
hideTimeline={true}
hideMetatada={true}
/>;
};

Expand All @@ -114,13 +167,19 @@ async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> {
return new MultiTraceModel(contextEntries);
}


type ConnectionOptions = {
setSources: (sources: Source[]) => void;
setMode: (mode: Mode) => void;
};

class Connection {
private _lastId = 0;
private _webSocket: WebSocket;
private _callbacks = new Map<number, { resolve: (arg: any) => void, reject: (arg: Error) => void }>();
private _options: { setSources: (sources: Source[]) => void; };
private _options: ConnectionOptions;

constructor(webSocket: WebSocket, options: { setSources: (sources: Source[]) => void }) {
constructor(webSocket: WebSocket, options: ConnectionOptions) {
this._webSocket = webSocket;
this._callbacks = new Map();
this._options = options;
Expand Down Expand Up @@ -157,7 +216,7 @@ class Connection {
}

private _sendMessageNoReply(method: string, params?: any) {
this._sendMessage(method, params).catch(() => { });
this._sendMessage(method, params);
}

private _dispatchEvent(method: string, params?: any) {
Expand All @@ -166,5 +225,10 @@ class Connection {
this._options.setSources(sources);
window.playwrightSourcesEchoForTest = sources;
}

if (method === 'setMode') {
const { mode } = params as { mode: Mode };
this._options.setMode(mode);
}
}
}
4 changes: 0 additions & 4 deletions packages/trace-viewer/src/ui/snapshotTab.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@
background-color: var(--vscode-sideBar-background);
}

.snapshot-tab .toolbar .pick-locator {
margin: 0 4px;
}

.snapshot-controls {
flex: none;
background-color: var(--vscode-sideBar-background);
Expand Down
3 changes: 2 additions & 1 deletion packages/trace-viewer/src/ui/snapshotTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ export const SnapshotTab: React.FunctionComponent<{
iframe={iframeRef1.current}
iteration={loadingRef.current.iteration} />
<Toolbar>
<ToolbarButton className='pick-locator' title={showScreenshotInsteadOfSnapshot ? 'Disable "screenshots instead of snapshots" to pick a locator' : 'Pick locator'} icon='target' toggled={isInspecting} onClick={() => setIsInspecting(!isInspecting)} disabled={showScreenshotInsteadOfSnapshot} />
<ToolbarButton title={showScreenshotInsteadOfSnapshot ? 'Disable "screenshots instead of snapshots" to pick a locator' : 'Pick locator'} icon='target' toggled={isInspecting} onClick={() => setIsInspecting(!isInspecting)} disabled={showScreenshotInsteadOfSnapshot} />
<div style={{ width: 4 }}></div>
{['action', 'before', 'after'].map(tab => {
return <TabbedPaneTab
key={tab}
Expand Down
11 changes: 8 additions & 3 deletions packages/trace-viewer/src/ui/sourceTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { SplitView } from '@web/components/splitView';
import * as React from 'react';
import { useAsyncMemo } from '@web/uiUtils';
import { copy, useAsyncMemo } from '@web/uiUtils';
import './sourceTab.css';
import { StackTraceView } from './stackTrace';
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
Expand Down Expand Up @@ -104,13 +104,18 @@ export const SourceTab: React.FunctionComponent<{
orientation={stackFrameLocation === 'bottom' ? 'vertical' : 'horizontal'}
sidebarHidden={!showStackFrames}
main={<div className='vbox' data-testid='source-code'>
{ fileName && <Toolbar>
{fileName && <Toolbar>
<div className='source-tab-file-name' title={fileName}>
<div>{shortFileName}</div>
</div>
<CopyToClipboard description='Copy filename' value={shortFileName}/>
{location && <ToolbarButton icon='link-external' title='Open in VS Code' onClick={openExternally}></ToolbarButton>}
</Toolbar> }
</Toolbar>}
{!fileName && <div style={{ position: 'absolute', right: 5, top: 5 }}>
<ToolbarButton icon='files' title='Copy' onClick={() => {
copy(source.content || '');
}} />
</div>}
<CodeMirrorWrapper text={source.content || ''} language='javascript' highlight={highlight} revealLine={targetLine} readOnly={true} lineNumbers={true} />
</div>}
sidebar={<StackTraceView stack={stack} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame} />}
Expand Down
Loading