mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	chore: preserve selected trace action in live trace (#32630)
This commit is contained in:
		
							parent
							
								
									2a347b5494
								
							
						
					
					
						commit
						3bff7b6ab1
					
				| @ -22,7 +22,7 @@ import { renderAction } from './actionList'; | |||||||
| import type { Language } from '@isomorphic/locatorGenerators'; | import type { Language } from '@isomorphic/locatorGenerators'; | ||||||
| import type { StackFrame } from '@protocol/channels'; | import type { StackFrame } from '@protocol/channels'; | ||||||
| 
 | 
 | ||||||
| type ErrorDescription = { | export type ErrorDescription = { | ||||||
|   action?: modelUtil.ActionTraceEventInContext; |   action?: modelUtil.ActionTraceEventInContext; | ||||||
|   stack?: StackFrame[]; |   stack?: StackFrame[]; | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -342,10 +342,6 @@ export function buildActionTree(actions: ActionTraceEventInContext[]): { rootIte | |||||||
|   return { rootItem, itemMap }; |   return { rootItem, itemMap }; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function idForAction(action: ActionTraceEvent) { |  | ||||||
|   return `${action.pageId || 'none'}:${action.callId}`; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function context(action: ActionTraceEvent | trace.EventTraceEvent | ResourceSnapshot): ContextEntry { | export function context(action: ActionTraceEvent | trace.EventTraceEvent | ResourceSnapshot): ContextEntry { | ||||||
|   return (action as any)[contextSymbol]; |   return (action as any)[contextSymbol]; | ||||||
| } | } | ||||||
|  | |||||||
| @ -16,14 +16,13 @@ | |||||||
| 
 | 
 | ||||||
| import { artifactsFolderName } from '@testIsomorphic/folders'; | import { artifactsFolderName } from '@testIsomorphic/folders'; | ||||||
| import type { TreeItem } from '@testIsomorphic/testTree'; | import type { TreeItem } from '@testIsomorphic/testTree'; | ||||||
| import type { ActionTraceEvent } from '@trace/trace'; |  | ||||||
| import '@web/common.css'; | import '@web/common.css'; | ||||||
| import '@web/third_party/vscode/codicon.css'; | import '@web/third_party/vscode/codicon.css'; | ||||||
| import type * as reporterTypes from 'playwright/types/testReporter'; | import type * as reporterTypes from 'playwright/types/testReporter'; | ||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import type { ContextEntry } from '../entries'; | import type { ContextEntry } from '../entries'; | ||||||
| import type { SourceLocation } from './modelUtil'; | import type { SourceLocation } from './modelUtil'; | ||||||
| import { idForAction, MultiTraceModel } from './modelUtil'; | import { MultiTraceModel } from './modelUtil'; | ||||||
| import { Workbench } from './workbench'; | import { Workbench } from './workbench'; | ||||||
| 
 | 
 | ||||||
| export const TraceView: React.FC<{ | export const TraceView: React.FC<{ | ||||||
| @ -42,12 +41,6 @@ export const TraceView: React.FC<{ | |||||||
|     return { outputDir }; |     return { outputDir }; | ||||||
|   }, [item]); |   }, [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<string | undefined>(); |  | ||||||
|   const onSelectionChanged = React.useCallback((action: ActionTraceEvent) => setSelectedActionId(idForAction(action)), [setSelectedActionId]); |  | ||||||
|   const initialSelection = selectedActionId ? model?.model.actions.find(a => idForAction(a) === selectedActionId) : undefined; |  | ||||||
| 
 |  | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|     if (pollTimer.current) |     if (pollTimer.current) | ||||||
|       clearTimeout(pollTimer.current); |       clearTimeout(pollTimer.current); | ||||||
| @ -98,8 +91,6 @@ export const TraceView: React.FC<{ | |||||||
|     model={model?.model} |     model={model?.model} | ||||||
|     showSourcesFirst={true} |     showSourcesFirst={true} | ||||||
|     rootDir={rootDir} |     rootDir={rootDir} | ||||||
|     initialSelection={initialSelection} |  | ||||||
|     onSelectionChanged={onSelectionChanged} |  | ||||||
|     fallbackLocation={item.testFile} |     fallbackLocation={item.testFile} | ||||||
|     isLive={model?.isLive} |     isLive={model?.isLive} | ||||||
|     status={item.treeItem?.status} |     status={item.treeItem?.status} | ||||||
|  | |||||||
| @ -20,11 +20,11 @@ import { ActionList } from './actionList'; | |||||||
| import { CallTab } from './callTab'; | import { CallTab } from './callTab'; | ||||||
| import { LogTab } from './logTab'; | import { LogTab } from './logTab'; | ||||||
| import { ErrorsTab, useErrorsTabModel } from './errorsTab'; | import { ErrorsTab, useErrorsTabModel } from './errorsTab'; | ||||||
|  | import type { ErrorDescription } from './errorsTab'; | ||||||
| import type { ConsoleEntry } from './consoleTab'; | import type { ConsoleEntry } from './consoleTab'; | ||||||
| import { ConsoleTab, useConsoleTabModel } from './consoleTab'; | import { ConsoleTab, useConsoleTabModel } from './consoleTab'; | ||||||
| import type * as modelUtil from './modelUtil'; | import type * as modelUtil from './modelUtil'; | ||||||
| import { isRouteAction } from './modelUtil'; | import { isRouteAction } from './modelUtil'; | ||||||
| import type { StackFrame } from '@protocol/channels'; |  | ||||||
| import { NetworkTab, useNetworkTabModel } from './networkTab'; | import { NetworkTab, useNetworkTabModel } from './networkTab'; | ||||||
| import { SnapshotTab } from './snapshotTab'; | import { SnapshotTab } from './snapshotTab'; | ||||||
| import { SourceTab } from './sourceTab'; | import { SourceTab } from './sourceTab'; | ||||||
| @ -49,8 +49,6 @@ export const Workbench: React.FunctionComponent<{ | |||||||
|   showSourcesFirst?: boolean, |   showSourcesFirst?: boolean, | ||||||
|   rootDir?: string, |   rootDir?: string, | ||||||
|   fallbackLocation?: modelUtil.SourceLocation, |   fallbackLocation?: modelUtil.SourceLocation, | ||||||
|   initialSelection?: modelUtil.ActionTraceEventInContext, |  | ||||||
|   onSelectionChanged?: (action: modelUtil.ActionTraceEventInContext) => void, |  | ||||||
|   isLive?: boolean, |   isLive?: boolean, | ||||||
|   status?: UITestStatus, |   status?: UITestStatus, | ||||||
|   annotations?: { type: string; description?: string; }[]; |   annotations?: { type: string; description?: string; }[]; | ||||||
| @ -59,9 +57,10 @@ export const Workbench: React.FunctionComponent<{ | |||||||
|   onOpenExternally?: (location: modelUtil.SourceLocation) => void, |   onOpenExternally?: (location: modelUtil.SourceLocation) => void, | ||||||
|   revealSource?: boolean, |   revealSource?: boolean, | ||||||
|   showSettings?: boolean, |   showSettings?: boolean, | ||||||
| }> = ({ model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => { | }> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => { | ||||||
|   const [selectedAction, setSelectedActionImpl] = React.useState<modelUtil.ActionTraceEventInContext | undefined>(undefined); |   const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined); | ||||||
|   const [revealedStack, setRevealedStack] = React.useState<StackFrame[] | undefined>(undefined); |   const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined); | ||||||
|  | 
 | ||||||
|   const [highlightedAction, setHighlightedAction] = React.useState<modelUtil.ActionTraceEventInContext | undefined>(); |   const [highlightedAction, setHighlightedAction] = React.useState<modelUtil.ActionTraceEventInContext | undefined>(); | ||||||
|   const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>(); |   const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>(); | ||||||
|   const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>(); |   const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>(); | ||||||
| @ -69,38 +68,39 @@ export const Workbench: React.FunctionComponent<{ | |||||||
|   const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('propertiesTab', showSourcesFirst ? 'source' : 'call'); |   const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('propertiesTab', showSourcesFirst ? 'source' : 'call'); | ||||||
|   const [isInspecting, setIsInspectingState] = React.useState(false); |   const [isInspecting, setIsInspectingState] = React.useState(false); | ||||||
|   const [highlightedLocator, setHighlightedLocator] = React.useState<string>(''); |   const [highlightedLocator, setHighlightedLocator] = React.useState<string>(''); | ||||||
|   const activeAction = model ? highlightedAction || selectedAction : undefined; |  | ||||||
|   const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>(); |   const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>(); | ||||||
|   const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom'); |   const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom'); | ||||||
|   const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true); |   const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true); | ||||||
|   const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false); |   const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false); | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   const filteredActions = React.useMemo(() => { |   const filteredActions = React.useMemo(() => { | ||||||
|     return (model?.actions || []).filter(action => showRouteActions || !isRouteAction(action)); |     return (model?.actions || []).filter(action => showRouteActions || !isRouteAction(action)); | ||||||
|   }, [model, showRouteActions]); |   }, [model, showRouteActions]); | ||||||
| 
 | 
 | ||||||
|   const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => { |   const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => { | ||||||
|     setSelectedActionImpl(action); |     setSelectedCallId(action?.callId); | ||||||
|     setRevealedStack(action?.stack); |     setRevealedError(undefined); | ||||||
|   }, [setSelectedActionImpl, setRevealedStack]); |   }, []); | ||||||
| 
 | 
 | ||||||
|   const sources = React.useMemo(() => model?.sources || new Map<string, modelUtil.SourceModel>(), [model]); |   const sources = React.useMemo(() => model?.sources || new Map<string, modelUtil.SourceModel>(), [model]); | ||||||
| 
 | 
 | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|     setSelectedTime(undefined); |     setSelectedTime(undefined); | ||||||
|     setRevealedStack(undefined); |     setRevealedError(undefined); | ||||||
|   }, [model]); |   }, [model]); | ||||||
| 
 | 
 | ||||||
