2021-01-07 16:15:34 -08:00
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2022-03-25 13:12:00 -08:00
|
|
|
import { SplitView } from '@web/components/splitView';
|
|
|
|
import * as React from 'react';
|
2021-01-07 16:15:34 -08:00
|
|
|
import { ActionList } from './actionList';
|
2021-07-02 14:33:38 -07:00
|
|
|
import { CallTab } from './callTab';
|
2023-09-01 20:12:05 -07:00
|
|
|
import { LogTab } from './logTab';
|
|
|
|
import { ErrorsTab, useErrorsTabModel } from './errorsTab';
|
|
|
|
import { ConsoleTab, useConsoleTabModel } from './consoleTab';
|
2023-07-10 12:56:56 -07:00
|
|
|
import type * as modelUtil from './modelUtil';
|
2023-05-19 15:18:18 -07:00
|
|
|
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
2023-09-01 20:12:05 -07:00
|
|
|
import { NetworkTab, useNetworkTabModel } from './networkTab';
|
2022-03-25 13:12:00 -08:00
|
|
|
import { SnapshotTab } from './snapshotTab';
|
|
|
|
import { SourceTab } from './sourceTab';
|
2023-02-17 11:19:53 -08:00
|
|
|
import { TabbedPane } from '@web/components/tabbedPane';
|
2023-03-07 14:24:50 -08:00
|
|
|
import type { TabbedPaneTabModel } from '@web/components/tabbedPane';
|
2022-03-25 13:12:00 -08:00
|
|
|
import { Timeline } from './timeline';
|
2023-03-06 12:25:00 -08:00
|
|
|
import { MetadataView } from './metadataView';
|
2023-05-09 17:53:01 -07:00
|
|
|
import { AttachmentsTab } from './attachmentsTab';
|
2023-08-16 16:30:17 -07:00
|
|
|
import type { Boundaries } from '../geometry';
|
2023-08-18 17:53:03 -07:00
|
|
|
import { InspectorTab } from './inspectorTab';
|
|
|
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
2023-10-24 16:41:40 -07:00
|
|
|
import { useSetting, msToString } from '@web/uiUtils';
|
2023-08-29 22:23:08 -07:00
|
|
|
import type { Entry } from '@trace/har';
|
2023-10-24 16:41:40 -07:00
|
|
|
import './workbench.css';
|
|
|
|
import { testStatusIcon, testStatusText } from './testUtils';
|
|
|
|
import type { UITestStatus } from './testUtils';
|
2021-01-07 16:15:34 -08:00
|
|
|
|
2023-02-16 07:59:21 -08:00
|
|
|
export const Workbench: React.FunctionComponent<{
|
2023-03-07 12:43:16 -08:00
|
|
|
model?: MultiTraceModel,
|
2023-03-10 22:52:31 -08:00
|
|
|
hideStackFrames?: boolean,
|
2023-03-11 11:43:33 -08:00
|
|
|
showSourcesFirst?: boolean,
|
2023-03-19 12:04:19 -07:00
|
|
|
rootDir?: string,
|
2023-05-08 18:51:27 -07:00
|
|
|
fallbackLocation?: modelUtil.SourceLocation,
|
2023-05-19 15:18:18 -07:00
|
|
|
initialSelection?: ActionTraceEventInContext,
|
|
|
|
onSelectionChanged?: (action: ActionTraceEventInContext) => void,
|
2023-05-18 15:52:44 -07:00
|
|
|
isLive?: boolean,
|
2023-10-24 16:41:40 -07:00
|
|
|
status?: UITestStatus,
|
|
|
|
}> = ({ model, hideStackFrames, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status }) => {
|
2023-05-19 15:18:18 -07:00
|
|
|
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEventInContext | undefined>(undefined);
|
|
|
|
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
2023-08-29 22:23:08 -07:00
|
|
|
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
2023-02-16 07:59:21 -08:00
|
|
|
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
2023-09-01 20:12:05 -07:00
|
|
|
const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('propertiesTab', showSourcesFirst ? 'source' : 'call');
|
2023-08-18 17:53:03 -07:00
|
|
|
const [isInspecting, setIsInspecting] = React.useState(false);
|
|
|
|
const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
|
2023-03-07 12:43:16 -08:00
|
|
|
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
2023-08-16 16:30:17 -07:00
|
|
|
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
|
2023-08-21 19:40:44 -07:00
|
|
|
const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom');
|
2021-10-22 07:00:34 -08:00
|
|
|
|
2023-03-19 12:04:19 -07:00
|
|
|
const sources = React.useMemo(() => model?.sources || new Map(), [model]);
|
|
|
|
|
2023-08-20 14:47:18 -07:00
|
|
|
React.useEffect(() => {
|
|
|
|
setSelectedTime(undefined);
|
|
|
|
}, [model]);
|
|
|
|
|
2023-03-10 17:01:30 -08:00
|
|
|
React.useEffect(() => {
|
2023-03-11 11:43:33 -08:00
|
|
|
if (selectedAction && model?.actions.includes(selectedAction))
|
2023-03-10 17:01:30 -08:00
|
|
|
return;
|
2023-09-01 13:48:15 -07:00
|
|
|
const failedAction = model?.failedAction();
|
2023-10-25 17:05:06 -07:00
|
|
|
if (initialSelection && model?.actions.includes(initialSelection)) {
|
2023-03-31 18:34:51 -07:00
|
|
|
setSelectedAction(initialSelection);
|
2023-10-25 17:05:06 -07:00
|
|
|
} else if (failedAction) {
|
2023-03-10 17:01:30 -08:00
|
|
|
setSelectedAction(failedAction);
|
2023-10-25 17:05:06 -07:00
|
|
|
} else 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) {
|
|
|
|
if (model.actions[i].apiName === 'After Hooks' && i) {
|
|
|
|
index = i - 1;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setSelectedAction(model.actions[index]);
|
|
|
|
}
|
2023-08-21 10:59:49 -07:00
|
|
|
}, [model, selectedAction, setSelectedAction, initialSelection]);
|
2023-03-31 18:34:51 -07:00
|
|
|
|
2023-05-19 15:18:18 -07:00
|
|
|
const onActionSelected = React.useCallback((action: ActionTraceEventInContext) => {
|
2023-03-31 18:34:51 -07:00
|
|
|
setSelectedAction(action);
|
|
|
|
onSelectionChanged?.(action);
|
|
|
|
}, [setSelectedAction, onSelectionChanged]);
|
2023-03-10 17:01:30 -08:00
|
|
|
|
2023-08-21 10:59:49 -07:00
|
|
|
const selectPropertiesTab = React.useCallback((tab: string) => {
|
|
|
|
setSelectedPropertiesTab(tab);
|
|
|
|
if (tab !== 'inspector')
|
|
|
|
setIsInspecting(false);
|
2023-09-01 20:12:05 -07:00
|
|
|
}, [setSelectedPropertiesTab]);
|
2023-08-21 10:59:49 -07:00
|
|
|
|
2023-08-18 17:53:03 -07:00
|
|
|
const locatorPicked = React.useCallback((locator: string) => {
|
|
|
|
setHighlightedLocator(locator);
|
2023-08-21 10:59:49 -07:00
|
|
|
selectPropertiesTab('inspector');
|
|
|
|
}, [selectPropertiesTab]);
|
2023-08-18 17:53:03 -07:00
|
|
|
|
2023-09-01 20:12:05 -07:00
|
|
|
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]);
|
|
|
|
|
2023-03-07 12:43:16 -08:00
|
|
|
const sdkLanguage = model?.sdkLanguage || 'javascript';
|
2021-07-01 20:46:56 -07:00
|
|
|
|
2023-08-18 17:53:03 -07:00
|
|
|
const inspectorTab: TabbedPaneTabModel = {
|
|
|
|
id: 'inspector',
|
|
|
|
title: 'Locator',
|
|
|
|
render: () => <InspectorTab
|
|
|
|
sdkLanguage={sdkLanguage}
|
|
|
|
setIsInspecting={setIsInspecting}
|
|
|
|
highlightedLocator={highlightedLocator}
|
|
|
|
setHighlightedLocator={setHighlightedLocator} />,
|
|
|
|
};
|
2023-03-11 11:43:33 -08:00
|
|
|
const callTab: TabbedPaneTabModel = {
|
|
|
|
id: 'call',
|
2023-05-12 19:15:31 -07:00
|
|
|
title: 'Call',
|
2023-03-11 11:43:33 -08:00
|
|
|
render: () => <CallTab action={activeAction} sdkLanguage={sdkLanguage} />
|
|
|
|
};
|
2023-09-01 20:12:05 -07:00
|
|
|
const logTab: TabbedPaneTabModel = {
|
|
|
|
id: 'log',
|
|
|
|
title: 'Log',
|
|
|
|
render: () => <LogTab action={activeAction} />
|
|
|
|
};
|
|
|
|
const errorsTab: TabbedPaneTabModel = {
|
|
|
|
id: 'errors',
|
|
|
|
title: 'Errors',
|
|
|
|
errorCount: errorsModel.errors.size,
|
2023-10-05 14:59:59 -07:00
|
|
|
render: () => <ErrorsTab errorsModel={errorsModel} sdkLanguage={sdkLanguage} revealInSource={action => {
|
|
|
|
setSelectedAction(action);
|
|
|
|
selectPropertiesTab('source');
|
|
|
|
}} />
|
2023-09-01 20:12:05 -07:00
|
|
|
};
|
2023-03-11 11:43:33 -08:00
|
|
|
const sourceTab: TabbedPaneTabModel = {
|
|
|
|
id: 'source',
|
|
|
|
title: 'Source',
|
2023-03-19 12:04:19 -07:00
|
|
|
render: () => <SourceTab
|
|
|
|
action={activeAction}
|
|
|
|
sources={sources}
|
|
|
|
hideStackFrames={hideStackFrames}
|
|
|
|
rootDir={rootDir}
|
2023-05-08 18:51:27 -07:00
|
|
|
fallbackLocation={fallbackLocation} />
|
2023-03-11 11:43:33 -08:00
|
|
|
};
|
|
|
|
const consoleTab: TabbedPaneTabModel = {
|
|
|
|
id: 'console',
|
|
|
|
title: 'Console',
|
2023-09-01 20:12:05 -07:00
|
|
|
count: consoleModel.entries.length,
|
|
|
|
render: () => <ConsoleTab consoleModel={consoleModel} boundaries={boundaries} selectedTime={selectedTime} />
|
2023-03-11 11:43:33 -08:00
|
|
|
};
|
|
|
|
const networkTab: TabbedPaneTabModel = {
|
|
|
|
id: 'network',
|
|
|
|
title: 'Network',
|
2023-09-01 20:12:05 -07:00
|
|
|
count: networkModel.resources.length,
|
|
|
|
render: () => <NetworkTab boundaries={boundaries} networkModel={networkModel} onEntryHovered={setHighlightedEntry}/>
|
2023-03-11 11:43:33 -08:00
|
|
|
};
|
2023-05-09 17:53:01 -07:00
|
|
|
const attachmentsTab: TabbedPaneTabModel = {
|
|
|
|
id: 'attachments',
|
|
|
|
title: 'Attachments',
|
2023-09-01 20:12:05 -07:00
|
|
|
count: attachments.length,
|
2023-07-10 12:56:56 -07:00
|
|
|
render: () => <AttachmentsTab model={model} />
|
2023-05-09 17:53:01 -07:00
|
|
|
};
|
2021-10-23 10:23:39 -08:00
|
|
|
|
2023-09-01 20:12:05 -07:00
|
|
|
const tabs: TabbedPaneTabModel[] = [
|
2023-08-18 17:53:03 -07:00
|
|
|
inspectorTab,
|
2023-03-11 11:43:33 -08:00
|
|
|
callTab,
|
2023-09-01 20:12:05 -07:00
|
|
|
logTab,
|
|
|
|
errorsTab,
|
2023-03-11 11:43:33 -08:00
|
|
|
consoleTab,
|
|
|
|
networkTab,
|
|
|
|
sourceTab,
|
2023-05-09 17:53:01 -07:00
|
|
|
attachmentsTab,
|
2023-03-11 11:43:33 -08:00
|
|
|
];
|
2023-09-01 20:12:05 -07:00
|
|
|
if (showSourcesFirst) {
|
|
|
|
const sourceTabIndex = tabs.indexOf(sourceTab);
|
|
|
|
tabs.splice(sourceTabIndex, 1);
|
|
|
|
tabs.splice(1, 0, sourceTab);
|
|
|
|
}
|
2021-10-23 10:23:39 -08:00
|
|
|
|
2023-08-16 16:30:17 -07:00
|
|
|
const { boundaries } = React.useMemo(() => {
|
|
|
|
const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 };
|
|
|
|
if (boundaries.minimum > boundaries.maximum) {
|
|
|
|
boundaries.minimum = 0;
|
|
|
|
boundaries.maximum = 30000;
|
|
|
|
}
|
|
|
|
// Leave some nice free space on the right hand side.
|
|
|
|
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
|
|
|
|
return { boundaries };
|
|
|
|
}, [model]);
|
|
|
|
|
2023-10-24 16:41:40 -07:00
|
|
|
let time: number = 0;
|
|
|
|
if (model && model.endTime >= 0)
|
|
|
|
time = model.endTime - model.startTime;
|
|
|
|
else if (model && model.wallTime)
|
|
|
|
time = Date.now() - model.wallTime;
|
|
|
|
|
2023-08-16 16:30:17 -07:00
|
|
|
return <div className='vbox workbench'>
|
2023-03-06 21:37:39 -08:00
|
|
|
<Timeline
|
|
|
|
model={model}
|
2023-08-16 16:30:17 -07:00
|
|
|
boundaries={boundaries}
|
2023-08-29 22:23:08 -07:00
|
|
|
highlightedAction={highlightedAction}
|
|
|
|
highlightedEntry={highlightedEntry}
|
2023-03-31 18:34:51 -07:00
|
|
|
onSelected={onActionSelected}
|
2023-06-16 17:56:11 +02:00
|
|
|
sdkLanguage={sdkLanguage}
|
2023-08-16 16:30:17 -07:00
|
|
|
selectedTime={selectedTime}
|
|
|
|
setSelectedTime={setSelectedTime}
|
2023-03-06 21:37:39 -08:00
|
|
|
/>
|
2023-09-22 10:43:44 -07:00
|
|
|
<SplitView sidebarSize={250} orientation={sidebarLocation === 'bottom' ? 'vertical' : 'horizontal'} settingName='propertiesSidebar'>
|
|
|
|
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true} settingName='actionListSidebar'>
|
2023-08-18 17:53:03 -07:00
|
|
|
<SnapshotTab
|
|
|
|
action={activeAction}
|
|
|
|
sdkLanguage={sdkLanguage}
|
|
|
|
testIdAttributeName={model?.testIdAttributeName || 'data-testid'}
|
|
|
|
isInspecting={isInspecting}
|
|
|
|
setIsInspecting={setIsInspecting}
|
|
|
|
highlightedLocator={highlightedLocator}
|
|
|
|
setHighlightedLocator={locatorPicked} />
|
|
|
|
<TabbedPane
|
2023-09-22 10:43:44 -07:00
|
|
|
tabs={[
|
|
|
|
{
|
|
|
|
id: 'actions',
|
|
|
|
title: 'Actions',
|
2023-10-24 16:41:40 -07:00
|
|
|
component: <div className='vbox'>
|
|
|
|
{status && <div className='workbench-run-status'>
|
|
|
|
<span className={`codicon ${testStatusIcon(status)}`}></span>
|
|
|
|
<div>{testStatusText(status)}</div>
|
|
|
|
<div className='spacer'></div>
|
|
|
|
<div className='workbench-run-duration'>{time ? msToString(time) : ''}</div>
|
|
|
|
</div>}
|
|
|
|
<ActionList
|
|
|
|
sdkLanguage={sdkLanguage}
|
|
|
|
actions={model?.actions || []}
|
|
|
|
selectedAction={model ? selectedAction : undefined}
|
|
|
|
selectedTime={selectedTime}
|
|
|
|
setSelectedTime={setSelectedTime}
|
|
|
|
onSelected={onActionSelected}
|
|
|
|
onHighlighted={setHighlightedAction}
|
|
|
|
revealConsole={() => selectPropertiesTab('console')}
|
|
|
|
isLive={isLive}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-09-22 10:43:44 -07:00
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 'metadata',
|
|
|
|
title: 'Metadata',
|
|
|
|
component: <MetadataView model={model}/>
|
|
|
|
},
|
2023-08-18 17:53:03 -07:00
|
|
|
]}
|
2023-09-22 10:43:44 -07:00
|
|
|
selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
|
2021-03-11 11:22:59 -08:00
|
|
|
</SplitView>
|
2023-08-18 17:53:03 -07:00
|
|
|
<TabbedPane
|
2023-09-22 10:43:44 -07:00
|
|
|
tabs={tabs}
|
|
|
|
selectedTab={selectedPropertiesTab}
|
|
|
|
setSelectedTab={selectPropertiesTab}
|
|
|
|
leftToolbar={[
|
|
|
|
<ToolbarButton title='Pick locator' icon='target' toggled={isInspecting} onClick={() => {
|
|
|
|
if (!isInspecting)
|
|
|
|
selectPropertiesTab('inspector');
|
|
|
|
setIsInspecting(!isInspecting);
|
|
|
|
}} />
|
|
|
|
]}
|
|
|
|
rightToolbar={[
|
|
|
|
sidebarLocation === 'bottom' ?
|
|
|
|
<ToolbarButton title='Dock to right' icon='layout-sidebar-right-off' onClick={() => {
|
|
|
|
setSidebarLocation('right');
|
|
|
|
}} /> :
|
|
|
|
<ToolbarButton title='Dock to bottom' icon='layout-panel-off' onClick={() => {
|
|
|
|
setSidebarLocation('bottom');
|
|
|
|
}} />
|
2023-08-18 17:53:03 -07:00
|
|
|
]}
|
2023-09-22 10:43:44 -07:00
|
|
|
/>
|
2021-04-06 11:27:57 +08:00
|
|
|
</SplitView>
|
2021-01-07 16:15:34 -08:00
|
|
|
</div>;
|
|
|
|
};
|