/** * 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(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(); 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 ; }; 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 { 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); }