mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: add log/error tabs and counters (#26843)
This commit is contained in:
parent
ce3e0dcf84
commit
8c494e2519
@ -19,12 +19,14 @@ import './attachmentsTab.css';
|
||||
import { ImageDiffView } from '@web/components/imageDiffView';
|
||||
import type { TestAttachment } from '@web/components/imageDiffView';
|
||||
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
||||
import { PlaceholderPanel } from './placeholderPanel';
|
||||
|
||||
export const AttachmentsTab: React.FunctionComponent<{
|
||||
model: MultiTraceModel | undefined,
|
||||
}> = ({ model }) => {
|
||||
if (!model)
|
||||
return null;
|
||||
const attachments = model?.actions.map(a => a.attachments || []).flat() || [];
|
||||
if (!model || !attachments.length)
|
||||
return <PlaceholderPanel text='No attachments' />;
|
||||
return <div className='attachments-tab'>
|
||||
{ model.actions.map((action, index) => <AttachmentsSection key={index} action={action} />) }
|
||||
</div>;
|
||||
|
||||
@ -22,16 +22,14 @@ import './callTab.css';
|
||||
import { CopyToClipboard } from './copyToClipboard';
|
||||
import { asLocator } from '@isomorphic/locatorGenerators';
|
||||
import type { Language } from '@isomorphic/locatorGenerators';
|
||||
import { ErrorMessage } from '@web/components/errorMessage';
|
||||
import { PlaceholderPanel } from './placeholderPanel';
|
||||
|
||||
export const CallTab: React.FunctionComponent<{
|
||||
action: ActionTraceEvent | undefined,
|
||||
sdkLanguage: Language | undefined,
|
||||
}> = ({ action, sdkLanguage }) => {
|
||||
if (!action)
|
||||
return null;
|
||||
const logs = action.log;
|
||||
const error = action.error?.message;
|
||||
return <PlaceholderPanel text='No action selected' />;
|
||||
const params = { ...action.params };
|
||||
// Strip down the waitForEventInfo data, we never need it.
|
||||
delete params.info;
|
||||
@ -40,8 +38,6 @@ export const CallTab: React.FunctionComponent<{
|
||||
const duration = action.endTime ? msToString(action.endTime - action.startTime) : 'Timed Out';
|
||||
|
||||
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-section'>Time</div>
|
||||
@ -58,14 +54,6 @@ export const CallTab: React.FunctionComponent<{
|
||||
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>;
|
||||
};
|
||||
|
||||
|
||||
@ -23,8 +23,9 @@ import type { Boundaries } from '../geometry';
|
||||
import { msToString } from '@web/uiUtils';
|
||||
import { ansi2html } from '@web/ansi2html';
|
||||
import type * as trace from '@trace/trace';
|
||||
import { PlaceholderPanel } from './placeholderPanel';
|
||||
|
||||
type ConsoleEntry = {
|
||||
export type ConsoleEntry = {
|
||||
browserMessage?: trace.ConsoleMessageTraceEvent['initializer'],
|
||||
browserError?: channels.SerializedError;
|
||||
nodeMessage?: {
|
||||
@ -36,13 +37,14 @@ type ConsoleEntry = {
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
type ConsoleTabModel = {
|
||||
entries: ConsoleEntry[],
|
||||
};
|
||||
|
||||
const ConsoleListView = ListView<ConsoleEntry>;
|
||||
|
||||
export const ConsoleTab: React.FunctionComponent<{
|
||||
model: modelUtil.MultiTraceModel | undefined,
|
||||
boundaries: Boundaries,
|
||||
selectedTime: Boundaries | undefined,
|
||||
}> = ({ model, boundaries, selectedTime }) => {
|
||||
|
||||
export function useConsoleTabModel(model: modelUtil.MultiTraceModel | undefined, selectedTime: Boundaries | undefined): ConsoleTabModel {
|
||||
const { entries } = React.useMemo(() => {
|
||||
if (!model)
|
||||
return { entries: [] };
|
||||
@ -89,9 +91,20 @@ export const ConsoleTab: React.FunctionComponent<{
|
||||
return entries.filter(entry => entry.timestamp >= selectedTime.minimum && entry.timestamp <= selectedTime.maximum);
|
||||
}, [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'>
|
||||
<ConsoleListView
|
||||
items={filteredEntries}
|
||||
items={consoleModel.entries}
|
||||
isError={entry => entry.isError}
|
||||
isWarning={entry => entry.isWarning}
|
||||
render={entry => {
|
||||
|
||||
61
packages/trace-viewer/src/ui/errorsTab.tsx
Normal file
61
packages/trace-viewer/src/ui/errorsTab.tsx
Normal 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>;
|
||||
};
|
||||
34
packages/trace-viewer/src/ui/logTab.tsx
Normal file
34
packages/trace-viewer/src/ui/logTab.tsx
Normal 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}
|
||||
/>;
|
||||
};
|
||||
@ -18,25 +18,21 @@ import type { Entry } from '@trace/har';
|
||||
import { ListView } from '@web/components/listView';
|
||||
import * as React from 'react';
|
||||
import type { Boundaries } from '../geometry';
|
||||
import type * as modelUtil from './modelUtil';
|
||||
import './networkTab.css';
|
||||
import { NetworkResourceDetails } from './networkResourceDetails';
|
||||
import { bytesToString, msToString } from '@web/uiUtils';
|
||||
import { PlaceholderPanel } from './placeholderPanel';
|
||||
import type { MultiTraceModel } from './modelUtil';
|
||||
|
||||
const NetworkListView = ListView<Entry>;
|
||||
|
||||
type SortBy = 'start' | 'status' | 'method' | 'file' | 'duration' | 'size' | 'content-type';
|
||||
type Sorting = { by: SortBy, negate: boolean};
|
||||
type NetworkTabModel = {
|
||||
resources: Entry[],
|
||||
};
|
||||
|
||||
export const NetworkTab: React.FunctionComponent<{
|
||||
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);
|
||||
|
||||
export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedTime: Boundaries | undefined): NetworkTabModel {
|
||||
const resources = React.useMemo(() => {
|
||||
const resources = model?.resources || [];
|
||||
const filtered = resources.filter(resource => {
|
||||
@ -44,21 +40,37 @@ export const NetworkTab: React.FunctionComponent<{
|
||||
return true;
|
||||
return !!resource._monotonicTime && (resource._monotonicTime >= selectedTime.minimum && resource._monotonicTime <= selectedTime.maximum);
|
||||
});
|
||||
if (sorting)
|
||||
sort(filtered, sorting);
|
||||
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) => {
|
||||
setSorting({ by: f, negate: sorting?.by === f ? !sorting.negate : false });
|
||||
}, [sorting]);
|
||||
|
||||
if (!networkModel.resources.length)
|
||||
return <PlaceholderPanel text='No network calls' />;
|
||||
|
||||
return <>
|
||||
{!resource && <div className='vbox'>
|
||||
<NetworkHeader sorting={sorting} toggleSorting={toggleSorting} />
|
||||
<NetworkListView
|
||||
dataTestId='network-request-list'
|
||||
items={resources}
|
||||
items={networkModel.resources}
|
||||
render={entry => <NetworkResource boundaries={boundaries} resource={entry}></NetworkResource>}
|
||||
onSelected={setResource}
|
||||
onHighlighted={onEntryHovered}
|
||||
|
||||
30
packages/trace-viewer/src/ui/placeholderPanel.tsx
Normal file
30
packages/trace-viewer/src/ui/placeholderPanel.tsx
Normal 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>;
|
||||
};
|
||||
@ -18,10 +18,12 @@ import { SplitView } from '@web/components/splitView';
|
||||
import * as React from 'react';
|
||||
import { ActionList } from './actionList';
|
||||
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 { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
||||
import { NetworkTab } from './networkTab';
|
||||
import { NetworkTab, useNetworkTabModel } from './networkTab';
|
||||
import { SnapshotTab } from './snapshotTab';
|
||||
import { SourceTab } from './sourceTab';
|
||||
import { TabbedPane } from '@web/components/tabbedPane';
|
||||
@ -49,7 +51,7 @@ export const Workbench: React.FunctionComponent<{
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
||||
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
||||
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 [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
|
||||
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
||||
@ -83,13 +85,20 @@ export const Workbench: React.FunctionComponent<{
|
||||
setSelectedPropertiesTab(tab);
|
||||
if (tab !== 'inspector')
|
||||
setIsInspecting(false);
|
||||
}, []);
|
||||
}, [setSelectedPropertiesTab]);
|
||||
|
||||
const locatorPicked = React.useCallback((locator: string) => {
|
||||
setHighlightedLocator(locator);
|
||||
selectPropertiesTab('inspector');
|
||||
}, [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 inspectorTab: TabbedPaneTabModel = {
|
||||
@ -106,6 +115,17 @@ export const Workbench: React.FunctionComponent<{
|
||||
title: 'Call',
|
||||
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 = {
|
||||
id: 'source',
|
||||
title: 'Source',
|
||||
@ -119,34 +139,37 @@ export const Workbench: React.FunctionComponent<{
|
||||
const consoleTab: TabbedPaneTabModel = {
|
||||
id: '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 = {
|
||||
id: '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 = {
|
||||
id: 'attachments',
|
||||
title: 'Attachments',
|
||||
count: attachments.length,
|
||||
render: () => <AttachmentsTab model={model} />
|
||||
};
|
||||
|
||||
const tabs: TabbedPaneTabModel[] = showSourcesFirst ? [
|
||||
inspectorTab,
|
||||
sourceTab,
|
||||
consoleTab,
|
||||
networkTab,
|
||||
callTab,
|
||||
attachmentsTab,
|
||||
] : [
|
||||
const tabs: TabbedPaneTabModel[] = [
|
||||
inspectorTab,
|
||||
callTab,
|
||||
logTab,
|
||||
errorsTab,
|
||||
consoleTab,
|
||||
networkTab,
|
||||
sourceTab,
|
||||
attachmentsTab,
|
||||
];
|
||||
if (showSourcesFirst) {
|
||||
const sourceTabIndex = tabs.indexOf(sourceTab);
|
||||
tabs.splice(sourceTabIndex, 1);
|
||||
tabs.splice(1, 0, sourceTab);
|
||||
}
|
||||
|
||||
const { boundaries } = React.useMemo(() => {
|
||||
const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 };
|
||||
|
||||
@ -81,6 +81,14 @@ svg {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.hbox {
|
||||
display: flex;
|
||||
flex: auto;
|
||||
|
||||
@ -20,5 +20,5 @@
|
||||
font-size: var(--vscode-editor-font-size);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@ -28,13 +28,13 @@
|
||||
display: flex;
|
||||
flex: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tabbed-pane-tab {
|
||||
padding: 2px 10px 0 10px;
|
||||
padding: 2px 6px 0 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
@ -54,3 +54,21 @@
|
||||
.tabbed-pane-tab.selected {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -20,7 +20,9 @@ import * as React from 'react';
|
||||
|
||||
export interface TabbedPaneTabModel {
|
||||
id: string;
|
||||
title: string | JSX.Element;
|
||||
title: string;
|
||||
count?: number;
|
||||
errorCount?: number;
|
||||
component?: React.ReactElement;
|
||||
render?: () => React.ReactElement;
|
||||
}
|
||||
@ -44,6 +46,8 @@ export const TabbedPane: React.FunctionComponent<{
|
||||
<TabbedPaneTab
|
||||
id={tab.id}
|
||||
title={tab.title}
|
||||
count={tab.count}
|
||||
errorCount={tab.errorCount}
|
||||
selected={selectedTab === tab.id}
|
||||
onSelect={setSelectedTab}
|
||||
></TabbedPaneTab>)),
|
||||
@ -67,13 +71,18 @@ export const TabbedPane: React.FunctionComponent<{
|
||||
|
||||
export const TabbedPaneTab: React.FunctionComponent<{
|
||||
id: string,
|
||||
title: string | JSX.Element,
|
||||
title: string,
|
||||
count?: number,
|
||||
errorCount?: number,
|
||||
selected?: boolean,
|
||||
onSelect: (id: string) => void
|
||||
}> = ({ id, title, selected, onSelect }) => {
|
||||
}> = ({ id, title, count, errorCount, selected, onSelect }) => {
|
||||
return <div className={'tabbed-pane-tab ' + (selected ? 'selected' : '')}
|
||||
onClick={() => onSelect(id)}
|
||||
title={title}
|
||||
key={id}>
|
||||
<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>;
|
||||
};
|
||||
|
||||
@ -38,6 +38,7 @@ class TraceViewerPage {
|
||||
actionTitles: Locator;
|
||||
callLines: Locator;
|
||||
consoleLines: Locator;
|
||||
logLines: Locator;
|
||||
consoleLineMessages: Locator;
|
||||
consoleStacks: Locator;
|
||||
stackFrames: Locator;
|
||||
@ -47,6 +48,7 @@ class TraceViewerPage {
|
||||
constructor(public page: Page) {
|
||||
this.actionTitles = page.locator('.action-title');
|
||||
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.consoleLineMessages = page.locator('.console-line-message');
|
||||
this.consoleStacks = page.locator('.console-stack');
|
||||
|
||||
@ -122,7 +122,8 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
|
||||
test('should contain action info', async ({ showTraceViewer }) => {
|
||||
const traceViewer = await showTraceViewer([traceFile]);
|
||||
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).toContain('attempting click action');
|
||||
expect(logLines).toContain(' click action done');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user