feat(ui): render all console / network messages in trace (#24115)

This commit is contained in:
Pavel Feldman 2023-07-10 12:56:56 -07:00 committed by GitHub
parent 74cf869c03
commit 67ad2c2bf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 205 additions and 166 deletions

View file

@ -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;
} }

View file

@ -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);
} }

View file

@ -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: {

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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];
} }

View file

@ -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;

View file

@ -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>

View file

@ -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>;
}; };

View file

@ -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 {

View file

@ -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,

View file

@ -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}/>
}, },
] ]

View file

@ -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 {

View file

@ -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);
}

View file

@ -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}

View file

@ -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);
} }

View file

@ -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>;
}; };

View file

@ -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>

View file

@ -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 }) => {

View file

@ -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');