/** * 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 { ActionEntry } from '../../traceModel'; import * as React from 'react'; import { useAsyncMemo } from './helpers'; import './sourceTab.css'; import '../../../../third_party/highlightjs/highlightjs/tomorrow.css'; import * as highlightjs from '../../../../third_party/highlightjs/highlightjs'; type StackInfo = string | { frames: { filePath: string, fileName: string, lineNumber: number, functionName: string, }[]; fileContent: Map; }; export const SourceTab: React.FunctionComponent<{ actionEntry: ActionEntry | undefined, }> = ({ actionEntry }) => { const [lastAction, setLastAction] = React.useState(); const [selectedFrame, setSelectedFrame] = React.useState(0); const [needReveal, setNeedReveal] = React.useState(false); if (lastAction !== actionEntry) { setLastAction(actionEntry); setSelectedFrame(0); setNeedReveal(true); } const stackInfo = React.useMemo(() => { if (!actionEntry) return ''; const { action } = actionEntry; if (!action.stack) return ''; let frames = action.stack.split('\n').slice(1); frames = frames.filter(frame => !frame.includes('playwright/lib/') && !frame.includes('playwright/src/')); const info: StackInfo = { frames: [], fileContent: new Map(), }; for (const frame of frames) { let filePath: string; let lineNumber: number; let functionName: string; const match1 = frame.match(/at ([^(]+)\(([^:]+):(\d+):\d+\)/); const match2 = frame.match(/at ([^:^(]+):(\d+):\d+/); if (match1) { functionName = match1[1]; filePath = match1[2]; lineNumber = parseInt(match1[3], 10); } else if (match2) { functionName = ''; filePath = match2[1]; lineNumber = parseInt(match2[2], 10); } else { continue; } const pathSep = navigator.platform.includes('Win') ? '\\' : '/'; const fileName = filePath.substring(filePath.lastIndexOf(pathSep) + 1); info.frames.push({ filePath, fileName, lineNumber, functionName: functionName || '(anonymous)', }); } if (!info.frames.length) return action.stack; return info; }, [actionEntry]); const content = useAsyncMemo(async () => { let value: string; if (typeof stackInfo === 'string') { value = stackInfo; } else { const filePath = stackInfo.frames[selectedFrame].filePath; if (!stackInfo.fileContent.has(filePath)) stackInfo.fileContent.set(filePath, await window.readFile(filePath).catch(e => ``)); value = stackInfo.fileContent.get(filePath)!; } const result = []; let continuation: any; for (const line of (value || '').split('\n')) { const highlighted = highlightjs.highlight('javascript', line, true, continuation); continuation = highlighted.top; result.push(highlighted.value); } return result; }, [stackInfo, selectedFrame], []); const targetLine = typeof stackInfo === 'string' ? -1 : stackInfo.frames[selectedFrame].lineNumber; const targetLineRef = React.createRef(); React.useLayoutEffect(() => { if (needReveal && targetLineRef.current) { targetLineRef.current.scrollIntoView({ block: 'center', inline: 'nearest' }); setNeedReveal(false); } }, [needReveal, targetLineRef]); return
{ content.map((markup, index) => { const isTargetLine = (index + 1) === targetLine; return
{index + 1}
; }) }
{typeof stackInfo !== 'string' &&
{ stackInfo.frames.map((frame, index) => { return
{ setSelectedFrame(index); setNeedReveal(true); }} > {frame.functionName} {frame.fileName} {':' + frame.lineNumber}
; }) }
}
; };