playwright/packages/trace-viewer/src/ui/uiModeTraceView.tsx
2024-03-20 16:00:35 -07:00

115 lines
4.2 KiB
TypeScript

/**
* 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 { 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 { Workbench } from './workbench';
export const TraceView: React.FC<{
item: { treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase },
rootDir?: string,
}> = ({ item, rootDir }) => {
const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>();
const [counter, setCounter] = React.useState(0);
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
const { outputDir } = React.useMemo(() => {
const outputDir = item.testCase ? outputDirForTestCase(item.testCase) : undefined;
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<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(() => {
if (pollTimer.current)
clearTimeout(pollTimer.current);
const result = item.testCase?.results[0];
if (!result) {
setModel(undefined);
return;
}
// Test finished.
const attachment = result && result.duration >= 0 && result.attachments.find(a => a.name === 'trace');
if (attachment && attachment.path) {
loadSingleTraceFile(attachment.path).then(model => setModel({ model, isLive: false }));
return;
}
if (!outputDir) {
setModel(undefined);
return;
}
const traceLocation = `${outputDir}/${artifactsFolderName(result!.workerIndex)}/traces/${item.testCase?.id}.json`;
// Start polling running test.
pollTimer.current = setTimeout(async () => {
try {
const model = await loadSingleTraceFile(traceLocation);
setModel({ model, isLive: true });
} catch {
setModel(undefined);
} finally {
setCounter(counter + 1);
}
}, 500);
return () => {
if (pollTimer.current)
clearTimeout(pollTimer.current);
};
}, [outputDir, item, setModel, counter, setCounter]);
return <Workbench
key='workbench'
model={model?.model}
showSourcesFirst={true}
rootDir={rootDir}
initialSelection={initialSelection}
onSelectionChanged={onSelectionChanged}
fallbackLocation={item.testFile}
isLive={model?.isLive}
status={item.treeItem?.status} />;
};
const outputDirForTestCase = (testCase: reporterTypes.TestCase): string | undefined => {
for (let suite: reporterTypes.Suite | undefined = testCase.parent; suite; suite = suite.parent) {
if (suite.project())
return suite.project()?.outputDir;
}
return undefined;
};
async function loadSingleTraceFile(url: string): Promise<MultiTraceModel> {
const params = new URLSearchParams();
params.set('trace', url);
const response = await fetch(`contexts?${params.toString()}`);
const contextEntries = await response.json() as ContextEntry[];
return new MultiTraceModel(contextEntries);
}