mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: unify recorder & tracer uis (#5791)
This commit is contained in:
parent
43de259522
commit
ad69b2af83
@ -138,7 +138,7 @@ function snapshotScript() {
|
||||
for (const iframe of root.querySelectorAll('iframe')) {
|
||||
const src = iframe.getAttribute('src');
|
||||
if (!src) {
|
||||
iframe.setAttribute('src', 'data:text/html,<body>Snapshot is not available</body>');
|
||||
iframe.setAttribute('src', 'data:text/html,<body style="background: #ddd"></body>');
|
||||
} else {
|
||||
// Append query parameters to inherit ?name= or ?time= values from parent.
|
||||
iframe.setAttribute('src', window.location.origin + src + window.location.search);
|
||||
|
@ -75,7 +75,7 @@ export class SnapshotServer {
|
||||
}
|
||||
|
||||
function respondNotAvailable(): Response {
|
||||
return new Response('<body>Snapshot is not available</body>', { status: 200, headers: { 'Content-Type': 'text/html' } });
|
||||
return new Response('<body style="background: #ddd"></body>', { status: 200, headers: { 'Content-Type': 'text/html' } });
|
||||
}
|
||||
|
||||
function removeHash(url: string) {
|
||||
|
@ -30,11 +30,13 @@ export type UIState = {
|
||||
snapshotUrl?: string;
|
||||
};
|
||||
|
||||
export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused';
|
||||
|
||||
export type CallLog = {
|
||||
id: number;
|
||||
title: string;
|
||||
messages: string[];
|
||||
status: 'in-progress' | 'done' | 'error' | 'paused';
|
||||
status: CallLogStatus;
|
||||
error?: string;
|
||||
reveal?: boolean;
|
||||
duration?: number;
|
||||
@ -44,7 +46,7 @@ export type CallLog = {
|
||||
};
|
||||
snapshots: {
|
||||
before: boolean,
|
||||
in: boolean,
|
||||
action: boolean,
|
||||
after: boolean,
|
||||
}
|
||||
};
|
||||
|
60
src/server/supplements/recorder/recorderUtils.ts
Normal file
60
src/server/supplements/recorder/recorderUtils.ts
Normal file
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CallMetadata } from '../../instrumentation';
|
||||
import { CallLog, CallLogStatus } from './recorderTypes';
|
||||
|
||||
export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus, snapshots: Set<string>): CallLog {
|
||||
const title = metadata.apiName || metadata.method;
|
||||
if (metadata.error)
|
||||
status = 'error';
|
||||
const params = {
|
||||
url: metadata.params?.url,
|
||||
selector: metadata.params?.selector,
|
||||
};
|
||||
let duration = metadata.endTime ? metadata.endTime - metadata.startTime : undefined;
|
||||
if (typeof duration === 'number' && metadata.pauseStartTime && metadata.pauseEndTime) {
|
||||
duration -= (metadata.pauseEndTime - metadata.pauseStartTime);
|
||||
duration = Math.max(duration, 0);
|
||||
}
|
||||
const callLog: CallLog = {
|
||||
id: metadata.id,
|
||||
messages: metadata.log,
|
||||
title,
|
||||
status,
|
||||
error: metadata.error,
|
||||
params,
|
||||
duration,
|
||||
snapshots: {
|
||||
before: showBeforeSnapshot(metadata) && snapshots.has(`before@${metadata.id}`),
|
||||
action: showActionSnapshot(metadata) && snapshots.has(`action@${metadata.id}`),
|
||||
after: showAfterSnapshot(metadata) && snapshots.has(`after@${metadata.id}`),
|
||||
}
|
||||
};
|
||||
return callLog;
|
||||
}
|
||||
|
||||
function showBeforeSnapshot(metadata: CallMetadata): boolean {
|
||||
return metadata.method === 'close';
|
||||
}
|
||||
|
||||
function showActionSnapshot(metadata: CallMetadata): boolean {
|
||||
return ['click', 'dblclick', 'check', 'uncheck', 'fill', 'press'].includes(metadata.method);
|
||||
}
|
||||
|
||||
function showAfterSnapshot(metadata: CallMetadata): boolean {
|
||||
return ['goto', 'click', 'dblclick', 'dblclick', 'check', 'uncheck', 'fill', 'press'].includes(metadata.method);
|
||||
}
|
@ -31,9 +31,10 @@ import * as consoleApiSource from '../../generated/consoleApiSource';
|
||||
import { RecorderApp } from './recorder/recorderApp';
|
||||
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
|
||||
import { Point } from '../../common/types';
|
||||
import { CallLog, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
|
||||
import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes';
|
||||
import { isUnderTest, monotonicTime } from '../../utils/utils';
|
||||
import { InMemorySnapshotter } from '../snapshot/inMemorySnapshotter';
|
||||
import { metadataToCallLog } from './recorder/recorderUtils';
|
||||
|
||||
type BindingSource = { frame: Frame, page: Page };
|
||||
|
||||
@ -56,7 +57,7 @@ export class RecorderSupplement {
|
||||
private _recorderSources: Source[];
|
||||
private _userSources = new Map<string, Source>();
|
||||
private _snapshotter: InMemorySnapshotter;
|
||||
private _hoveredSnapshot: { callLogId: number, phase: 'before' | 'after' | 'in' } | undefined;
|
||||
private _hoveredSnapshot: { callLogId: number, phase: 'before' | 'after' | 'action' } | undefined;
|
||||
private _snapshots = new Set<string>();
|
||||
private _allMetadatas = new Map<number, CallMetadata>();
|
||||
|
||||
@ -209,7 +210,7 @@ export class RecorderSupplement {
|
||||
if (this._hoveredSnapshot) {
|
||||
const metadata = this._allMetadatas.get(this._hoveredSnapshot.callLogId)!;
|
||||
snapshotUrl = `${metadata.pageId}?name=${this._hoveredSnapshot.phase}@${this._hoveredSnapshot.callLogId}`;
|
||||
actionPoint = this._hoveredSnapshot.phase === 'in' ? metadata?.point : undefined;
|
||||
actionPoint = this._hoveredSnapshot.phase === 'action' ? metadata?.point : undefined;
|
||||
} else {
|
||||
for (const [metadata, sdkObject] of this._currentCallsMetadata) {
|
||||
if (source.page === sdkObject.attribution.page) {
|
||||
@ -401,7 +402,7 @@ export class RecorderSupplement {
|
||||
this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) });
|
||||
}
|
||||
|
||||
_captureSnapshot(sdkObject: SdkObject, metadata: CallMetadata, phase: 'before' | 'after' | 'in') {
|
||||
_captureSnapshot(sdkObject: SdkObject, metadata: CallMetadata, phase: 'before' | 'after' | 'action') {
|
||||
if (sdkObject.attribution.page) {
|
||||
const snapshotName = `${phase}@${metadata.id}`;
|
||||
this._snapshots.add(snapshotName);
|
||||
@ -428,7 +429,7 @@ export class RecorderSupplement {
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
if (this._mode === 'recording')
|
||||
return;
|
||||
await this._captureSnapshot(sdkObject, metadata, 'after');
|
||||
this._captureSnapshot(sdkObject, metadata, 'after');
|
||||
if (!metadata.error)
|
||||
this._currentCallsMetadata.delete(metadata);
|
||||
this._pausedCallsMetadata.delete(metadata);
|
||||
@ -473,49 +474,24 @@ export class RecorderSupplement {
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
if (this._mode === 'recording')
|
||||
return;
|
||||
await this._captureSnapshot(sdkObject, metadata, 'in');
|
||||
this._captureSnapshot(sdkObject, metadata, 'action');
|
||||
if (this._pauseOnNextStatement)
|
||||
await this.pause(metadata);
|
||||
}
|
||||
|
||||
async updateCallLog(metadatas: CallMetadata[]): Promise<void> {
|
||||
updateCallLog(metadatas: CallMetadata[]) {
|
||||
if (this._mode === 'recording')
|
||||
return;
|
||||
const logs: CallLog[] = [];
|
||||
for (const metadata of metadatas) {
|
||||
if (!metadata.method)
|
||||
continue;
|
||||
const title = metadata.apiName || metadata.method;
|
||||
let status: 'done' | 'in-progress' | 'paused' | 'error' = 'done';
|
||||
let status: CallLogStatus = 'done';
|
||||
if (this._currentCallsMetadata.has(metadata))
|
||||
status = 'in-progress';
|
||||
if (this._pausedCallsMetadata.has(metadata))
|
||||
status = 'paused';
|
||||
if (metadata.error)
|
||||
status = 'error';
|
||||
const params = {
|
||||
url: metadata.params?.url,
|
||||
selector: metadata.params?.selector,
|
||||
};
|
||||
let duration = metadata.endTime ? metadata.endTime - metadata.startTime : undefined;
|
||||
if (typeof duration === 'number' && metadata.pauseStartTime && metadata.pauseEndTime) {
|
||||
duration -= (metadata.pauseEndTime - metadata.pauseStartTime);
|
||||
duration = Math.max(duration, 0);
|
||||
}
|
||||
logs.push({
|
||||
id: metadata.id,
|
||||
messages: metadata.log,
|
||||
title,
|
||||
status,
|
||||
error: metadata.error,
|
||||
params,
|
||||
duration,
|
||||
snapshots: {
|
||||
before: showBeforeSnapshot(metadata) && this._snapshots.has(`before@${metadata.id}`),
|
||||
in: showInSnapshot(metadata) && this._snapshots.has(`in@${metadata.id}`),
|
||||
after: showAfterSnapshot(metadata) && this._snapshots.has(`after@${metadata.id}`),
|
||||
}
|
||||
});
|
||||
logs.push(metadataToCallLog(metadata, status, this._snapshots));
|
||||
}
|
||||
this._recorderApp?.updateCallLogs(logs);
|
||||
}
|
||||
@ -548,15 +524,3 @@ function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolea
|
||||
function shouldPauseOnStep(sdkObject: SdkObject, metadata: CallMetadata): boolean {
|
||||
return metadata.method === 'goto' || metadata.method === 'close';
|
||||
}
|
||||
|
||||
function showBeforeSnapshot(metadata: CallMetadata): boolean {
|
||||
return metadata.method === 'close';
|
||||
}
|
||||
|
||||
function showInSnapshot(metadata: CallMetadata): boolean {
|
||||
return ['click', 'dblclick', 'check', 'uncheck', 'fill', 'press'].includes(metadata.method);
|
||||
}
|
||||
|
||||
function showAfterSnapshot(metadata: CallMetadata): boolean {
|
||||
return ['goto', 'click', 'dblclick', 'dblclick', 'check', 'uncheck', 'fill', 'press'].includes(metadata.method);
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ class ContextTracer {
|
||||
await this._snapshotter.start();
|
||||
}
|
||||
|
||||
async _captureSnapshot(name: string, sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise<void> {
|
||||
async _captureSnapshot(name: 'before' | 'after' | 'action', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise<void> {
|
||||
if (!sdkObject.attribution.page)
|
||||
return;
|
||||
const snapshotName = `${name}@${metadata.id}`;
|
||||
|
@ -115,7 +115,7 @@ class TraceViewer {
|
||||
const traceViewerPlaywright = createPlaywright(true);
|
||||
const args = [
|
||||
'--app=data:text/html,',
|
||||
'--window-position=1280,10',
|
||||
'--window-size=1280,800'
|
||||
];
|
||||
if (isUnderTest())
|
||||
args.push(`--remote-debugging-port=0`);
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
:root {
|
||||
--toolbar-bg-color: #fafafa;
|
||||
--toolbar-color: #777;
|
||||
--toolbar-color: #555;
|
||||
|
||||
--light-background: #f3f2f1;
|
||||
--background: #edebe9;
|
||||
@ -27,7 +27,7 @@
|
||||
--purple: #9C27B0;
|
||||
--yellow: #FFC107;
|
||||
--white: #FFFFFF;
|
||||
--blue: #2196F3;
|
||||
--blue: #0b7ad5;
|
||||
--transparent-blue: #2196F355;
|
||||
--orange: #d24726;
|
||||
--black: #1E1E1E;
|
||||
@ -53,7 +53,6 @@ html, body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
background: var(--background);
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
@ -64,7 +63,6 @@ html, body {
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--color);
|
||||
font-size: 14px;
|
||||
font-family: SegoeUI-SemiBold-final,Segoe UI Semibold,SegoeUI-Regular-final,Segoe UI,"Segoe UI Web (West European)",Segoe,-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,Tahoma,Helvetica,Arial,sans-serif;
|
||||
|
@ -24,6 +24,7 @@
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
background: white;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.source-line {
|
||||
@ -36,7 +37,7 @@
|
||||
padding: 0 8px;
|
||||
width: 30px;
|
||||
text-align: right;
|
||||
background: #edebe9;
|
||||
background: #f6f5f4;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,10 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.split-view.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.split-view-main {
|
||||
display: flex;
|
||||
flex: auto;
|
||||
@ -32,12 +36,29 @@
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.split-view.vertical > .split-view-sidebar {
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.split-view.horizontal > .split-view-sidebar {
|
||||
border-left: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.split-view-resizer {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.split-view.vertical > .split-view-resizer {
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 12px;
|
||||
cursor: resize;
|
||||
cursor: ns-resize;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.split-view.horizontal > .split-view-resizer {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 12px;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import * as React from 'react';
|
||||
export interface SplitViewProps {
|
||||
sidebarSize: number,
|
||||
sidebarHidden?: boolean
|
||||
orientation?: 'vertical' | 'horizontal',
|
||||
}
|
||||
|
||||
const kMinSidebarSize = 50;
|
||||
@ -27,26 +28,30 @@ const kMinSidebarSize = 50;
|
||||
export const SplitView: React.FC<SplitViewProps> = ({
|
||||
sidebarSize,
|
||||
sidebarHidden,
|
||||
orientation = 'vertical',
|
||||
children
|
||||
}) => {
|
||||
let [size, setSize] = React.useState<number>(Math.max(kMinSidebarSize, sidebarSize));
|
||||
const [resizing, setResizing] = React.useState<{ offsetY: number, size: number } | null>(null);
|
||||
const [resizing, setResizing] = React.useState<{ offset: number, size: number } | null>(null);
|
||||
|
||||
const childrenArray = React.Children.toArray(children);
|
||||
document.body.style.userSelect = resizing ? 'none' : 'inherit';
|
||||
return <div className='split-view'>
|
||||
const resizerStyle = orientation === 'vertical' ?
|
||||
{bottom: resizing ? 0 : size - 4, top: resizing ? 0 : undefined, height: resizing ? 'initial' : 8 } :
|
||||
{right: resizing ? 0 : size - 4, left: resizing ? 0 : undefined, width: resizing ? 'initial' : 8 };
|
||||
return <div className={'split-view ' + orientation}>
|
||||
<div className='split-view-main'>{childrenArray[0]}</div>
|
||||
{ !sidebarHidden && <div style={{flexBasis: size}} className='split-view-sidebar'>{childrenArray[1]}</div> }
|
||||
{ !sidebarHidden && <div
|
||||
style={{bottom: resizing ? 0 : size - 4, top: resizing ? 0 : undefined, height: resizing ? 'initial' : 8 }}
|
||||
style={resizerStyle}
|
||||
className='split-view-resizer'
|
||||
onMouseDown={event => setResizing({ offsetY: event.clientY, size })}
|
||||
onMouseDown={event => setResizing({ offset: orientation === 'vertical' ? event.clientY : event.clientX, size })}
|
||||
onMouseUp={() => setResizing(null)}
|
||||
onMouseMove={event => {
|
||||
if (!event.buttons)
|
||||
setResizing(null);
|
||||
else if (resizing)
|
||||
setSize(Math.max(kMinSidebarSize, resizing.size - event.clientY + resizing.offsetY));
|
||||
setSize(Math.max(kMinSidebarSize, resizing.size - (orientation === 'vertical' ? event.clientY : event.clientX) + resizing.offset));
|
||||
}}
|
||||
></div> }
|
||||
</div>;
|
||||
|
@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
.toolbar-button:not(.disabled):hover {
|
||||
color: #555;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.toolbar-button .codicon {
|
||||
|
@ -21,7 +21,7 @@ import { msToString } from '../uiUtils';
|
||||
|
||||
export interface CallLogProps {
|
||||
log: CallLog[],
|
||||
onHover: (callLog: CallLog | undefined, phase?: 'before' | 'after' | 'in') => void
|
||||
onHover: (callLog: CallLog | undefined, phase?: 'before' | 'after' | 'action') => void
|
||||
}
|
||||
|
||||
export const CallLogView: React.FC<CallLogProps> = ({
|
||||
@ -53,7 +53,7 @@ export const CallLogView: React.FC<CallLogProps> = ({
|
||||
{ typeof callLog.duration === 'number' ? <span className='call-log-time'>— {msToString(callLog.duration)}</span> : undefined}
|
||||
{ <div style={{flex: 'auto'}}></div> }
|
||||
<span className={'codicon codicon-vm-outline preview' + (callLog.snapshots.before ? '' : ' invisible')} onMouseEnter={() => onHover(callLog, 'before')} onMouseLeave={() => onHover(undefined)}></span>
|
||||
<span className={'codicon codicon-vm-running preview' + (callLog.snapshots.in ? '' : ' invisible')} onMouseEnter={() => onHover(callLog, 'in')} onMouseLeave={() => onHover(undefined)}></span>
|
||||
<span className={'codicon codicon-vm-running preview' + (callLog.snapshots.action ? '' : ' invisible')} onMouseEnter={() => onHover(callLog, 'action')} onMouseLeave={() => onHover(undefined)}></span>
|
||||
<span className={'codicon codicon-vm-active preview' + (callLog.snapshots.after ? '' : ' invisible')} onMouseEnter={() => onHover(callLog, 'after')} onMouseLeave={() => onHover(undefined)}></span>
|
||||
</div>
|
||||
{ (isExpanded ? callLog.messages : []).map((message, i) => {
|
||||
|
@ -21,28 +21,27 @@
|
||||
flex: none;
|
||||
position: relative;
|
||||
padding: 0 var(--layout-gap);
|
||||
user-select: none;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.action-entry {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
border: 3px solid var(--background);
|
||||
margin-top: var(--layout-gap);
|
||||
user-select: none;
|
||||
padding: 0 5px 5px 5px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
line-height: 28px;
|
||||
padding-left: 3px;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.action-entry:hover {
|
||||
border-color: var(--inactive-focus-ring);
|
||||
.action-entry:hover, .action-entry.selected {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.action-entry.selected {
|
||||
border-color: var(--inactive-focus-ring);
|
||||
border-left: 3px solid #666;
|
||||
}
|
||||
|
||||
.action-entry.selected:focus {
|
||||
@ -52,19 +51,9 @@
|
||||
.action-title {
|
||||
display: inline;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-header {
|
||||
display: block;
|
||||
align-items: center;
|
||||
margin: 5px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-header .action-error {
|
||||
.action-error {
|
||||
color: red;
|
||||
top: 2px;
|
||||
position: relative;
|
||||
@ -74,9 +63,11 @@
|
||||
.action-selector {
|
||||
display: inline;
|
||||
padding-left: 5px;
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.action-url {
|
||||
display: inline;
|
||||
padding-left: 5px;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
@ -33,22 +33,19 @@ export const ActionList: React.FC<ActionListProps> = ({
|
||||
onSelected = () => {},
|
||||
onHighlighted = () => {},
|
||||
}) => {
|
||||
const targetAction = highlightedAction || selectedAction;
|
||||
return <div className='action-list'>{actions.map(actionEntry => {
|
||||
const { metadata, actionId } = actionEntry;
|
||||
return <div
|
||||
className={'action-entry' + (actionEntry === targetAction ? ' selected' : '')}
|
||||
className={'action-entry' + (actionEntry === selectedAction ? ' selected' : '')}
|
||||
key={actionId}
|
||||
onClick={() => onSelected(actionEntry)}
|
||||
onMouseEnter={() => onHighlighted(actionEntry)}
|
||||
onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)}
|
||||
>
|
||||
<div className='action-header'>
|
||||
<div className={'action-error codicon codicon-issues'} hidden={!metadata.error} />
|
||||
<div className='action-title'>{metadata.method}</div>
|
||||
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>}
|
||||
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
|
||||
</div>
|
||||
<div className={'action-error codicon codicon-issues'} hidden={!metadata.error} />
|
||||
<div className='action-title'>{metadata.method}</div>
|
||||
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>}
|
||||
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
|
||||
</div>;
|
||||
})}</div>;
|
||||
};
|
||||
|
@ -16,14 +16,15 @@
|
||||
|
||||
.logs-tab {
|
||||
flex: auto;
|
||||
position: relative;
|
||||
line-height: 20px;
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
background: #fdfcfc;
|
||||
font-family: var(--monospace-font);
|
||||
white-space: nowrap;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
margin: 0 10px;
|
||||
white-space: pre;
|
||||
flex: none;
|
||||
padding: 3px 0 3px 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
@ -15,15 +15,12 @@
|
||||
*/
|
||||
|
||||
.network-request {
|
||||
box-shadow: var(--box-shadow);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
margin-bottom: 10px;
|
||||
background: #fdfcfc;
|
||||
width: 100%;
|
||||
border: 3px solid transparent;
|
||||
flex: none;
|
||||
outline: none;
|
||||
}
|
||||
@ -38,23 +35,14 @@
|
||||
}
|
||||
|
||||
.network-request-title {
|
||||
height: 36px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.network-request-title-status {
|
||||
font-weight: bold;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.network-request-title-status.status-success {
|
||||
background-color: var(--green);
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.network-request-title-status.status-failure {
|
||||
@ -66,20 +54,12 @@
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.network-request-title-method {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.network-request-title-url {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.network-request-title-content-type {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.network-request-details {
|
||||
font-family: var(--monospace-font);
|
||||
width: 100%;
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
.snapshot-tab {
|
||||
display: flex;
|
||||
flex: auto;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
@ -25,11 +26,18 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px 4px 0 0;
|
||||
}
|
||||
|
||||
.snapshot-toggle {
|
||||
padding: 5px 10px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 20px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.snapshot-toggle:hover {
|
||||
background-color: #ededed;
|
||||
}
|
||||
|
||||
.snapshot-toggle.toggled {
|
||||
@ -39,12 +47,13 @@
|
||||
.snapshot-wrapper {
|
||||
flex: auto;
|
||||
margin: 1px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.snapshot-container {
|
||||
display: block;
|
||||
background: white;
|
||||
outline: 1px solid #aaa;
|
||||
box-shadow: rgb(0 0 0 / 15%) 0px 0.1em 4.5em;
|
||||
}
|
||||
|
||||
iframe#snapshot {
|
||||
|
@ -51,7 +51,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||
point = actionEntry.metadata.point;
|
||||
}
|
||||
}
|
||||
const snapshotUrl = snapshotUri ? `${window.location.origin}/snapshot/${snapshotUri}` : 'data:text/html,Snapshot is not available';
|
||||
const snapshotUrl = snapshotUri ? `${window.location.origin}/snapshot/${snapshotUri}` : 'data:text/html,<body style="background: #ddd"></body>';
|
||||
try {
|
||||
(iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl, { point });
|
||||
} catch (e) {
|
||||
@ -59,6 +59,10 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||
}, [actionEntry, snapshotIndex, pageId, time]);
|
||||
|
||||
const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height);
|
||||
const scaledSize = {
|
||||
width: snapshotSize.width * scale,
|
||||
height: snapshotSize.height * scale,
|
||||
};
|
||||
return <div className='snapshot-tab'>
|
||||
<div className='snapshot-controls'>{
|
||||
selection && <div key='selectedTime' className='snapshot-toggle'>
|
||||
@ -77,7 +81,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||
<div className='snapshot-container' style={{
|
||||
width: snapshotSize.width + 'px',
|
||||
height: snapshotSize.height + 'px',
|
||||
transform: `translate(${-snapshotSize.width * (1 - scale) / 2}px, ${-snapshotSize.height * (1 - scale) / 2}px) scale(${scale})`,
|
||||
transform: `translate(${-snapshotSize.width * (1 - scale) / 2 + (measure.width - scaledSize.width) / 2}px, ${-snapshotSize.height * (1 - scale) / 2 + (measure.height - scaledSize.height) / 2}px) scale(${scale})`,
|
||||
}}>
|
||||
<iframe ref={iframeRef} id='snapshot' name='snapshot' src='/snapshot/'></iframe>
|
||||
</div>
|
||||
|
@ -18,72 +18,6 @@
|
||||
flex: auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #fdfcfc;
|
||||
font-family: var(--monospace-font);
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.source-content {
|
||||
flex: 1 1 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.source-stack {
|
||||
flex: 1 1 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.source-stack-frame {
|
||||
flex: 0 0 20px;
|
||||
font-size: smaller;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.source-stack-frame.selected,
|
||||
.source-stack-frame:hover {
|
||||
background: var(--inactive-focus-ring);
|
||||
}
|
||||
|
||||
.source-stack-frame-function {
|
||||
flex: 1 1 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.source-stack-frame-location {
|
||||
flex: 1 1 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.source-stack-frame-line {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.source-line-number {
|
||||
width: 80px;
|
||||
border-right: 1px solid var(--separator);
|
||||
display: inline-block;
|
||||
margin-right: 3px;
|
||||
text-align: end;
|
||||
padding-right: 4px;
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
.source-code {
|
||||
white-space: pre;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.source-line-highlight {
|
||||
background-color: #ff69b460;
|
||||
}
|
||||
|
@ -19,8 +19,10 @@ import * as React from 'react';
|
||||
import { useAsyncMemo } from './helpers';
|
||||
import './sourceTab.css';
|
||||
import '../../../third_party/highlightjs/highlightjs/tomorrow.css';
|
||||
import * as highlightjs from '../../../third_party/highlightjs/highlightjs';
|
||||
import { StackFrame } from '../../../common/types';
|
||||
import { Source as SourceView } from '../../components/source';
|
||||
import { StackTraceView } from './stackTrace';
|
||||
import { SplitView } from '../../components/splitView';
|
||||
|
||||
type StackInfo = string | {
|
||||
frames: StackFrame[];
|
||||
@ -53,7 +55,7 @@ export const SourceTab: React.FunctionComponent<{
|
||||
};
|
||||
}, [actionEntry]);
|
||||
|
||||
const content = useAsyncMemo<string[]>(async () => {
|
||||
const content = useAsyncMemo<string>(async () => {
|
||||
let value: string;
|
||||
if (typeof stackInfo === 'string') {
|
||||
value = stackInfo;
|
||||
@ -63,17 +65,10 @@ export const SourceTab: React.FunctionComponent<{
|
||||
stackInfo.fileContent.set(filePath, await fetch(`/file?${filePath}`).then(response => response.text()).catch(e => `<Unable to read "${filePath}">`));
|
||||
value = stackInfo.fileContent.get(filePath)!;
|
||||
}
|
||||
const result = [];
|
||||
let continuation: any;
|
||||
for (const line of (value || '').split('\n')) {
|
||||
const highlighted = highlightjs.highlight('javascript', line, true, continuation);
|
||||
continuation = highlighted.top;
|
||||
result.push(highlighted.value);
|
||||
}
|
||||
return result;
|
||||
}, [stackInfo, selectedFrame], []);
|
||||
return value;
|
||||
}, [stackInfo, selectedFrame], '');
|
||||
|
||||
const targetLine = typeof stackInfo === 'string' ? -1 : stackInfo.frames[selectedFrame].line;
|
||||
const targetLine = typeof stackInfo === 'string' ? 0 : stackInfo.frames[selectedFrame].line || 0;
|
||||
|
||||
const targetLineRef = React.createRef<HTMLDivElement>();
|
||||
React.useLayoutEffect(() => {
|
||||
@ -83,41 +78,8 @@ export const SourceTab: React.FunctionComponent<{
|
||||
}
|
||||
}, [needReveal, targetLineRef]);
|
||||
|
||||
return <div className='source-tab'>
|
||||
<div className='source-content'>{
|
||||
content.map((markup, index) => {
|
||||
const isTargetLine = (index + 1) === targetLine;
|
||||
return <div
|
||||
key={index}
|
||||
className={isTargetLine ? 'source-line-highlight' : ''}
|
||||
ref={isTargetLine ? targetLineRef : null}
|
||||
>
|
||||
<div className='source-line-number'>{index + 1}</div>
|
||||
<div className='source-code' dangerouslySetInnerHTML={{ __html: markup }}></div>
|
||||
</div>;
|
||||
})
|
||||
}</div>
|
||||
{typeof stackInfo !== 'string' && <div className='source-stack'>{
|
||||
stackInfo.frames.map((frame, index) => {
|
||||
return <div
|
||||
key={index}
|
||||
className={'source-stack-frame' + (selectedFrame === index ? ' selected' : '')}
|
||||
onClick={() => {
|
||||
setSelectedFrame(index);
|
||||
setNeedReveal(true);
|
||||
}}
|
||||
>
|
||||
<span className='source-stack-frame-function'>
|
||||
{frame.function || '(anonymous)'}
|
||||
</span>
|
||||
<span className='source-stack-frame-location'>
|
||||
{frame.file}
|
||||
</span>
|
||||
<span className='source-stack-frame-line'>
|
||||
{':' + frame.line}
|
||||
</span>
|
||||
</div>;
|
||||
})
|
||||
}</div>}
|
||||
</div>;
|
||||
return <SplitView sidebarSize={250} orientation='horizontal'>
|
||||
<SourceView text={content} language='javascript' highlight={[{ line: targetLine, type: 'running' }]} revealLine={targetLine}></SourceView>
|
||||
<StackTraceView actionEntry={actionEntry} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame}></StackTraceView>
|
||||
</SplitView>;
|
||||
};
|
||||
|
55
src/web/traceViewer/ui/stackTrace.css
Normal file
55
src/web/traceViewer/ui/stackTrace.css
Normal file
@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.stack-trace {
|
||||
flex: 1 1 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.stack-trace-frame {
|
||||
flex: 0 0 20px;
|
||||
font-size: smaller;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.stack-trace-frame.selected,
|
||||
.stack-trace-frame:hover {
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
|
||||
.stack-trace-frame-function {
|
||||
flex: 1 1 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.stack-trace-frame-location {
|
||||
flex: 1 1 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.stack-trace-frame-line {
|
||||
flex: none;
|
||||
}
|
49
src/web/traceViewer/ui/stackTrace.tsx
Normal file
49
src/web/traceViewer/ui/stackTrace.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
|
||||
import * as React from 'react';
|
||||
import './stackTrace.css';
|
||||
|
||||
export const StackTraceView: React.FunctionComponent<{
|
||||
actionEntry: ActionEntry | undefined,
|
||||
selectedFrame: number,
|
||||
setSelectedFrame: (index: number) => void
|
||||
}> = ({ actionEntry, setSelectedFrame, selectedFrame }) => {
|
||||
const frames = actionEntry?.metadata.stack || [];
|
||||
return <div className='stack-trace'>{
|
||||
frames.map((frame, index) => {
|
||||
return <div
|
||||
key={index}
|
||||
className={'stack-trace-frame' + (selectedFrame === index ? ' selected' : '')}
|
||||
onClick={() => {
|
||||
setSelectedFrame(index);
|
||||
}}
|
||||
>
|
||||
<span className='stack-trace-frame-function'>
|
||||
{frame.function || '(anonymous)'}
|
||||
</span>
|
||||
<span className='stack-trace-frame-location'>
|
||||
{frame.file.split('/').pop()}
|
||||
</span>
|
||||
<span className='stack-trace-frame-line'>
|
||||
{':' + frame.line}
|
||||
</span>
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
</div>;
|
||||
};
|
@ -27,11 +27,16 @@
|
||||
}
|
||||
|
||||
.tab-strip {
|
||||
flex: auto;
|
||||
color: var(--toolbar-color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-shadow: var(--box-shadow);
|
||||
background-color: var(--toolbar-bg-color);
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
height: 34px;
|
||||
padding-right: 10px;
|
||||
flex: none;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tab-strip:focus {
|
||||
@ -50,6 +55,7 @@
|
||||
border-bottom: 3px solid transparent;
|
||||
width: 80px;
|
||||
outline: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
@ -61,9 +67,9 @@
|
||||
}
|
||||
|
||||
.tab-element.selected {
|
||||
border-bottom-color: var(--color);
|
||||
border-bottom-color: #666;
|
||||
}
|
||||
|
||||
.tab-element:hover {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 20px 0 5px;
|
||||
cursor: text;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
.workbench {
|
||||
contain: size;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.workbench .header {
|
||||
|
@ -25,6 +25,8 @@ import { NetworkTab } from './networkTab';
|
||||
import { SourceTab } from './sourceTab';
|
||||
import { SnapshotTab } from './snapshotTab';
|
||||
import { LogsTab } from './logsTab';
|
||||
import { SplitView } from '../../components/splitView';
|
||||
|
||||
|
||||
export const Workbench: React.FunctionComponent<{
|
||||
contexts: ContextEntry[],
|
||||
@ -71,7 +73,7 @@ export const Workbench: React.FunctionComponent<{
|
||||
/>
|
||||
</div>
|
||||
<div className='hbox'>
|
||||
<div style={{ display: 'flex', flex: 'none', overflow: 'auto' }}>
|
||||
<div style={{ display: 'flex', flex: 'none', overflow: 'auto', borderRight: '1px solid #ddd' }}>
|
||||
<ActionList
|
||||
actions={actions}
|
||||
selectedAction={selectedAction}
|
||||
@ -83,12 +85,15 @@ export const Workbench: React.FunctionComponent<{
|
||||
onHighlighted={action => setHighlightedAction(action)}
|
||||
/>
|
||||
</div>
|
||||
<TabbedPane tabs={[
|
||||
{ id: 'snapshot', title: 'Snapshot', render: () => <SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} selection={snapshotSelection} boundaries={boundaries} /> },
|
||||
{ id: 'source', title: 'Source', render: () => <SourceTab actionEntry={selectedAction} /> },
|
||||
{ id: 'network', title: 'Network', render: () => <NetworkTab actionEntry={selectedAction} /> },
|
||||
{ id: 'logs', title: 'Logs', render: () => <LogsTab actionEntry={selectedAction} /> },
|
||||
]}/>
|
||||
<SplitView sidebarSize={250}>
|
||||
<SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} selection={snapshotSelection} boundaries={boundaries} />
|
||||
<TabbedPane tabs={[
|
||||
{ id: 'logs', title: 'Log', render: () => <LogsTab actionEntry={selectedAction} /> },
|
||||
{ id: 'source', title: 'Source', render: () => <SourceTab actionEntry={selectedAction} /> },
|
||||
{ id: 'network', title: 'Network', render: () => <NetworkTab actionEntry={selectedAction} /> },
|
||||
]}/>
|
||||
|
||||
</SplitView>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user