feat(ui): render all console / network messages in trace (#24115)
This commit is contained in:
parent
74cf869c03
commit
67ad2c2bf4
|
|
@ -73,11 +73,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-log-url {
|
.call-log-url {
|
||||||
color: var(--blue);
|
color: var(--vscode-charts-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-log-selector {
|
.call-log-selector {
|
||||||
color: var(--orange);
|
color: var(--vscode-charts-orange);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,12 @@
|
||||||
display: inline;
|
display: inline;
|
||||||
flex: none;
|
flex: none;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
color: var(--orange);
|
color: var(--vscode-charts-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-url {
|
.action-url {
|
||||||
display: inline;
|
display: inline;
|
||||||
flex: none;
|
flex: none;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
color: var(--blue);
|
color: var(--vscode-charts-blue);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,19 @@ import * as React from 'react';
|
||||||
import './attachmentsTab.css';
|
import './attachmentsTab.css';
|
||||||
import { ImageDiffView } from '@web/components/imageDiffView';
|
import { ImageDiffView } from '@web/components/imageDiffView';
|
||||||
import type { TestAttachment } from '@web/components/imageDiffView';
|
import type { TestAttachment } from '@web/components/imageDiffView';
|
||||||
import type { ActionTraceEventInContext } from './modelUtil';
|
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
||||||
|
|
||||||
export const AttachmentsTab: React.FunctionComponent<{
|
export const AttachmentsTab: React.FunctionComponent<{
|
||||||
|
model: MultiTraceModel | undefined,
|
||||||
|
}> = ({ model }) => {
|
||||||
|
if (!model)
|
||||||
|
return null;
|
||||||
|
return <div className='attachments-tab'>
|
||||||
|
{ model.actions.map((action, index) => <AttachmentsSection key={index} action={action} />) }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AttachmentsSection: React.FunctionComponent<{
|
||||||
action: ActionTraceEventInContext | undefined,
|
action: ActionTraceEventInContext | undefined,
|
||||||
}> = ({ action }) => {
|
}> = ({ action }) => {
|
||||||
if (!action)
|
if (!action)
|
||||||
|
|
@ -34,7 +44,7 @@ export const AttachmentsTab: React.FunctionComponent<{
|
||||||
|
|
||||||
const traceUrl = action.context.traceUrl;
|
const traceUrl = action.context.traceUrl;
|
||||||
|
|
||||||
return <div className='attachments-tab'>
|
return <>
|
||||||
{expected && actual && <div className='attachments-section'>Image diff</div>}
|
{expected && actual && <div className='attachments-section'>Image diff</div>}
|
||||||
{expected && actual && <ImageDiffView imageDiff={{
|
{expected && actual && <ImageDiffView imageDiff={{
|
||||||
name: 'Image diff',
|
name: 'Image diff',
|
||||||
|
|
@ -55,7 +65,7 @@ export const AttachmentsTab: React.FunctionComponent<{
|
||||||
<a target='_blank' href={attachmentURL(traceUrl, a)}>{a.name}</a>
|
<a target='_blank' href={attachmentURL(traceUrl, a)}>{a.name}</a>
|
||||||
</div>;
|
</div>;
|
||||||
})}
|
})}
|
||||||
</div>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function attachmentURL(traceUrl: string, attachment: {
|
function attachmentURL(traceUrl: string, attachment: {
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
.call-value.datetime,
|
.call-value.datetime,
|
||||||
.call-value.string,
|
.call-value.string,
|
||||||
.call-value.locator {
|
.call-value.locator {
|
||||||
color: var(--orange);
|
color: var(--vscode-charts-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-value.number,
|
.call-value.number,
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
.call-value.undefined,
|
.call-value.undefined,
|
||||||
.call-value.function,
|
.call-value.function,
|
||||||
.call-value.object {
|
.call-value.object {
|
||||||
color: var(--blue);
|
color: var(--vscode-charts-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-tab .error-message {
|
.call-tab .error-message {
|
||||||
|
|
|
||||||
|
|
@ -16,40 +16,21 @@
|
||||||
|
|
||||||
|
|
||||||
.console-tab {
|
.console-tab {
|
||||||
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
line-height: 16px;
|
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
overflow: auto;
|
|
||||||
padding-top: 3px;
|
|
||||||
user-select: text;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-line {
|
.console-line {
|
||||||
flex: none;
|
width: 100%;
|
||||||
padding: 3px 0 3px 3px;
|
|
||||||
align-items: center;
|
|
||||||
border-top: 1px solid transparent;
|
|
||||||
border-bottom: 1px solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.console-line.error {
|
|
||||||
background: var(--vscode-inputValidation-errorBackground);
|
|
||||||
border-top-color: var(--vscode-inputValidation-errorBorder);
|
|
||||||
border-bottom-color: var(--vscode-inputValidation-errorBorder);
|
|
||||||
color: var(--vscode-errorForeground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.console-line.warning {
|
|
||||||
background: var(--vscode-inputValidation-warningBackground);
|
|
||||||
border-top-color: var(--vscode-inputValidation-warningBorder);
|
|
||||||
border-bottom-color: var(--vscode-inputValidation-warningBorder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-line .codicon {
|
.console-line .codicon {
|
||||||
padding: 0 2px 0 3px;
|
padding: 0 2px 0 3px;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: none;
|
flex: none;
|
||||||
top: 1px;
|
top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-line.warning .codicon {
|
.console-line.warning .codicon {
|
||||||
|
|
@ -57,11 +38,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-line-message {
|
.console-line-message {
|
||||||
white-space: initial;
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -2px;
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-location {
|
.console-location {
|
||||||
|
|
|
||||||
|
|
@ -19,58 +19,81 @@ import type { ActionTraceEvent } from '@trace/trace';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './consoleTab.css';
|
import './consoleTab.css';
|
||||||
import * as modelUtil from './modelUtil';
|
import * as modelUtil from './modelUtil';
|
||||||
|
import { ListView } from '@web/components/listView';
|
||||||
|
|
||||||
|
type ConsoleEntry = {
|
||||||
|
message?: channels.ConsoleMessageInitializer;
|
||||||
|
error?: channels.SerializedError;
|
||||||
|
highlight: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConsoleListView = ListView<ConsoleEntry>;
|
||||||
|
|
||||||
export const ConsoleTab: React.FunctionComponent<{
|
export const ConsoleTab: React.FunctionComponent<{
|
||||||
|
model: modelUtil.MultiTraceModel | undefined,
|
||||||
action: ActionTraceEvent | undefined,
|
action: ActionTraceEvent | undefined,
|
||||||
}> = ({ action }) => {
|
}> = ({ model, action }) => {
|
||||||
const entries = React.useMemo(() => {
|
const { entries } = React.useMemo(() => {
|
||||||
if (!action)
|
if (!model)
|
||||||
return [];
|
return { entries: [] };
|
||||||
const entries: { message?: channels.ConsoleMessageInitializer, error?: channels.SerializedError }[] = [];
|
const entries: ConsoleEntry[] = [];
|
||||||
const context = modelUtil.context(action);
|
const actionEvents = action ? modelUtil.eventsForAction(action) : [];
|
||||||
for (const event of modelUtil.eventsForAction(action)) {
|
for (const event of model.events) {
|
||||||
if (event.method !== 'console' && event.method !== 'pageError')
|
if (event.method !== 'console' && event.method !== 'pageError')
|
||||||
continue;
|
continue;
|
||||||
if (event.method === 'console') {
|
if (event.method === 'console') {
|
||||||
const { guid } = event.params.message;
|
const { guid } = event.params.message;
|
||||||
entries.push({ message: context.initializers[guid] });
|
entries.push({
|
||||||
|
message: modelUtil.context(event).initializers[guid],
|
||||||
|
highlight: actionEvents.includes(event),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (event.method === 'pageError') {
|
||||||
|
entries.push({
|
||||||
|
error: event.params.error,
|
||||||
|
highlight: actionEvents.includes(event),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (event.method === 'pageError')
|
|
||||||
entries.push({ error: event.params.error });
|
|
||||||
}
|
}
|
||||||
return entries;
|
return { entries };
|
||||||
}, [action]);
|
}, [model, action]);
|
||||||
|
|
||||||
return <div className='console-tab'>{
|
return <div className='console-tab'>
|
||||||
entries.map((entry, index) => {
|
<ConsoleListView
|
||||||
const { message, error } = entry;
|
items={entries}
|
||||||
if (message) {
|
isError={entry => !!entry.error || entry.message?.type === 'error'}
|
||||||
const url = message.location.url;
|
isWarning={entry => entry.message?.type === 'warning'}
|
||||||
const filename = url ? url.substring(url.lastIndexOf('/') + 1) : '<anonymous>';
|
render={entry => {
|
||||||
return <div className={'console-line ' + message.type} key={index}>
|
const { message, error } = entry;
|
||||||
<span className='console-location'>{filename}:{message.location.lineNumber}</span>
|
if (message) {
|
||||||
<span className={'codicon codicon-' + iconClass(message)}></span>
|
const url = message.location.url;
|
||||||
<span className='console-line-message'>{message.text}</span>
|
const filename = url ? url.substring(url.lastIndexOf('/') + 1) : '<anonymous>';
|
||||||
</div>;
|
return <div className='console-line'>
|
||||||
}
|
<span className='console-location'>{filename}:{message.location.lineNumber}</span>
|
||||||
if (error) {
|
<span className={'codicon codicon-' + iconClass(message)}></span>
|
||||||
const { error: errorObject, value } = error;
|
<span className='console-line-message'>{message.text}</span>
|
||||||
if (errorObject) {
|
|
||||||
return <div className='console-line error' key={index}>
|
|
||||||
<span className={'codicon codicon-error'}></span>
|
|
||||||
<span className='console-line-message'>{errorObject.message}</span>
|
|
||||||
<div className='console-stack'>{errorObject.stack}</div>
|
|
||||||
</div>;
|
|
||||||
} else {
|
|
||||||
return <div className='console-line error' key={index}>
|
|
||||||
<span className={'codicon codicon-error'}></span>
|
|
||||||
<span className='console-line-message'>{String(value)}</span>
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
if (error) {
|
||||||
return null;
|
const { error: errorObject, value } = error;
|
||||||
})
|
if (errorObject) {
|
||||||
}</div>;
|
return <div className='console-line'>
|
||||||
|
<span className={'codicon codicon-error'}></span>
|
||||||
|
<span className='console-line-message'>{errorObject.message}</span>
|
||||||
|
<div className='console-stack'>{errorObject.stack}</div>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
return <div className='console-line'>
|
||||||
|
<span className={'codicon codicon-error'}></span>
|
||||||
|
<span className='console-line-message'>{String(value)}</span>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
isHighlighted={entry => !!entry.highlight}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function iconClass(message: channels.ConsoleMessageInitializer): string {
|
function iconClass(message: channels.ConsoleMessageInitializer): string {
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ export class MultiTraceModel {
|
||||||
readonly sdkLanguage: Language | undefined;
|
readonly sdkLanguage: Language | undefined;
|
||||||
readonly testIdAttributeName: string | undefined;
|
readonly testIdAttributeName: string | undefined;
|
||||||
readonly sources: Map<string, SourceModel>;
|
readonly sources: Map<string, SourceModel>;
|
||||||
|
resources: ResourceSnapshot[];
|
||||||
|
|
||||||
|
|
||||||
constructor(contexts: ContextEntry[]) {
|
constructor(contexts: ContextEntry[]) {
|
||||||
|
|
@ -81,6 +82,7 @@ export class MultiTraceModel {
|
||||||
this.actions = mergeActions(contexts);
|
this.actions = mergeActions(contexts);
|
||||||
this.events = ([] as EventTraceEvent[]).concat(...contexts.map(c => c.events));
|
this.events = ([] as EventTraceEvent[]).concat(...contexts.map(c => c.events));
|
||||||
this.hasSource = contexts.some(c => c.hasSource);
|
this.hasSource = contexts.some(c => c.hasSource);
|
||||||
|
this.resources = [...contexts.map(c => c.resources)].flat();
|
||||||
|
|
||||||
this.events.sort((a1, a2) => a1.time - a2.time);
|
this.events.sort((a1, a2) => a1.time - a2.time);
|
||||||
this.sources = collectSources(this.actions);
|
this.sources = collectSources(this.actions);
|
||||||
|
|
@ -191,7 +193,7 @@ export function idForAction(action: ActionTraceEvent) {
|
||||||
return `${action.pageId || 'none'}:${action.callId}`;
|
return `${action.pageId || 'none'}:${action.callId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function context(action: ActionTraceEvent): ContextEntry {
|
export function context(action: ActionTraceEvent | EventTraceEvent): ContextEntry {
|
||||||
return (action as any)[contextSymbol];
|
return (action as any)[contextSymbol];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,6 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request.selected:focus {
|
|
||||||
border-color: var(--orange);
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-request-title {
|
.network-request-title {
|
||||||
height: 28px;
|
height: 28px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -34,6 +30,10 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.network-request.highlighted {
|
||||||
|
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||||
|
}
|
||||||
|
|
||||||
.network-request-title-status {
|
.network-request-title-status {
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
|
||||||
|
|
@ -22,18 +22,15 @@ import type { Entry } from '@trace/har';
|
||||||
|
|
||||||
export const NetworkResourceDetails: React.FunctionComponent<{
|
export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
resource: ResourceSnapshot,
|
resource: ResourceSnapshot,
|
||||||
index: number,
|
highlighted: boolean,
|
||||||
selected: boolean,
|
}> = ({ resource, highlighted }) => {
|
||||||
setSelected: React.Dispatch<React.SetStateAction<number>>,
|
|
||||||
}> = ({ resource, index, selected, setSelected }) => {
|
|
||||||
const [expanded, setExpanded] = React.useState(false);
|
const [expanded, setExpanded] = React.useState(false);
|
||||||
const [requestBody, setRequestBody] = React.useState<string | null>(null);
|
const [requestBody, setRequestBody] = React.useState<string | null>(null);
|
||||||
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string } | null>(null);
|
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string } | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setExpanded(false);
|
setExpanded(false);
|
||||||
setSelected(-1);
|
}, [resource]);
|
||||||
}, [resource, setSelected]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const readResources = async () => {
|
const readResources = async () => {
|
||||||
|
|
@ -89,7 +86,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
}, [contentType, resource, resourceName, routeStatus]);
|
}, [contentType, resource, resourceName, routeStatus]);
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
className={'network-request ' + (selected ? 'selected' : '')} onClick={() => setSelected(index)}>
|
className={'network-request' + (highlighted ? ' highlighted' : '')}>
|
||||||
<Expandable expanded={expanded} setExpanded={setExpanded} title={ renderTitle() }>
|
<Expandable expanded={expanded} setExpanded={setExpanded} title={ renderTitle() }>
|
||||||
<div className='network-request-details'>
|
<div className='network-request-details'>
|
||||||
<div className='network-request-details-time'>{resource.time}ms</div>
|
<div className='network-request-details-time'>{resource.time}ms</div>
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,18 @@ import { NetworkResourceDetails } from './networkResourceDetails';
|
||||||
import './networkTab.css';
|
import './networkTab.css';
|
||||||
|
|
||||||
export const NetworkTab: React.FunctionComponent<{
|
export const NetworkTab: React.FunctionComponent<{
|
||||||
|
model: modelUtil.MultiTraceModel | undefined,
|
||||||
action: ActionTraceEvent | undefined,
|
action: ActionTraceEvent | undefined,
|
||||||
}> = ({ action }) => {
|
}> = ({ model, action }) => {
|
||||||
const [selected, setSelected] = React.useState(0);
|
const actionResources = action ? modelUtil.resourcesForAction(action) : [];
|
||||||
|
const resources = model?.resources || [];
|
||||||
const resources = action ? modelUtil.resourcesForAction(action) : [];
|
|
||||||
return <div className='network-tab'>{
|
return <div className='network-tab'>{
|
||||||
resources.map((resource, index) => {
|
resources.map((resource, index) => {
|
||||||
return <NetworkResourceDetails resource={resource} key={index} index={index} selected={selected === index} setSelected={setSelected} />;
|
return <NetworkResourceDetails
|
||||||
|
resource={resource}
|
||||||
|
key={index}
|
||||||
|
highlighted={actionResources.includes(resource)}
|
||||||
|
/>;
|
||||||
})
|
})
|
||||||
}</div>;
|
}</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-tab:focus .snapshot-toggle.toggled {
|
.snapshot-tab:focus .snapshot-toggle.toggled {
|
||||||
background: var(--blue);
|
background: var(--vscode-charts-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-wrapper {
|
.snapshot-wrapper {
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@
|
||||||
.timeline-bar.frame_check,
|
.timeline-bar.frame_check,
|
||||||
.timeline-bar.frame_uncheck,
|
.timeline-bar.frame_uncheck,
|
||||||
.timeline-bar.frame_tap {
|
.timeline-bar.frame_tap {
|
||||||
--action-color: var(--green);
|
--action-color: var(--vscode-charts-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-bar.page_load,
|
.timeline-bar.page_load,
|
||||||
|
|
@ -110,11 +110,11 @@
|
||||||
.timeline-bar.frame_goback,
|
.timeline-bar.frame_goback,
|
||||||
.timeline-bar.frame_goforward,
|
.timeline-bar.frame_goforward,
|
||||||
.timeline-bar.reload {
|
.timeline-bar.reload {
|
||||||
--action-color: var(--blue);
|
--action-color: var(--vscode-charts-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-bar.frame_evaluateexpression {
|
.timeline-bar.frame_evaluateexpression {
|
||||||
--action-color: var(--yellow);
|
--action-color: var(--vscode-charts-yellow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-bar.frame_dialog {
|
.timeline-bar.frame_dialog {
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-bar.frame_navigated {
|
.timeline-bar.frame_navigated {
|
||||||
--action-color: var(--blue);
|
--action-color: var(--vscode-charts-blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-bar.frame_waitforeventinfo,
|
.timeline-bar.frame_waitforeventinfo,
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import * as React from 'react';
|
||||||
import { ActionList } from './actionList';
|
import { ActionList } from './actionList';
|
||||||
import { CallTab } from './callTab';
|
import { CallTab } from './callTab';
|
||||||
import { ConsoleTab } from './consoleTab';
|
import { ConsoleTab } from './consoleTab';
|
||||||
import * as modelUtil from './modelUtil';
|
import type * as modelUtil from './modelUtil';
|
||||||
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
||||||
import { NetworkTab } from './networkTab';
|
import { NetworkTab } from './networkTab';
|
||||||
import { SnapshotTab } from './snapshotTab';
|
import { SnapshotTab } from './snapshotTab';
|
||||||
|
|
@ -68,9 +68,6 @@ export const Workbench: React.FunctionComponent<{
|
||||||
onSelectionChanged?.(action);
|
onSelectionChanged?.(action);
|
||||||
}, [setSelectedAction, onSelectionChanged]);
|
}, [setSelectedAction, onSelectionChanged]);
|
||||||
|
|
||||||
const { errors, warnings } = activeAction ? modelUtil.stats(activeAction) : { errors: 0, warnings: 0 };
|
|
||||||
const consoleCount = errors + warnings;
|
|
||||||
const networkCount = activeAction ? modelUtil.resourcesForAction(activeAction).length : 0;
|
|
||||||
const sdkLanguage = model?.sdkLanguage || 'javascript';
|
const sdkLanguage = model?.sdkLanguage || 'javascript';
|
||||||
|
|
||||||
const callTab: TabbedPaneTabModel = {
|
const callTab: TabbedPaneTabModel = {
|
||||||
|
|
@ -91,19 +88,17 @@ export const Workbench: React.FunctionComponent<{
|
||||||
const consoleTab: TabbedPaneTabModel = {
|
const consoleTab: TabbedPaneTabModel = {
|
||||||
id: 'console',
|
id: 'console',
|
||||||
title: 'Console',
|
title: 'Console',
|
||||||
count: consoleCount,
|
render: () => <ConsoleTab model={model} action={activeAction} />
|
||||||
render: () => <ConsoleTab action={activeAction} />
|
|
||||||
};
|
};
|
||||||
const networkTab: TabbedPaneTabModel = {
|
const networkTab: TabbedPaneTabModel = {
|
||||||
id: 'network',
|
id: 'network',
|
||||||
title: 'Network',
|
title: 'Network',
|
||||||
count: networkCount,
|
render: () => <NetworkTab model={model} action={activeAction} />
|
||||||
render: () => <NetworkTab action={activeAction} />
|
|
||||||
};
|
};
|
||||||
const attachmentsTab: TabbedPaneTabModel = {
|
const attachmentsTab: TabbedPaneTabModel = {
|
||||||
id: 'attachments',
|
id: 'attachments',
|
||||||
title: 'Attachments',
|
title: 'Attachments',
|
||||||
render: () => <AttachmentsTab action={activeAction} />
|
render: () => <AttachmentsTab model={model} />
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs: TabbedPaneTabModel[] = showSourcesFirst ? [
|
const tabs: TabbedPaneTabModel[] = showSourcesFirst ? [
|
||||||
|
|
@ -136,7 +131,6 @@ export const Workbench: React.FunctionComponent<{
|
||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
title: 'Actions',
|
title: 'Actions',
|
||||||
count: 0,
|
|
||||||
component: <ActionList
|
component: <ActionList
|
||||||
sdkLanguage={sdkLanguage}
|
sdkLanguage={sdkLanguage}
|
||||||
actions={model?.actions || []}
|
actions={model?.actions || []}
|
||||||
|
|
@ -150,7 +144,6 @@ export const Workbench: React.FunctionComponent<{
|
||||||
{
|
{
|
||||||
id: 'metadata',
|
id: 'metadata',
|
||||||
title: 'Metadata',
|
title: 'Metadata',
|
||||||
count: 0,
|
|
||||||
component: <MetadataView model={model}/>
|
component: <MetadataView model={model}/>
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -19,28 +19,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
--red: #F44336;
|
|
||||||
--green: #367c39;
|
|
||||||
--purple: #9C27B0;
|
|
||||||
--yellow: #ff9207;
|
|
||||||
--white: #FFFFFF;
|
|
||||||
--blue: #0b7ad5;
|
|
||||||
--transparent-blue: #2196F355;
|
--transparent-blue: #2196F355;
|
||||||
--orange: #d24726;
|
|
||||||
--light-pink: #ff69b460;
|
--light-pink: #ff69b460;
|
||||||
--gray: #888888;
|
--gray: #888888;
|
||||||
--sidebar-width: 250px;
|
--sidebar-width: 250px;
|
||||||
--box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
|
--box-shadow: rgba(0, 0, 0, 0.133) 0px 1.6px 3.6px 0px, rgba(0, 0, 0, 0.11) 0px 0.3px 0.9px 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.dark-mode {
|
|
||||||
--green: #28d12f;
|
|
||||||
--yellow: #ff9207;
|
|
||||||
--purple: #dc12ff;
|
|
||||||
--blue: #4dafff;
|
|
||||||
--orange: #ff9800;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
@ -103,11 +88,15 @@ svg {
|
||||||
}
|
}
|
||||||
|
|
||||||
.codicon-check {
|
.codicon-check {
|
||||||
color: var(--green);
|
color: var(--vscode-charts-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.codicon-error {
|
.codicon-error {
|
||||||
color: var(--red);
|
color: var(--vscode-errorForeground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.codicon-warning {
|
||||||
|
color: var(--vscode-list-warningForeground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.codicon-circle-outline {
|
.codicon-circle-outline {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
flex: auto;
|
flex: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
overflow: auto;
|
overflow-y: auto;
|
||||||
outline: 1px solid transparent;
|
outline: 1px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,3 +75,7 @@
|
||||||
.list-view-entry.error {
|
.list-view-entry.error {
|
||||||
color: var(--vscode-list-errorForeground);
|
color: var(--vscode-list-errorForeground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-view-entry.warning {
|
||||||
|
color: var(--vscode-list-warningForeground);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,18 +19,20 @@ import './listView.css';
|
||||||
|
|
||||||
export type ListViewProps<T> = {
|
export type ListViewProps<T> = {
|
||||||
items: T[],
|
items: T[],
|
||||||
id?: (item: T) => string,
|
id?: (item: T, index: number) => string,
|
||||||
render: (item: T) => React.ReactNode,
|
render: (item: T, index: number) => React.ReactNode,
|
||||||
icon?: (item: T) => string | undefined,
|
icon?: (item: T, index: number) => string | undefined,
|
||||||
indent?: (item: T) => number | undefined,
|
indent?: (item: T, index: number) => number | undefined,
|
||||||
isError?: (item: T) => boolean,
|
isError?: (item: T, index: number) => boolean,
|
||||||
|
isWarning?: (item: T, index: number) => boolean,
|
||||||
|
isHighlighted?: (item: T, index: number) => boolean,
|
||||||
selectedItem?: T,
|
selectedItem?: T,
|
||||||
onAccepted?: (item: T) => void,
|
onAccepted?: (item: T, index: number) => void,
|
||||||
onSelected?: (item: T) => void,
|
onSelected?: (item: T, index: number) => void,
|
||||||
onLeftArrow?: (item: T) => void,
|
onLeftArrow?: (item: T, index: number) => void,
|
||||||
onRightArrow?: (item: T) => void,
|
onRightArrow?: (item: T, index: number) => void,
|
||||||
onHighlighted?: (item: T | undefined) => void,
|
onHighlighted?: (item: T | undefined) => void,
|
||||||
onIconClicked?: (item: T) => void,
|
onIconClicked?: (item: T, index: number) => void,
|
||||||
noItemsMessage?: string,
|
noItemsMessage?: string,
|
||||||
dataTestId?: string,
|
dataTestId?: string,
|
||||||
};
|
};
|
||||||
|
|
@ -41,6 +43,8 @@ export function ListView<T>({
|
||||||
render,
|
render,
|
||||||
icon,
|
icon,
|
||||||
isError,
|
isError,
|
||||||
|
isWarning,
|
||||||
|
isHighlighted,
|
||||||
indent,
|
indent,
|
||||||
selectedItem,
|
selectedItem,
|
||||||
onAccepted,
|
onAccepted,
|
||||||
|
|
@ -63,10 +67,10 @@ export function ListView<T>({
|
||||||
<div
|
<div
|
||||||
className='list-view-content'
|
className='list-view-content'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onDoubleClick={() => selectedItem && onAccepted?.(selectedItem)}
|
onDoubleClick={() => selectedItem && onAccepted?.(selectedItem, items.indexOf(selectedItem))}
|
||||||
onKeyDown={event => {
|
onKeyDown={event => {
|
||||||
if (selectedItem && event.key === 'Enter') {
|
if (selectedItem && event.key === 'Enter') {
|
||||||
onAccepted?.(selectedItem);
|
onAccepted?.(selectedItem, items.indexOf(selectedItem));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
|
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
|
||||||
|
|
@ -76,11 +80,11 @@ export function ListView<T>({
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (selectedItem && event.key === 'ArrowLeft') {
|
if (selectedItem && event.key === 'ArrowLeft') {
|
||||||
onLeftArrow?.(selectedItem);
|
onLeftArrow?.(selectedItem, items.indexOf(selectedItem));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selectedItem && event.key === 'ArrowRight') {
|
if (selectedItem && event.key === 'ArrowRight') {
|
||||||
onRightArrow?.(selectedItem);
|
onRightArrow?.(selectedItem, items.indexOf(selectedItem));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,28 +106,29 @@ export function ListView<T>({
|
||||||
const element = itemListRef.current?.children.item(newIndex);
|
const element = itemListRef.current?.children.item(newIndex);
|
||||||
scrollIntoViewIfNeeded(element || undefined);
|
scrollIntoViewIfNeeded(element || undefined);
|
||||||
onHighlighted?.(undefined);
|
onHighlighted?.(undefined);
|
||||||
onSelected?.(items[newIndex]);
|
onSelected?.(items[newIndex], newIndex);
|
||||||
}}
|
}}
|
||||||
ref={itemListRef}
|
ref={itemListRef}
|
||||||
>
|
>
|
||||||
{noItemsMessage && items.length === 0 && <div className='list-view-empty'>{noItemsMessage}</div>}
|
{noItemsMessage && items.length === 0 && <div className='list-view-empty'>{noItemsMessage}</div>}
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
const selectedSuffix = selectedItem === item ? ' selected' : '';
|
const selectedSuffix = selectedItem === item ? ' selected' : '';
|
||||||
const highlightedSuffix = highlightedItem === item ? ' highlighted' : '';
|
const highlightedSuffix = isHighlighted?.(item, index) || highlightedItem === item ? ' highlighted' : '';
|
||||||
const errorSuffix = isError?.(item) ? ' error' : '';
|
const errorSuffix = isError?.(item, index) ? ' error' : '';
|
||||||
const indentation = indent?.(item) || 0;
|
const warningSuffix = isWarning?.(item, index) ? ' warning' : '';
|
||||||
const rendered = render(item);
|
const indentation = indent?.(item, index) || 0;
|
||||||
|
const rendered = render(item, index);
|
||||||
return <div
|
return <div
|
||||||
key={id?.(item) || index}
|
key={id?.(item, index) || index}
|
||||||
role='listitem'
|
role='listitem'
|
||||||
className={'list-view-entry' + selectedSuffix + highlightedSuffix + errorSuffix}
|
className={'list-view-entry' + selectedSuffix + highlightedSuffix + errorSuffix + warningSuffix}
|
||||||
onClick={() => onSelected?.(item)}
|
onClick={() => onSelected?.(item, index)}
|
||||||
onMouseEnter={() => setHighlightedItem(item)}
|
onMouseEnter={() => setHighlightedItem(item)}
|
||||||
onMouseLeave={() => setHighlightedItem(undefined)}
|
onMouseLeave={() => setHighlightedItem(undefined)}
|
||||||
>
|
>
|
||||||
{indentation ? new Array(indentation).fill(0).map(() => <div className='list-view-indent'></div>) : undefined}
|
{indentation ? new Array(indentation).fill(0).map(() => <div className='list-view-indent'></div>) : undefined}
|
||||||
{icon && <div
|
{icon && <div
|
||||||
className={'codicon ' + (icon(item) || 'codicon-blank')}
|
className={'codicon ' + (icon(item, index) || 'codicon-blank')}
|
||||||
style={{ minWidth: 16, marginRight: 4 }}
|
style={{ minWidth: 16, marginRight: 4 }}
|
||||||
onDoubleClick={e => {
|
onDoubleClick={e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -132,7 +137,7 @@ export function ListView<T>({
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onIconClicked?.(item);
|
onIconClicked?.(item, index);
|
||||||
}}
|
}}
|
||||||
></div>}
|
></div>}
|
||||||
{typeof rendered === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{rendered}</div> : rendered}
|
{typeof rendered === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{rendered}</div> : rendered}
|
||||||
|
|
|
||||||
|
|
@ -51,13 +51,6 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabbed-pane-tab-count {
|
|
||||||
font-size: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-self: flex-start;
|
|
||||||
width: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbed-pane-tab.selected {
|
.tabbed-pane-tab.selected {
|
||||||
background-color: var(--vscode-tab-activeBackground);
|
background-color: var(--vscode-tab-activeBackground);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import * as React from 'react';
|
||||||
export interface TabbedPaneTabModel {
|
export interface TabbedPaneTabModel {
|
||||||
id: string;
|
id: string;
|
||||||
title: string | JSX.Element;
|
title: string | JSX.Element;
|
||||||
count?: number;
|
|
||||||
component?: React.ReactElement;
|
component?: React.ReactElement;
|
||||||
render?: () => React.ReactElement;
|
render?: () => React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +40,6 @@ export const TabbedPane: React.FunctionComponent<{
|
||||||
<TabbedPaneTab
|
<TabbedPaneTab
|
||||||
id={tab.id}
|
id={tab.id}
|
||||||
title={tab.title}
|
title={tab.title}
|
||||||
count={tab.count}
|
|
||||||
selected={selectedTab === tab.id}
|
selected={selectedTab === tab.id}
|
||||||
onSelect={setSelectedTab}
|
onSelect={setSelectedTab}
|
||||||
></TabbedPaneTab>)),
|
></TabbedPaneTab>)),
|
||||||
|
|
@ -63,14 +61,12 @@ export const TabbedPane: React.FunctionComponent<{
|
||||||
export const TabbedPaneTab: React.FunctionComponent<{
|
export const TabbedPaneTab: React.FunctionComponent<{
|
||||||
id: string,
|
id: string,
|
||||||
title: string | JSX.Element,
|
title: string | JSX.Element,
|
||||||
count?: number,
|
|
||||||
selected?: boolean,
|
selected?: boolean,
|
||||||
onSelect: (id: string) => void
|
onSelect: (id: string) => void
|
||||||
}> = ({ id, title, count, selected, onSelect }) => {
|
}> = ({ id, title, selected, onSelect }) => {
|
||||||
return <div className={'tabbed-pane-tab ' + (selected ? 'selected' : '')}
|
return <div className={'tabbed-pane-tab ' + (selected ? 'selected' : '')}
|
||||||
onClick={() => onSelect(id)}
|
onClick={() => onSelect(id)}
|
||||||
key={id}>
|
key={id}>
|
||||||
<div className='tabbed-pane-tab-label'>{title}</div>
|
<div className='tabbed-pane-tab-label'>{title}</div>
|
||||||
<div className='tabbed-pane-tab-count'>{count || ''}</div>
|
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
<link rel='stylesheet' href='./style.css'>
|
<link rel='stylesheet' href='./style.css'>
|
||||||
<script src='./script.js' type='text/javascript'></script>
|
<script src='./script.js' type='text/javascript'></script>
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
|
||||||
const context = await browser.newContext();
|
const context = await browser.newContext();
|
||||||
await context.tracing.start({ name: 'test', screenshots: true, snapshots: true, sources: true });
|
await context.tracing.start({ name: 'test', screenshots: true, snapshots: true, sources: true });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
await page.goto(`data:text/html,<html>Hello world</html>`);
|
await page.goto(`data:text/html,<!DOCTYPE html><html>Hello world</html>`);
|
||||||
await page.setContent('<button>Click</button>');
|
await page.setContent('<!DOCTYPE html><button>Click</button>');
|
||||||
await expect(page.locator('button')).toHaveText('Click');
|
await expect(page.locator('button')).toHaveText('Click');
|
||||||
await expect(page.getByTestId('amazing-btn')).toBeHidden();
|
await expect(page.getByTestId('amazing-btn')).toBeHidden();
|
||||||
await expect(page.getByTestId(/amazing-btn-regex/)).toBeHidden();
|
await expect(page.getByTestId(/amazing-btn-regex/)).toBeHidden();
|
||||||
|
|
@ -102,7 +102,7 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer([traceFile]);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await expect(traceViewer.actionTitles).toHaveText([
|
await expect(traceViewer.actionTitles).toHaveText([
|
||||||
/browserContext.newPage/,
|
/browserContext.newPage/,
|
||||||
/page.gotodata:text\/html,<html>Hello world<\/html>/,
|
/page.gotodata:text\/html,<!DOCTYPE html><html>Hello world<\/html>/,
|
||||||
/page.setContent/,
|
/page.setContent/,
|
||||||
/expect.toHaveTextlocator\('button'\)/,
|
/expect.toHaveTextlocator\('button'\)/,
|
||||||
/expect.toBeHiddengetByTestId\('amazing-btn'\)/,
|
/expect.toBeHiddengetByTestId\('amazing-btn'\)/,
|
||||||
|
|
@ -135,12 +135,32 @@ test('should render events', async ({ showTraceViewer }) => {
|
||||||
|
|
||||||
test('should render console', async ({ showTraceViewer, browserName }) => {
|
test('should render console', async ({ showTraceViewer, browserName }) => {
|
||||||
const traceViewer = await showTraceViewer([traceFile]);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.selectAction('page.evaluate');
|
|
||||||
await traceViewer.showConsoleTab();
|
await traceViewer.showConsoleTab();
|
||||||
|
|
||||||
await expect(traceViewer.consoleLineMessages).toHaveText(['Info', 'Warning', 'Error', 'Unhandled exception']);
|
await expect(traceViewer.consoleLineMessages).toHaveText([
|
||||||
await expect(traceViewer.consoleLines).toHaveClass(['console-line log', 'console-line warning', 'console-line error', 'console-line error']);
|
'Info',
|
||||||
|
'Warning',
|
||||||
|
'Error',
|
||||||
|
'Unhandled exception',
|
||||||
|
'Cheers!'
|
||||||
|
]);
|
||||||
|
await expect(traceViewer.consoleLines.locator('.codicon')).toHaveClass([
|
||||||
|
'codicon codicon-blank',
|
||||||
|
'codicon codicon-warning',
|
||||||
|
'codicon codicon-error',
|
||||||
|
'codicon codicon-error',
|
||||||
|
'codicon codicon-blank',
|
||||||
|
]);
|
||||||
await expect(traceViewer.consoleStacks.first()).toContainText('Error: Unhandled exception');
|
await expect(traceViewer.consoleStacks.first()).toContainText('Error: Unhandled exception');
|
||||||
|
|
||||||
|
await traceViewer.selectAction('page.evaluate');
|
||||||
|
await expect(traceViewer.page.locator('.console-tab').locator('.list-view-entry')).toHaveClass([
|
||||||
|
'list-view-entry highlighted',
|
||||||
|
'list-view-entry highlighted warning',
|
||||||
|
'list-view-entry highlighted error',
|
||||||
|
'list-view-entry highlighted error',
|
||||||
|
'list-view-entry',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should open console errors on click', async ({ showTraceViewer, browserName }) => {
|
test('should open console errors on click', async ({ showTraceViewer, browserName }) => {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,28 @@
|
||||||
|
|
||||||
import { test as it, expect } from './pageTest';
|
import { test as it, expect } from './pageTest';
|
||||||
|
|
||||||
|
it('console.log', async ({ page }) => {
|
||||||
|
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
|
||||||
|
await page.check('input');
|
||||||
|
expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true);
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
console.log('1');
|
||||||
|
console.log('2');
|
||||||
|
console.log(window);
|
||||||
|
console.log({ a: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.setContent(`<input id='checkbox' type='checkbox' checked></input>`);
|
||||||
|
await page.check('input');
|
||||||
|
expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true);
|
||||||
|
|
||||||
|
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
|
||||||
|
await page.uncheck('input');
|
||||||
|
expect(await page.evaluate(() => window['checkbox'].checked)).toBe(false);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
it('should check the box @smoke', async ({ page }) => {
|
it('should check the box @smoke', async ({ page }) => {
|
||||||
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
|
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
|
||||||
await page.check('input');
|
await page.check('input');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue