chore: add log/error tabs and counters (#26843)

This commit is contained in:
Pavel Feldman 2023-09-01 20:12:05 -07:00 committed by GitHub
parent ce3e0dcf84
commit 8c494e2519
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 259 additions and 58 deletions

View file

@ -19,12 +19,14 @@ 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, MultiTraceModel } from './modelUtil'; import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
import { PlaceholderPanel } from './placeholderPanel';
export const AttachmentsTab: React.FunctionComponent<{ export const AttachmentsTab: React.FunctionComponent<{
model: MultiTraceModel | undefined, model: MultiTraceModel | undefined,
}> = ({ model }) => { }> = ({ model }) => {
if (!model) const attachments = model?.actions.map(a => a.attachments || []).flat() || [];
return null; if (!model || !attachments.length)
return <PlaceholderPanel text='No attachments' />;
return <div className='attachments-tab'> return <div className='attachments-tab'>
{ model.actions.map((action, index) => <AttachmentsSection key={index} action={action} />) } { model.actions.map((action, index) => <AttachmentsSection key={index} action={action} />) }
</div>; </div>;

View file

@ -22,16 +22,14 @@ import './callTab.css';
import { CopyToClipboard } from './copyToClipboard'; import { CopyToClipboard } from './copyToClipboard';
import { asLocator } from '@isomorphic/locatorGenerators'; import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators'; import type { Language } from '@isomorphic/locatorGenerators';
import { ErrorMessage } from '@web/components/errorMessage'; import { PlaceholderPanel } from './placeholderPanel';
export const CallTab: React.FunctionComponent<{ export const CallTab: React.FunctionComponent<{
action: ActionTraceEvent | undefined, action: ActionTraceEvent | undefined,
sdkLanguage: Language | undefined, sdkLanguage: Language | undefined,
}> = ({ action, sdkLanguage }) => { }> = ({ action, sdkLanguage }) => {
if (!action) if (!action)
return null; return <PlaceholderPanel text='No action selected' />;
const logs = action.log;
const error = action.error?.message;
const params = { ...action.params }; const params = { ...action.params };
// Strip down the waitForEventInfo data, we never need it. // Strip down the waitForEventInfo data, we never need it.
delete params.info; delete params.info;
@ -40,8 +38,6 @@ export const CallTab: React.FunctionComponent<{
const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out'; const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out';
return <div className='call-tab'> return <div className='call-tab'>
{!!error && <ErrorMessage error={error} />}
{!!error && <div className='call-section'>Call</div>}
<div className='call-line'>{action.apiName}</div> <div className='call-line'>{action.apiName}</div>
{<> {<>
<div className='call-section'>Time</div> <div className='call-section'>Time</div>
@ -58,14 +54,6 @@ export const CallTab: React.FunctionComponent<{
renderProperty(propertyToString(action, name, action.result[name], sdkLanguage), 'result-' + index) renderProperty(propertyToString(action, name, action.result[name], sdkLanguage), 'result-' + index)
) )
} }
<div className='call-section'>Log</div>
{
logs.map((logLine, index) => {
return <div key={index} className='call-line'>
{logLine}
</div>;
})
}
</div>; </div>;
}; };

View file

@ -23,8 +23,9 @@ import type { Boundaries } from '../geometry';
import { msToString } from '@web/uiUtils'; import { msToString } from '@web/uiUtils';
import { ansi2html } from '@web/ansi2html'; import { ansi2html } from '@web/ansi2html';
import type * as trace from '@trace/trace'; import type * as trace from '@trace/trace';
import { PlaceholderPanel } from './placeholderPanel';
type ConsoleEntry = { export type ConsoleEntry = {
browserMessage?: trace.ConsoleMessageTraceEvent['initializer'], browserMessage?: trace.ConsoleMessageTraceEvent['initializer'],
browserError?: channels.SerializedError; browserError?: channels.SerializedError;
nodeMessage?: { nodeMessage?: {
@ -36,13 +37,14 @@ type ConsoleEntry = {
timestamp: number; timestamp: number;
}; };
type ConsoleTabModel = {
entries: ConsoleEntry[],
};
const ConsoleListView = ListView<ConsoleEntry>; const ConsoleListView = ListView<ConsoleEntry>;
export const ConsoleTab: React.FunctionComponent<{
model: modelUtil.MultiTraceModel | undefined, export function useConsoleTabModel(model: modelUtil.MultiTraceModel | undefined, selectedTime: Boundaries | undefined): ConsoleTabModel {
boundaries: Boundaries,
selectedTime: Boundaries | undefined,
}> = ({ model, boundaries, selectedTime }) => {
const { entries } = React.useMemo(() => { const { entries } = React.useMemo(() => {
if (!model) if (!model)
return { entries: [] }; return { entries: [] };
@ -89,9 +91,20 @@ export const ConsoleTab: React.FunctionComponent<{
return entries.filter(entry => entry.timestamp >= selectedTime.minimum && entry.timestamp <= selectedTime.maximum); return entries.filter(entry => entry.timestamp >= selectedTime.minimum && entry.timestamp <= selectedTime.maximum);
}, [entries, selectedTime]); }, [entries, selectedTime]);
return { entries: filteredEntries };
}
export const ConsoleTab: React.FunctionComponent<{
boundaries: Boundaries,
consoleModel: ConsoleTabModel,
selectedTime: Boundaries | undefined,
}> = ({ consoleModel, boundaries }) => {
if (!consoleModel.entries.length)
return <PlaceholderPanel text='No console entries' />;
return <div className='console-tab'> return <div className='console-tab'>
<ConsoleListView <ConsoleListView
items={filteredEntries} items={consoleModel.entries}
isError={entry => entry.isError} isError={entry => entry.isError}
isWarning={entry => entry.isWarning} isWarning={entry => entry.isWarning}
render={entry => { render={entry => {

View file

@ -0,0 +1,61 @@
/**
* 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 { ErrorMessage } from '@web/components/errorMessage';
import * as React from 'react';
import type * as modelUtil from './modelUtil';
import { PlaceholderPanel } from './placeholderPanel';
import { renderAction } from './actionList';
import type { Language } from '@isomorphic/locatorGenerators';
import type { Boundaries } from '../geometry';
import { msToString } from '@web/uiUtils';
type ErrorsTabModel = {
errors: Map<string, modelUtil.ActionTraceEventInContext>;
};
export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined): ErrorsTabModel {
return React.useMemo(() => {
const errors = new Map<string, modelUtil.ActionTraceEventInContext>();
for (const action of model?.actions || []) {
// Overwrite errors with the last one.
if (action.error?.message)
errors.set(action.error.message, action);
}
return { errors };
}, [model]);
}
export const ErrorsTab: React.FunctionComponent<{
errorsModel: ErrorsTabModel,
sdkLanguage: Language,
boundaries: Boundaries,
}> = ({ errorsModel, sdkLanguage, boundaries }) => {
if (!errorsModel.errors.size)
return <PlaceholderPanel text='No errors' />;
return <div className='fill' style={{ overflow: 'auto ' }}>
{[...errorsModel.errors.entries()].map(([message, action]) => {
return <div key={message}>
<div className='hbox' style={{ alignItems: 'center', padding: 5 }}>
<div style={{ color: 'var(--vscode-editorCodeLens-foreground)', marginRight: 5 }}>{msToString(action.startTime - boundaries.minimum)}</div>
{renderAction(action, sdkLanguage)}
</div>
<ErrorMessage error={message} />
</div>;
})}
</div>;
};

View file

@ -0,0 +1,34 @@
/**
* 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 type { ActionTraceEvent } from '@trace/trace';
import * as React from 'react';
import { ListView } from '@web/components/listView';
import { PlaceholderPanel } from './placeholderPanel';
const LogList = ListView<string>;
export const LogTab: React.FunctionComponent<{
action: ActionTraceEvent | undefined,
}> = ({ action }) => {
if (!action?.log.length)
return <PlaceholderPanel text='No log entries' />;
return <LogList
dataTestId='log-list'
items={action?.log || []}
render={logLine => logLine}
/>;
};

View file

@ -18,25 +18,21 @@ import type { Entry } from '@trace/har';
import { ListView } from '@web/components/listView'; import { ListView } from '@web/components/listView';
import * as React from 'react'; import * as React from 'react';
import type { Boundaries } from '../geometry'; import type { Boundaries } from '../geometry';
import type * as modelUtil from './modelUtil';
import './networkTab.css'; import './networkTab.css';
import { NetworkResourceDetails } from './networkResourceDetails'; import { NetworkResourceDetails } from './networkResourceDetails';
import { bytesToString, msToString } from '@web/uiUtils'; import { bytesToString, msToString } from '@web/uiUtils';
import { PlaceholderPanel } from './placeholderPanel';
import type { MultiTraceModel } from './modelUtil';
const NetworkListView = ListView<Entry>; const NetworkListView = ListView<Entry>;
type SortBy = 'start' | 'status' | 'method' | 'file' | 'duration' | 'size' | 'content-type'; type SortBy = 'start' | 'status' | 'method' | 'file' | 'duration' | 'size' | 'content-type';
type Sorting = { by: SortBy, negate: boolean}; type Sorting = { by: SortBy, negate: boolean};
type NetworkTabModel = {
resources: Entry[],
};
export const NetworkTab: React.FunctionComponent<{ export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedTime: Boundaries | undefined): NetworkTabModel {
model: modelUtil.MultiTraceModel | undefined,
boundaries: Boundaries,
selectedTime: Boundaries | undefined,
onEntryHovered: (entry: Entry | undefined) => void,
}> = ({ model, boundaries, selectedTime, onEntryHovered }) => {
const [resource, setResource] = React.useState<Entry | undefined>();
const [sorting, setSorting] = React.useState<Sorting | undefined>(undefined);
const resources = React.useMemo(() => { const resources = React.useMemo(() => {
const resources = model?.resources || []; const resources = model?.resources || [];
const filtered = resources.filter(resource => { const filtered = resources.filter(resource => {
@ -44,21 +40,37 @@ export const NetworkTab: React.FunctionComponent<{
return true; return true;
return !!resource._monotonicTime && (resource._monotonicTime >= selectedTime.minimum && resource._monotonicTime <= selectedTime.maximum); return !!resource._monotonicTime && (resource._monotonicTime >= selectedTime.minimum && resource._monotonicTime <= selectedTime.maximum);
}); });
if (sorting)
sort(filtered, sorting);
return filtered; return filtered;
}, [sorting, model, selectedTime]); }, [model, selectedTime]);
return { resources };
}
export const NetworkTab: React.FunctionComponent<{
boundaries: Boundaries,
networkModel: NetworkTabModel,
onEntryHovered: (entry: Entry | undefined) => void,
}> = ({ boundaries, networkModel, onEntryHovered }) => {
const [resource, setResource] = React.useState<Entry | undefined>();
const [sorting, setSorting] = React.useState<Sorting | undefined>(undefined);
React.useMemo(() => {
if (sorting)
sort(networkModel.resources, sorting);
}, [networkModel.resources, sorting]);
const toggleSorting = React.useCallback((f: SortBy) => { const toggleSorting = React.useCallback((f: SortBy) => {
setSorting({ by: f, negate: sorting?.by === f ? !sorting.negate : false }); setSorting({ by: f, negate: sorting?.by === f ? !sorting.negate : false });
}, [sorting]); }, [sorting]);
if (!networkModel.resources.length)
return <PlaceholderPanel text='No network calls' />;
return <> return <>
{!resource && <div className='vbox'> {!resource && <div className='vbox'>
<NetworkHeader sorting={sorting} toggleSorting={toggleSorting} /> <NetworkHeader sorting={sorting} toggleSorting={toggleSorting} />
<NetworkListView <NetworkListView
dataTestId='network-request-list' dataTestId='network-request-list'
items={resources} items={networkModel.resources}
render={entry => <NetworkResource boundaries={boundaries} resource={entry}></NetworkResource>} render={entry => <NetworkResource boundaries={boundaries} resource={entry}></NetworkResource>}
onSelected={setResource} onSelected={setResource}
onHighlighted={onEntryHovered} onHighlighted={onEntryHovered}

View file

@ -0,0 +1,30 @@
/**
* 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 * as React from 'react';
export const PlaceholderPanel: React.FunctionComponent<{
text: string,
}> = ({ text }) => {
return <div className='fill' style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 24,
fontWeight: 'bold',
opacity: 0.5,
}}>{text}</div>;
};

View file

@ -18,10 +18,12 @@ import { SplitView } from '@web/components/splitView';
import * as React from 'react'; 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 { LogTab } from './logTab';
import { ErrorsTab, useErrorsTabModel } from './errorsTab';
import { ConsoleTab, useConsoleTabModel } from './consoleTab';
import type * 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, useNetworkTabModel } from './networkTab';
import { SnapshotTab } from './snapshotTab'; import { SnapshotTab } from './snapshotTab';
import { SourceTab } from './sourceTab'; import { SourceTab } from './sourceTab';
import { TabbedPane } from '@web/components/tabbedPane'; import { TabbedPane } from '@web/components/tabbedPane';
@ -49,7 +51,7 @@ export const Workbench: React.FunctionComponent<{
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>(); const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>(); const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions'); const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>(showSourcesFirst ? 'source' : 'call'); const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('propertiesTab', showSourcesFirst ? 'source' : 'call');
const [isInspecting, setIsInspecting] = React.useState(false); const [isInspecting, setIsInspecting] = React.useState(false);
const [highlightedLocator, setHighlightedLocator] = React.useState<string>(''); const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
const activeAction = model ? highlightedAction || selectedAction : undefined; const activeAction = model ? highlightedAction || selectedAction : undefined;
@ -83,13 +85,20 @@ export const Workbench: React.FunctionComponent<{
setSelectedPropertiesTab(tab); setSelectedPropertiesTab(tab);
if (tab !== 'inspector') if (tab !== 'inspector')
setIsInspecting(false); setIsInspecting(false);
}, []); }, [setSelectedPropertiesTab]);
const locatorPicked = React.useCallback((locator: string) => { const locatorPicked = React.useCallback((locator: string) => {
setHighlightedLocator(locator); setHighlightedLocator(locator);
selectPropertiesTab('inspector'); selectPropertiesTab('inspector');
}, [selectPropertiesTab]); }, [selectPropertiesTab]);
const consoleModel = useConsoleTabModel(model, selectedTime);
const networkModel = useNetworkTabModel(model, selectedTime);
const errorsModel = useErrorsTabModel(model);
const attachments = React.useMemo(() => {
return model?.actions.map(a => a.attachments || []).flat() || [];
}, [model]);
const sdkLanguage = model?.sdkLanguage || 'javascript'; const sdkLanguage = model?.sdkLanguage || 'javascript';
const inspectorTab: TabbedPaneTabModel = { const inspectorTab: TabbedPaneTabModel = {
@ -106,6 +115,17 @@ export const Workbench: React.FunctionComponent<{
title: 'Call', title: 'Call',
render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} /> render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} />
}; };
const logTab: TabbedPaneTabModel = {
id: 'log',
title: 'Log',
render: () => <LogTab action={activeAction} />
};
const errorsTab: TabbedPaneTabModel = {
id: 'errors',
title: 'Errors',
errorCount: errorsModel.errors.size,
render: () => <ErrorsTab errorsModel={errorsModel} sdkLanguage={sdkLanguage} boundaries={boundaries} />
};
const sourceTab: TabbedPaneTabModel = { const sourceTab: TabbedPaneTabModel = {
id: 'source', id: 'source',
title: 'Source', title: 'Source',
@ -119,34 +139,37 @@ export const Workbench: React.FunctionComponent<{
const consoleTab: TabbedPaneTabModel = { const consoleTab: TabbedPaneTabModel = {
id: 'console', id: 'console',
title: 'Console', title: 'Console',
render: () => <ConsoleTab model={model} boundaries={boundaries} selectedTime={selectedTime} /> count: consoleModel.entries.length,
render: () => <ConsoleTab consoleModel={consoleModel} boundaries={boundaries} selectedTime={selectedTime} />
}; };
const networkTab: TabbedPaneTabModel = { const networkTab: TabbedPaneTabModel = {
id: 'network', id: 'network',
title: 'Network', title: 'Network',
render: () => <NetworkTab model={model} boundaries={boundaries} selectedTime={selectedTime} onEntryHovered={setHighlightedEntry}/> count: networkModel.resources.length,
render: () => <NetworkTab boundaries={boundaries} networkModel={networkModel} onEntryHovered={setHighlightedEntry}/>
}; };
const attachmentsTab: TabbedPaneTabModel = { const attachmentsTab: TabbedPaneTabModel = {
id: 'attachments', id: 'attachments',
title: 'Attachments', title: 'Attachments',
count: attachments.length,
render: () => <AttachmentsTab model={model} /> render: () => <AttachmentsTab model={model} />
}; };
const tabs: TabbedPaneTabModel[] = showSourcesFirst ? [ const tabs: TabbedPaneTabModel[] = [
inspectorTab,
sourceTab,
consoleTab,
networkTab,
callTab,
attachmentsTab,
] : [
inspectorTab, inspectorTab,
callTab, callTab,
logTab,
errorsTab,
consoleTab, consoleTab,
networkTab, networkTab,
sourceTab, sourceTab,
attachmentsTab, attachmentsTab,
]; ];
if (showSourcesFirst) {
const sourceTabIndex = tabs.indexOf(sourceTab);
tabs.splice(sourceTabIndex, 1);
tabs.splice(1, 0, sourceTab);
}
const { boundaries } = React.useMemo(() => { const { boundaries } = React.useMemo(() => {
const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 }; const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 };

View file

@ -81,6 +81,14 @@ svg {
position: relative; position: relative;
} }
.fill {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.hbox { .hbox {
display: flex; display: flex;
flex: auto; flex: auto;

View file

@ -20,5 +20,5 @@
font-size: var(--vscode-editor-font-size); font-size: var(--vscode-editor-font-size);
background-color: var(--vscode-inputValidation-errorBackground); background-color: var(--vscode-inputValidation-errorBackground);
white-space: pre; white-space: pre;
overflow: auto; padding: 10px;
} }

View file

@ -28,13 +28,13 @@
display: flex; display: flex;
flex: auto; flex: auto;
overflow: hidden; overflow: hidden;
position: relative;
} }
.tabbed-pane-tab { .tabbed-pane-tab {
padding: 2px 10px 0 10px; padding: 2px 6px 0 6px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
flex: none;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
user-select: none; user-select: none;
@ -54,3 +54,21 @@
.tabbed-pane-tab.selected { .tabbed-pane-tab.selected {
background-color: var(--vscode-tab-activeBackground); background-color: var(--vscode-tab-activeBackground);
} }
.tabbed-pane-tab-counter {
padding: 0 4px;
background: var(--vscode-menu-separatorBackground);
border-radius: 8px;
height: 16px;
margin-left: 4px;
line-height: 16px;
min-width: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.tabbed-pane-tab-counter.error {
background: var(--vscode-list-errorForeground);
color: var(--vscode-button-foreground);
}

View file

@ -20,7 +20,9 @@ import * as React from 'react';
export interface TabbedPaneTabModel { export interface TabbedPaneTabModel {
id: string; id: string;
title: string | JSX.Element; title: string;
count?: number;
errorCount?: number;
component?: React.ReactElement; component?: React.ReactElement;
render?: () => React.ReactElement; render?: () => React.ReactElement;
} }
@ -44,6 +46,8 @@ export const TabbedPane: React.FunctionComponent<{
<TabbedPaneTab <TabbedPaneTab
id={tab.id} id={tab.id}
title={tab.title} title={tab.title}
count={tab.count}
errorCount={tab.errorCount}
selected={selectedTab === tab.id} selected={selectedTab === tab.id}
onSelect={setSelectedTab} onSelect={setSelectedTab}
></TabbedPaneTab>)), ></TabbedPaneTab>)),
@ -67,13 +71,18 @@ 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,
count?: number,
errorCount?: number,
selected?: boolean, selected?: boolean,
onSelect: (id: string) => void onSelect: (id: string) => void
}> = ({ id, title, selected, onSelect }) => { }> = ({ id, title, count, errorCount, selected, onSelect }) => {
return <div className={'tabbed-pane-tab ' + (selected ? 'selected' : '')} return <div className={'tabbed-pane-tab ' + (selected ? 'selected' : '')}
onClick={() => onSelect(id)} onClick={() => onSelect(id)}
title={title}
key={id}> key={id}>
<div className='tabbed-pane-tab-label'>{title}</div> <div className='tabbed-pane-tab-label'>{title}</div>
{!!count && <div className='tabbed-pane-tab-counter'>{count}</div>}
{!!errorCount && <div className='tabbed-pane-tab-counter error'>{errorCount}</div>}
</div>; </div>;
}; };

View file

@ -38,6 +38,7 @@ class TraceViewerPage {
actionTitles: Locator; actionTitles: Locator;
callLines: Locator; callLines: Locator;
consoleLines: Locator; consoleLines: Locator;
logLines: Locator;
consoleLineMessages: Locator; consoleLineMessages: Locator;
consoleStacks: Locator; consoleStacks: Locator;
stackFrames: Locator; stackFrames: Locator;
@ -47,6 +48,7 @@ class TraceViewerPage {
constructor(public page: Page) { constructor(public page: Page) {
this.actionTitles = page.locator('.action-title'); this.actionTitles = page.locator('.action-title');
this.callLines = page.locator('.call-tab .call-line'); this.callLines = page.locator('.call-tab .call-line');
this.logLines = page.getByTestId('log-list').locator('.list-view-entry');
this.consoleLines = page.locator('.console-line'); this.consoleLines = page.locator('.console-line');
this.consoleLineMessages = page.locator('.console-line-message'); this.consoleLineMessages = page.locator('.console-line-message');
this.consoleStacks = page.locator('.console-stack'); this.consoleStacks = page.locator('.console-stack');

View file

@ -122,7 +122,8 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
test('should contain action info', async ({ showTraceViewer }) => { test('should contain action info', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('locator.click'); await traceViewer.selectAction('locator.click');
const logLines = await traceViewer.callLines.allTextContents(); await traceViewer.page.getByText('Log', { exact: true }).click();
const logLines = await traceViewer.logLines.allTextContents();
expect(logLines.length).toBeGreaterThan(10); expect(logLines.length).toBeGreaterThan(10);
expect(logLines).toContain('attempting click action'); expect(logLines).toContain('attempting click action');
expect(logLines).toContain(' click action done'); expect(logLines).toContain(' click action done');