|   React.useEffect(() => { |   const selectedAction = React.useMemo(() => { | ||||||
|     if (selectedAction && model?.actions.includes(selectedAction)) |     if (selectedCallId) { | ||||||
|       return; |       const action = model?.actions.find(a => a.callId === selectedCallId); | ||||||
|  |       if (action) | ||||||
|  |         return action; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const failedAction = model?.failedAction(); |     const failedAction = model?.failedAction(); | ||||||
|     if (initialSelection && model?.actions.includes(initialSelection)) { |     if (failedAction) | ||||||
|       setSelectedAction(initialSelection); |       return failedAction; | ||||||
|     } else if (failedAction) { | 
 | ||||||
|       setSelectedAction(failedAction); |     if (model?.actions.length) { | ||||||
|     } else if (model?.actions.length) { |  | ||||||
|       // Select the last non-after hooks item.
 |       // Select the last non-after hooks item.
 | ||||||
|       let index = model.actions.length - 1; |       let index = model.actions.length - 1; | ||||||
|       for (let i = 0; i < model.actions.length; ++i) { |       for (let i = 0; i < model.actions.length; ++i) { | ||||||
| @ -109,15 +109,24 @@ export const Workbench: React.FunctionComponent<{ | |||||||
|           break; |           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) => { |   const onActionSelected = React.useCallback((action: modelUtil.ActionTraceEventInContext) => { | ||||||
|     setSelectedAction(action); |     setSelectedAction(action); | ||||||
|     setHighlightedAction(undefined); |     setHighlightedAction(undefined); | ||||||
|     onSelectionChanged?.(action); |   }, [setSelectedAction, setHighlightedAction]); | ||||||
|   }, [setSelectedAction, onSelectionChanged, setHighlightedAction]); |  | ||||||
| 
 | 
 | ||||||
|   const selectPropertiesTab = React.useCallback((tab: string) => { |   const selectPropertiesTab = React.useCallback((tab: string) => { | ||||||
|     setSelectedPropertiesTab(tab); |     setSelectedPropertiesTab(tab); | ||||||
| @ -177,7 +186,7 @@ export const Workbench: React.FunctionComponent<{ | |||||||
|       if (error.action) |       if (error.action) | ||||||
|         setSelectedAction(error.action); |         setSelectedAction(error.action); | ||||||
|       else |       else | ||||||
|         setRevealedStack(error.stack); |         setRevealedError(error); | ||||||
|       selectPropertiesTab('source'); |       selectPropertiesTab('source'); | ||||||
|     }} /> |     }} /> | ||||||
|   }; |   }; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Pavel Feldman
						Pavel Feldman