diff --git a/packages/trace-viewer/src/ui/errorsTab.tsx b/packages/trace-viewer/src/ui/errorsTab.tsx index 71b6059c29..acf5bf838e 100644 --- a/packages/trace-viewer/src/ui/errorsTab.tsx +++ b/packages/trace-viewer/src/ui/errorsTab.tsx @@ -22,7 +22,7 @@ import { renderAction } from './actionList'; import type { Language } from '@isomorphic/locatorGenerators'; import type { StackFrame } from '@protocol/channels'; -type ErrorDescription = { +export type ErrorDescription = { action?: modelUtil.ActionTraceEventInContext; stack?: StackFrame[]; }; diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index 0c22d954f4..a544d4dc3f 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -342,10 +342,6 @@ export function buildActionTree(actions: ActionTraceEventInContext[]): { rootIte return { rootItem, itemMap }; } -export function idForAction(action: ActionTraceEvent) { - return `${action.pageId || 'none'}:${action.callId}`; -} - export function context(action: ActionTraceEvent | trace.EventTraceEvent | ResourceSnapshot): ContextEntry { return (action as any)[contextSymbol]; } diff --git a/packages/trace-viewer/src/ui/uiModeTraceView.tsx b/packages/trace-viewer/src/ui/uiModeTraceView.tsx index 48fd59e3ca..c0763fee3d 100644 --- a/packages/trace-viewer/src/ui/uiModeTraceView.tsx +++ b/packages/trace-viewer/src/ui/uiModeTraceView.tsx @@ -16,14 +16,13 @@ import { artifactsFolderName } from '@testIsomorphic/folders'; import type { TreeItem } from '@testIsomorphic/testTree'; -import type { ActionTraceEvent } from '@trace/trace'; import '@web/common.css'; import '@web/third_party/vscode/codicon.css'; import type * as reporterTypes from 'playwright/types/testReporter'; import React from 'react'; import type { ContextEntry } from '../entries'; import type { SourceLocation } from './modelUtil'; -import { idForAction, MultiTraceModel } from './modelUtil'; +import { MultiTraceModel } from './modelUtil'; import { Workbench } from './workbench'; export const TraceView: React.FC<{ @@ -42,12 +41,6 @@ export const TraceView: React.FC<{ return { outputDir }; }, [item]); - // Preserve user selection upon live-reloading trace model by persisting the action id. - // This avoids auto-selection of the last action every time we reload the model. - const [selectedActionId, setSelectedActionId] = React.useState(); - const onSelectionChanged = React.useCallback((action: ActionTraceEvent) => setSelectedActionId(idForAction(action)), [setSelectedActionId]); - const initialSelection = selectedActionId ? model?.model.actions.find(a => idForAction(a) === selectedActionId) : undefined; - React.useEffect(() => { if (pollTimer.current) clearTimeout(pollTimer.current); @@ -98,8 +91,6 @@ export const TraceView: React.FC<{ model={model?.model} showSourcesFirst={true} rootDir={rootDir} - initialSelection={initialSelection} - onSelectionChanged={onSelectionChanged} fallbackLocation={item.testFile} isLive={model?.isLive} status={item.treeItem?.status} diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 3565eb3850..e1ce2298ae 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -20,11 +20,11 @@ import { ActionList } from './actionList'; import { CallTab } from './callTab'; import { LogTab } from './logTab'; import { ErrorsTab, useErrorsTabModel } from './errorsTab'; +import type { ErrorDescription } from './errorsTab'; import type { ConsoleEntry } from './consoleTab'; import { ConsoleTab, useConsoleTabModel } from './consoleTab'; import type * as modelUtil from './modelUtil'; import { isRouteAction } from './modelUtil'; -import type { StackFrame } from '@protocol/channels'; import { NetworkTab, useNetworkTabModel } from './networkTab'; import { SnapshotTab } from './snapshotTab'; import { SourceTab } from './sourceTab'; @@ -49,8 +49,6 @@ export const Workbench: React.FunctionComponent<{ showSourcesFirst?: boolean, rootDir?: string, fallbackLocation?: modelUtil.SourceLocation, - initialSelection?: modelUtil.ActionTraceEventInContext, - onSelectionChanged?: (action: modelUtil.ActionTraceEventInContext) => void, isLive?: boolean, status?: UITestStatus, annotations?: { type: string; description?: string; }[]; @@ -59,9 +57,10 @@ export const Workbench: React.FunctionComponent<{ onOpenExternally?: (location: modelUtil.SourceLocation) => void, revealSource?: boolean, showSettings?: boolean, -}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => { - const [selectedAction, setSelectedActionImpl] = React.useState(undefined); - const [revealedStack, setRevealedStack] = React.useState(undefined); +}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => { + const [selectedCallId, setSelectedCallId] = React.useState(undefined); + const [revealedError, setRevealedError] = React.useState(undefined); + const [highlightedAction, setHighlightedAction] = React.useState(); const [highlightedEntry, setHighlightedEntry] = React.useState(); const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState(); @@ -69,38 +68,39 @@ export const Workbench: React.FunctionComponent<{ const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting('propertiesTab', showSourcesFirst ? 'source' : 'call'); const [isInspecting, setIsInspectingState] = React.useState(false); const [highlightedLocator, setHighlightedLocator] = React.useState(''); - const activeAction = model ? highlightedAction || selectedAction : undefined; const [selectedTime, setSelectedTime] = React.useState(); const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom'); const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true); const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false); - const filteredActions = React.useMemo(() => { return (model?.actions || []).filter(action => showRouteActions || !isRouteAction(action)); }, [model, showRouteActions]); const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => { - setSelectedActionImpl(action); - setRevealedStack(action?.stack); - }, [setSelectedActionImpl, setRevealedStack]); + setSelectedCallId(action?.callId); + setRevealedError(undefined); + }, []); const sources = React.useMemo(() => model?.sources || new Map(), [model]); React.useEffect(() => { setSelectedTime(undefined); - setRevealedStack(undefined); + setRevealedError(undefined); }, [model]); - React.useEffect(() => { - if (selectedAction && model?.actions.includes(selectedAction)) - return; + const selectedAction = React.useMemo(() => { + if (selectedCallId) { + const action = model?.actions.find(a => a.callId === selectedCallId); + if (action) + return action; + } + const failedAction = model?.failedAction(); - if (initialSelection && model?.actions.includes(initialSelection)) { - setSelectedAction(initialSelection); - } else if (failedAction) { - setSelectedAction(failedAction); - } else if (model?.actions.length) { + if (failedAction) + return failedAction; + + if (model?.actions.length) { // Select the last non-after hooks item. let index = model.actions.length - 1; for (let i = 0; i < model.actions.length; ++i) { @@ -109,15 +109,24 @@ export const Workbench: React.FunctionComponent<{ break; } } - setSelectedAction(model.actions[index]); + return model.actions[index]; } - }, [model, selectedAction, setSelectedAction, initialSelection]); + }, [model, selectedCallId]); + + const revealedStack = React.useMemo(() => { + if (revealedError) + return revealedError.stack; + return selectedAction?.stack; + }, [selectedAction, revealedError]); + + const activeAction = React.useMemo(() => { + return highlightedAction || selectedAction; + }, [selectedAction, highlightedAction]); const onActionSelected = React.useCallback((action: modelUtil.ActionTraceEventInContext) => { setSelectedAction(action); setHighlightedAction(undefined); - onSelectionChanged?.(action); - }, [setSelectedAction, onSelectionChanged, setHighlightedAction]); + }, [setSelectedAction, setHighlightedAction]); const selectPropertiesTab = React.useCallback((tab: string) => { setSelectedPropertiesTab(tab); @@ -177,7 +186,7 @@ export const Workbench: React.FunctionComponent<{ if (error.action) setSelectedAction(error.action); else - setRevealedStack(error.stack); + setRevealedError(error); selectPropertiesTab('source'); }} /> };