feat(trace-viewer): add error links to actions (#7434)
This commit is contained in:
parent
4c7343fe96
commit
82b21e912e
|
|
@ -41,12 +41,10 @@
|
||||||
|
|
||||||
.action-entry.highlighted,
|
.action-entry.highlighted,
|
||||||
.action-entry.selected {
|
.action-entry.selected {
|
||||||
color: white;
|
|
||||||
background-color: var(--gray);
|
background-color: var(--gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-entry.highlighted {
|
.action-entry.highlighted {
|
||||||
color: white;
|
|
||||||
background-color: var(--light-gray);
|
background-color: var(--light-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,15 +52,37 @@
|
||||||
background-color: var(--blue);
|
background-color: var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-entry.highlighted > div,
|
.action-entry.highlighted *,
|
||||||
.action-entry.selected > div {
|
.action-entry.selected * {
|
||||||
color: white;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-title {
|
.action-title {
|
||||||
|
flex: auto;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
flex: none;
|
flex: none;
|
||||||
display: inline;
|
display: flex;
|
||||||
white-space: nowrap;
|
align-items: center;
|
||||||
|
padding-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 20px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icons:hover {
|
||||||
|
border-bottom: 1px solid white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-error {
|
.action-error {
|
||||||
|
|
@ -85,3 +105,11 @@
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
color: var(--blue);
|
color: var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-entry .codicon-error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-entry .codicon-warning {
|
||||||
|
color: darkorange;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
import './actionList.css';
|
import './actionList.css';
|
||||||
import './tabbedPane.css';
|
import './tabbedPane.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import * as modelUtil from './modelUtil';
|
||||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||||
|
|
||||||
export interface ActionListProps {
|
export interface ActionListProps {
|
||||||
|
|
@ -25,6 +26,7 @@ export interface ActionListProps {
|
||||||
highlightedAction: ActionTraceEvent | undefined,
|
highlightedAction: ActionTraceEvent | undefined,
|
||||||
onSelected: (action: ActionTraceEvent) => void,
|
onSelected: (action: ActionTraceEvent) => void,
|
||||||
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
onHighlighted: (action: ActionTraceEvent | undefined) => void,
|
||||||
|
setSelectedTab: (tab: string) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionList: React.FC<ActionListProps> = ({
|
export const ActionList: React.FC<ActionListProps> = ({
|
||||||
|
|
@ -33,6 +35,7 @@ export const ActionList: React.FC<ActionListProps> = ({
|
||||||
highlightedAction = undefined,
|
highlightedAction = undefined,
|
||||||
onSelected = () => {},
|
onSelected = () => {},
|
||||||
onHighlighted = () => {},
|
onHighlighted = () => {},
|
||||||
|
setSelectedTab = () => {},
|
||||||
}) => {
|
}) => {
|
||||||
const actionListRef = React.createRef<HTMLDivElement>();
|
const actionListRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
|
@ -72,6 +75,8 @@ export const ActionList: React.FC<ActionListProps> = ({
|
||||||
const { metadata } = action;
|
const { metadata } = action;
|
||||||
const selectedSuffix = action === selectedAction ? ' selected' : '';
|
const selectedSuffix = action === selectedAction ? ' selected' : '';
|
||||||
const highlightedSuffix = action === highlightedAction ? ' highlighted' : '';
|
const highlightedSuffix = action === highlightedAction ? ' highlighted' : '';
|
||||||
|
const page = modelUtil.page(action);
|
||||||
|
const { errors, warnings } = modelUtil.stats(action);
|
||||||
return <div
|
return <div
|
||||||
className={'action-entry' + selectedSuffix + highlightedSuffix}
|
className={'action-entry' + selectedSuffix + highlightedSuffix}
|
||||||
key={metadata.id}
|
key={metadata.id}
|
||||||
|
|
@ -79,10 +84,15 @@ export const ActionList: React.FC<ActionListProps> = ({
|
||||||
onMouseEnter={() => onHighlighted(action)}
|
onMouseEnter={() => onHighlighted(action)}
|
||||||
onMouseLeave={() => (highlightedAction === action) && onHighlighted(undefined)}
|
onMouseLeave={() => (highlightedAction === action) && onHighlighted(undefined)}
|
||||||
>
|
>
|
||||||
<div className={'action-error codicon codicon-issues'} hidden={!metadata.error} />
|
<div className='action-title'>
|
||||||
<div className='action-title'>{metadata.apiName || metadata.method}</div>
|
<span>{metadata.apiName}</span>
|
||||||
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>}
|
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>}
|
||||||
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
|
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
|
||||||
|
</div>
|
||||||
|
<div className='action-icons' onClick={() => setSelectedTab('console')}>
|
||||||
|
{!!errors && <div className='action-icon'><span className={'codicon codicon-error'}></span><span className="action-icon-value">{errors}</span></div>}
|
||||||
|
{!!warnings && <div className='action-icon'><span className={'codicon codicon-warning'}></span><span className="action-icon-value">{warnings}</span></div>}
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
background: #fff0f0;
|
background: #fff0f0;
|
||||||
border-top-color: #ffd6d6;
|
border-top-color: #ffd6d6;
|
||||||
border-bottom-color: #ffd6d6;
|
border-bottom-color: #ffd6d6;
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.console-line.warning {
|
.console-line.warning {
|
||||||
|
|
|
||||||
|
|
@ -17,34 +17,29 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './consoleTab.css';
|
import './consoleTab.css';
|
||||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||||
import { ContextEntry } from '../../../server/trace/viewer/traceModel';
|
|
||||||
import * as channels from '../../../protocol/channels';
|
import * as channels from '../../../protocol/channels';
|
||||||
|
import * as modelUtil from './modelUtil';
|
||||||
|
|
||||||
export const ConsoleTab: React.FunctionComponent<{
|
export const ConsoleTab: React.FunctionComponent<{
|
||||||
context: ContextEntry,
|
|
||||||
action: ActionTraceEvent | undefined,
|
action: ActionTraceEvent | undefined,
|
||||||
nextAction: ActionTraceEvent | undefined,
|
}> = ({ action }) => {
|
||||||
}> = ({ context, action, nextAction }) => {
|
|
||||||
const entries = React.useMemo(() => {
|
const entries = React.useMemo(() => {
|
||||||
if (!action)
|
if (!action)
|
||||||
return [];
|
return [];
|
||||||
const entries: { message?: channels.ConsoleMessageInitializer, error?: channels.SerializedError }[] = [];
|
const entries: { message?: channels.ConsoleMessageInitializer, error?: channels.SerializedError }[] = [];
|
||||||
for (const page of context.pages) {
|
const page = modelUtil.page(action);
|
||||||
for (const event of page.events) {
|
for (const event of modelUtil.eventsForAction(action)) {
|
||||||
if (event.metadata.method !== 'console' && event.metadata.method !== 'pageError')
|
if (event.metadata.method !== 'console' && event.metadata.method !== 'pageError')
|
||||||
continue;
|
continue;
|
||||||
if (event.metadata.startTime < action.metadata.startTime || (nextAction && event.metadata.startTime >= nextAction.metadata.startTime))
|
if (event.metadata.method === 'console') {
|
||||||
continue;
|
const { guid } = event.metadata.params.message;
|
||||||
if (event.metadata.method === 'console') {
|
entries.push({ message: page.objects[guid] });
|
||||||
const { guid } = event.metadata.params.message;
|
|
||||||
entries.push({ message: page.objects[guid] });
|
|
||||||
}
|
|
||||||
if (event.metadata.method === 'pageError')
|
|
||||||
entries.push({ error: event.metadata.params.error });
|
|
||||||
}
|
}
|
||||||
|
if (event.metadata.method === 'pageError')
|
||||||
|
entries.push({ error: event.metadata.params.error });
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
}, [context, action]);
|
}, [action]);
|
||||||
|
|
||||||
return <div className='console-tab'>{
|
return <div className='console-tab'>{
|
||||||
entries.map((entry, index) => {
|
entries.map((entry, index) => {
|
||||||
|
|
|
||||||
91
src/web/traceViewer/ui/modelUtil.ts
Normal file
91
src/web/traceViewer/ui/modelUtil.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ResourceSnapshot } from '../../../server/snapshot/snapshotTypes';
|
||||||
|
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||||
|
import { ContextEntry, PageEntry } from '../../../server/trace/viewer/traceModel';
|
||||||
|
|
||||||
|
const contextSymbol = Symbol('context');
|
||||||
|
const pageSymbol = Symbol('context');
|
||||||
|
const nextSymbol = Symbol('next');
|
||||||
|
const eventsSymbol = Symbol('events');
|
||||||
|
|
||||||
|
export function indexModel(context: ContextEntry) {
|
||||||
|
for (const page of context.pages) {
|
||||||
|
(page as any)[contextSymbol] = context;
|
||||||
|
for (let i = 0; i < page.actions.length; ++i) {
|
||||||
|
const action = page.actions[i] as any;
|
||||||
|
action[contextSymbol] = context;
|
||||||
|
action[pageSymbol] = page;
|
||||||
|
action[nextSymbol] = page.actions[i + 1];
|
||||||
|
}
|
||||||
|
for (const event of page.events) {
|
||||||
|
(event as any)[contextSymbol] = context;
|
||||||
|
(event as any)[pageSymbol] = page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function context(action: ActionTraceEvent): ContextEntry {
|
||||||
|
return (action as any)[contextSymbol];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function page(action: ActionTraceEvent): PageEntry {
|
||||||
|
return (action as any)[pageSymbol];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function next(action: ActionTraceEvent): ActionTraceEvent {
|
||||||
|
return (action as any)[nextSymbol];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stats(action: ActionTraceEvent): { errors: number, warnings: number } {
|
||||||
|
let errors = 0;
|
||||||
|
let warnings = 0;
|
||||||
|
const p = page(action);
|
||||||
|
for (const event of eventsForAction(action)) {
|
||||||
|
if (event.metadata.method === 'console') {
|
||||||
|
const { guid } = event.metadata.params.message;
|
||||||
|
const type = p.objects[guid].type;
|
||||||
|
if (type === 'warning')
|
||||||
|
++warnings;
|
||||||
|
else if (type === 'error')
|
||||||
|
++errors;
|
||||||
|
}
|
||||||
|
if (event.metadata.method === 'pageError')
|
||||||
|
++errors;
|
||||||
|
}
|
||||||
|
return { errors, warnings };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function eventsForAction(action: ActionTraceEvent): ActionTraceEvent[] {
|
||||||
|
let result: ActionTraceEvent[] = (action as any)[eventsSymbol];
|
||||||
|
if (result)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
const nextAction = next(action);
|
||||||
|
result = page(action).events.filter(event => {
|
||||||
|
return event.metadata.startTime >= action.metadata.startTime && (!nextAction || event.metadata.startTime < nextAction.metadata.startTime);
|
||||||
|
});
|
||||||
|
(action as any)[eventsSymbol] = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resourcesForAction(action: ActionTraceEvent): ResourceSnapshot[] {
|
||||||
|
const nextAction = next(action);
|
||||||
|
return context(action).resources.filter(resource => {
|
||||||
|
return resource.timestamp > action.metadata.startTime && (!nextAction || resource.timestamp < nextAction.metadata.startTime);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -14,23 +14,18 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
|
||||||
import { ContextEntry } from '../../../server/trace/viewer/traceModel';
|
|
||||||
import './networkTab.css';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { ActionTraceEvent } from '../../../server/trace/common/traceEvents';
|
||||||
|
import * as modelUtil from './modelUtil';
|
||||||
import { NetworkResourceDetails } from './networkResourceDetails';
|
import { NetworkResourceDetails } from './networkResourceDetails';
|
||||||
import { ResourceSnapshot } from '../../../server/snapshot/snapshotTypes';
|
import './networkTab.css';
|
||||||
|
|
||||||
export const NetworkTab: React.FunctionComponent<{
|
export const NetworkTab: React.FunctionComponent<{
|
||||||
context: ContextEntry,
|
|
||||||
action: ActionTraceEvent | undefined,
|
action: ActionTraceEvent | undefined,
|
||||||
nextAction: ActionTraceEvent | undefined,
|
}> = ({ action }) => {
|
||||||
}> = ({ context, action, nextAction }) => {
|
|
||||||
const [selected, setSelected] = React.useState(0);
|
const [selected, setSelected] = React.useState(0);
|
||||||
|
|
||||||
const resources: ResourceSnapshot[] = context.resources.filter(resource => {
|
const resources = action ? modelUtil.resourcesForAction(action) : [];
|
||||||
return action && resource.timestamp > action.metadata.startTime && (!nextAction || resource.timestamp < nextAction.metadata.startTime);
|
|
||||||
});
|
|
||||||
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} index={index} selected={selected === index} setSelected={setSelected} />;
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,16 @@ export interface TabbedPaneTab {
|
||||||
|
|
||||||
export const TabbedPane: React.FunctionComponent<{
|
export const TabbedPane: React.FunctionComponent<{
|
||||||
tabs: TabbedPaneTab[],
|
tabs: TabbedPaneTab[],
|
||||||
}> = ({ tabs }) => {
|
selectedTab: string,
|
||||||
const [selected, setSelected] = React.useState<string>(tabs.length ? tabs[0].id : '');
|
setSelectedTab: (tab: string) => void
|
||||||
|
}> = ({ tabs, selectedTab, setSelectedTab }) => {
|
||||||
return <div className='tabbed-pane'>
|
return <div className='tabbed-pane'>
|
||||||
<div className='vbox'>
|
<div className='vbox'>
|
||||||
<div className='hbox' style={{ flex: 'none' }}>
|
<div className='hbox' style={{ flex: 'none' }}>
|
||||||
<div className='tab-strip'>{
|
<div className='tab-strip'>{
|
||||||
tabs.map(tab => {
|
tabs.map(tab => {
|
||||||
return <div className={'tab-element ' + (selected === tab.id ? 'selected' : '')}
|
return <div className={'tab-element ' + (selectedTab === tab.id ? 'selected' : '')}
|
||||||
onClick={() => setSelected(tab.id)}
|
onClick={() => setSelectedTab(tab.id)}
|
||||||
key={tab.id}>
|
key={tab.id}>
|
||||||
<div className='tab-label'>{tab.title}</div>
|
<div className='tab-label'>{tab.title}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -42,7 +43,7 @@ export const TabbedPane: React.FunctionComponent<{
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
tabs.map(tab => {
|
tabs.map(tab => {
|
||||||
if (selected === tab.id)
|
if (selectedTab === tab.id)
|
||||||
return <div key={tab.id} className='tab-content'>{tab.render()}</div>;
|
return <div key={tab.id} className='tab-content'>{tab.render()}</div>;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import { LogsTab } from './logsTab';
|
||||||
import { SplitView } from '../../components/splitView';
|
import { SplitView } from '../../components/splitView';
|
||||||
import { useAsyncMemo } from './helpers';
|
import { useAsyncMemo } from './helpers';
|
||||||
import { ConsoleTab } from './consoleTab';
|
import { ConsoleTab } from './consoleTab';
|
||||||
|
import * as modelUtil from './modelUtil';
|
||||||
|
|
||||||
export const Workbench: React.FunctionComponent<{
|
export const Workbench: React.FunctionComponent<{
|
||||||
debugNames: string[],
|
debugNames: string[],
|
||||||
|
|
@ -37,20 +37,22 @@ export const Workbench: React.FunctionComponent<{
|
||||||
const [debugName, setDebugName] = React.useState(debugNames[0]);
|
const [debugName, setDebugName] = React.useState(debugNames[0]);
|
||||||
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
|
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||||
|
const [selectedTab, setSelectedTab] = React.useState<string>('logs');
|
||||||
|
|
||||||
let context = useAsyncMemo(async () => {
|
let context = useAsyncMemo(async () => {
|
||||||
if (!debugName)
|
if (!debugName)
|
||||||
return emptyContext;
|
return emptyContext;
|
||||||
return (await fetch(`/context/${debugName}`).then(response => response.json())) as ContextEntry;
|
const context = (await fetch(`/context/${debugName}`).then(response => response.json())) as ContextEntry;
|
||||||
|
modelUtil.indexModel(context);
|
||||||
|
return context;
|
||||||
}, [debugName], emptyContext);
|
}, [debugName], emptyContext);
|
||||||
|
|
||||||
const { actions, nextAction } = React.useMemo(() => {
|
const actions = React.useMemo(() => {
|
||||||
const actions: ActionTraceEvent[] = [];
|
const actions: ActionTraceEvent[] = [];
|
||||||
for (const page of context.pages)
|
for (const page of context.pages)
|
||||||
actions.push(...page.actions);
|
actions.push(...page.actions);
|
||||||
const nextAction = selectedAction ? actions[actions.indexOf(selectedAction) + 1] : undefined;
|
return actions;
|
||||||
return { actions, nextAction };
|
}, [context]);
|
||||||
}, [context, selectedAction]);
|
|
||||||
|
|
||||||
const snapshotSize = context.options.viewport || { width: 1280, height: 720 };
|
const snapshotSize = context.options.viewport || { width: 1280, height: 720 };
|
||||||
const boundaries = { minimum: context.startTime, maximum: context.endTime };
|
const boundaries = { minimum: context.startTime, maximum: context.endTime };
|
||||||
|
|
@ -58,6 +60,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
// Leave some nice free space on the right hand side.
|
// Leave some nice free space on the right hand side.
|
||||||
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
|
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
|
||||||
|
|
||||||
|
|
||||||
return <div className='vbox workbench'>
|
return <div className='vbox workbench'>
|
||||||
<div className='hbox header'>
|
<div className='hbox header'>
|
||||||
<div className='logo'>🎭</div>
|
<div className='logo'>🎭</div>
|
||||||
|
|
@ -87,10 +90,10 @@ export const Workbench: React.FunctionComponent<{
|
||||||
<SnapshotTab action={selectedAction} snapshotSize={snapshotSize} />
|
<SnapshotTab action={selectedAction} snapshotSize={snapshotSize} />
|
||||||
<TabbedPane tabs={[
|
<TabbedPane tabs={[
|
||||||
{ id: 'logs', title: 'Log', render: () => <LogsTab action={selectedAction} /> },
|
{ id: 'logs', title: 'Log', render: () => <LogsTab action={selectedAction} /> },
|
||||||
{ id: 'console', title: 'Console', render: () => <ConsoleTab context={context} action={selectedAction} nextAction={nextAction}/> },
|
{ id: 'console', title: 'Console', render: () => <ConsoleTab action={selectedAction} /> },
|
||||||
{ id: 'source', title: 'Source', render: () => <SourceTab action={selectedAction} /> },
|
{ id: 'source', title: 'Source', render: () => <SourceTab action={selectedAction} /> },
|
||||||
{ id: 'network', title: 'Network', render: () => <NetworkTab context={context} action={selectedAction} nextAction={nextAction}/> },
|
{ id: 'network', title: 'Network', render: () => <NetworkTab action={selectedAction} /> },
|
||||||
]}/>
|
]} selectedTab={selectedTab} setSelectedTab={setSelectedTab}/>
|
||||||
</SplitView>
|
</SplitView>
|
||||||
<ActionList
|
<ActionList
|
||||||
actions={actions}
|
actions={actions}
|
||||||
|
|
@ -100,6 +103,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
setSelectedAction(action);
|
setSelectedAction(action);
|
||||||
}}
|
}}
|
||||||
onHighlighted={action => setHighlightedAction(action)}
|
onHighlighted={action => setHighlightedAction(action)}
|
||||||
|
setSelectedTab={setSelectedTab}
|
||||||
/>
|
/>
|
||||||
</SplitView>
|
</SplitView>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,18 @@ class TraceViewerPage {
|
||||||
return await this.page.$$eval('.action-title:visible', ee => ee.map(e => e.textContent));
|
return await this.page.$$eval('.action-title:visible', ee => ee.map(e => e.textContent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async actionIconsText(action: string) {
|
||||||
|
const entry = await this.page.waitForSelector(`.action-entry:has-text("${action}")`);
|
||||||
|
await entry.waitForSelector('.action-icon-value:visible');
|
||||||
|
return await entry.$$eval('.action-icon-value:visible', ee => ee.map(e => e.textContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
async actionIcons(action: string) {
|
||||||
|
return await this.page.waitForSelector(`.action-entry:has-text("${action}") .action-icons`);
|
||||||
|
}
|
||||||
|
|
||||||
async selectAction(title: string) {
|
async selectAction(title: string) {
|
||||||
await this.page.click(`.action-title:text("${title}")`);
|
await this.page.click(`.action-title:has-text("${title}")`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async logLines() {
|
async logLines() {
|
||||||
|
|
@ -98,11 +108,6 @@ test.beforeAll(async ({ browser, browserName }, workerInfo) => {
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(),
|
||||||
page.waitForTimeout(200).then(() => page.goto('data:text/html,<html>Hello world 2</html>'))
|
page.waitForTimeout(200).then(() => page.goto('data:text/html,<html>Hello world 2</html>'))
|
||||||
]);
|
]);
|
||||||
await page.evaluate(() => {
|
|
||||||
console.log('Log');
|
|
||||||
console.warn('Warning');
|
|
||||||
console.error('Error');
|
|
||||||
});
|
|
||||||
await page.close();
|
await page.close();
|
||||||
traceFile = path.join(workerInfo.project.outputDir, browserName, 'trace.zip');
|
traceFile = path.join(workerInfo.project.outputDir, browserName, 'trace.zip');
|
||||||
await context.tracing.stop({ path: traceFile });
|
await context.tracing.stop({ path: traceFile });
|
||||||
|
|
@ -116,13 +121,12 @@ test('should show empty trace viewer', async ({ showTraceViewer }, testInfo) =>
|
||||||
test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer(traceFile);
|
const traceViewer = await showTraceViewer(traceFile);
|
||||||
expect(await traceViewer.actionTitles()).toEqual([
|
expect(await traceViewer.actionTitles()).toEqual([
|
||||||
'page.goto',
|
'page.gotodata:text/html,<html>Hello world</html>',
|
||||||
'page.setContent',
|
'page.setContent',
|
||||||
'page.evaluate',
|
'page.evaluate',
|
||||||
'page.click',
|
'page.click\"Click\"',
|
||||||
'page.waitForNavigation',
|
'page.waitForNavigation',
|
||||||
'page.goto',
|
'page.gotodata:text/html,<html>Hello world 2</html>',
|
||||||
'page.evaluate'
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -155,3 +159,12 @@ test('should render console', async ({ showTraceViewer, browserName }) => {
|
||||||
expect(stacks.length).toBe(1);
|
expect(stacks.length).toBe(1);
|
||||||
expect(stacks[0]).toContain('Error: Unhandled exception');
|
expect(stacks[0]).toContain('Error: Unhandled exception');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should open console errors on click', async ({ showTraceViewer, browserName }) => {
|
||||||
|
test.fixme(browserName === 'firefox', 'Firefox generates stray console message for page error');
|
||||||
|
const traceViewer = await showTraceViewer(traceFile);
|
||||||
|
expect(await traceViewer.actionIconsText('page.evaluate')).toEqual(['2', '1']);
|
||||||
|
expect(await traceViewer.page.isHidden('.console-tab'));
|
||||||
|
await (await traceViewer.actionIcons('page.evaluate')).click();
|
||||||
|
expect(await traceViewer.page.waitForSelector('.console-tab'));
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue