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