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