Skip to content

Commit

Permalink
chore: start building tv-recorder toolbar
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Sep 21, 2024
1 parent 17ed944 commit c9215ad
Show file tree
Hide file tree
Showing 11 changed files with 222 additions and 102 deletions.
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

0 comments on commit c9215ad

Please sign in to comment.