chore: trace viewer server for vscode (#23383)

This commit is contained in:
Pavel Feldman 2023-05-30 18:31:15 -07:00 committed by GitHub
parent 5cd271a2a7
commit 658b1dfea3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 91 additions and 52 deletions

View File

@ -28,6 +28,8 @@ import type { Page } from '../../page';
type Options = { app?: string, headless?: boolean, host?: string, port?: number, isServer?: boolean };
export async function showTraceViewer(traceUrls: string[], browserName: string, options?: Options): Promise<Page> {
const stdinServer = options?.isServer ? new StdinServer() : undefined;
const { headless = false, host, port, app } = options || {};
for (const traceUrl of traceUrls) {
let traceFile = traceUrl;
@ -113,46 +115,59 @@ export async function showTraceViewer(traceUrls: string[], browserName: string,
page.on('close', () => process.exit());
}
if (options?.isServer)
params.push('isServer');
const searchQuery = params.length ? '?' + params.join('&') : '';
await page.mainFrame().goto(serverSideCallMetadata(), urlPrefix + `/trace/${app || 'index.html'}${searchQuery}`);
if (options?.isServer)
runServer(page);
stdinServer?.setPage(page);
return page;
}
function runServer(page: Page) {
let liveTraceTimer: NodeJS.Timeout | undefined;
const loadTrace = (url: string) => {
clearTimeout(liveTraceTimer);
page.mainFrame().evaluateExpression(`window.setTraceURL(${JSON.stringify(url)})`, false, undefined).catch(() => {});
};
class StdinServer {
private _pollTimer: NodeJS.Timeout | undefined;
private _traceUrl: string | undefined;
private _page: Page | undefined;
const pollLoadTrace = (url: string) => {
loadTrace(url);
liveTraceTimer = setTimeout(() => {
pollLoadTrace(url);
constructor() {
process.stdin.on('data', data => {
const url = data.toString().trim();
if (url === this._traceUrl)
return;
this._traceUrl = url;
if (url.endsWith('.json'))
this._pollLoadTrace(url);
else
this._loadTrace(url);
});
process.stdin.on('close', () => this._selfDestruct());
}
setPage(page: Page) {
this._page = page;
if (this._traceUrl)
this._loadTrace(this._traceUrl);
}
private _loadTrace(url: string) {
clearTimeout(this._pollTimer);
this._page?.mainFrame().evaluateExpression(`window.setTraceURL(${JSON.stringify(url)})`, false, undefined).catch(() => {});
}
private _pollLoadTrace(url: string) {
this._loadTrace(url);
this._pollTimer = setTimeout(() => {
this._pollLoadTrace(url);
}, 500);
};
}
process.stdin.on('data', data => {
const url = data.toString().trim();
if (url.endsWith('.json'))
pollLoadTrace(url);
else
loadTrace(url);
});
process.stdin.on('close', () => selfDestruct());
}
function selfDestruct() {
// Force exit after 30 seconds.
setTimeout(() => process.exit(0), 30000);
// Meanwhile, try to gracefully close all browsers.
gracefullyCloseAll().then(() => {
process.exit(0);
});
private _selfDestruct() {
// Force exit after 30 seconds.
setTimeout(() => process.exit(0), 30000);
// Meanwhile, try to gracefully close all browsers.
gracefullyCloseAll().then(() => {
process.exit(0);
});
}
}
function traceDescriptor(traceName: string) {

View File

@ -49,8 +49,6 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI
} catch (error: any) {
if (error?.message?.includes('Cannot find .trace file') && await traceModel.hasEntry('index.html'))
throw new Error('Could not load trace. Did you upload a Playwright HTML report instead? Make sure to extract the archive first and then double-click the index.html file or put it on a web server.');
// eslint-disable-next-line no-console
console.error(error);
if (traceFileName)
throw new Error(`Could not load trace from ${traceFileName}. Make sure to upload a valid Playwright trace.`);
throw new Error(`Could not load trace from ${traceUrl}. Make sure a valid Playwright Trace is accessible over this url.`);

View File

@ -30,9 +30,16 @@
line-height: 24px;
}
body .drop-target {
background: rgba(255, 255, 255, 0.8);
}
body.dark-mode .drop-target {
background: rgba(0, 0, 0, 0.8);
}
.drop-target .title {
font-size: 24px;
color: #666;
font-weight: bold;
margin-bottom: 30px;
}
@ -81,7 +88,7 @@
flex-basis: 48px;
line-height: 48px;
font-size: 16px;
color: white;
color: #cccccc;
}
.workbench .header .toolbar-button {

View File

@ -24,6 +24,7 @@ import { Workbench } from './workbench';
export const WorkbenchLoader: React.FunctionComponent<{
}> = () => {
const [isServer, setIsServer] = React.useState<boolean>(false);
const [traceURLs, setTraceURLs] = React.useState<string[]>([]);
const [uploadedTraceNames, setUploadedTraceNames] = React.useState<string[]>([]);
const [model, setModel] = React.useState<MultiTraceModel>(emptyModel);
@ -32,7 +33,7 @@ export const WorkbenchLoader: React.FunctionComponent<{
const [processingErrorMessage, setProcessingErrorMessage] = React.useState<string | null>(null);
const [fileForLocalModeError, setFileForLocalModeError] = React.useState<string | null>(null);
const processTraceFiles = (files: FileList) => {
const processTraceFiles = React.useCallback((files: FileList) => {
const blobUrls = [];
const fileNames = [];
const url = new URL(window.location.href);
@ -54,22 +55,25 @@ export const WorkbenchLoader: React.FunctionComponent<{
setUploadedTraceNames(fileNames);
setDragOver(false);
setProcessingErrorMessage(null);
};
}, []);
const handleDropEvent = (event: React.DragEvent<HTMLDivElement>) => {
const handleDropEvent = React.useCallback((event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
processTraceFiles(event.dataTransfer.files);
};
}, [processTraceFiles]);
const handleFileInputChange = (event: any) => {
const handleFileInputChange = React.useCallback((event: any) => {
event.preventDefault();
if (!event.target.files)
return;
processTraceFiles(event.target.files);
};
}, [processTraceFiles]);
React.useEffect(() => {
const newTraceURLs = new URL(window.location.href).searchParams.getAll('trace');
const params = new URL(window.location.href).searchParams;
const newTraceURLs = params.getAll('trace');
setIsServer(params.has('isServer'));
// Don't accept file:// URLs - this means we re opened locally.
for (const url of newTraceURLs) {
if (url.startsWith('file:')) {
@ -80,11 +84,16 @@ export const WorkbenchLoader: React.FunctionComponent<{
(window as any).setTraceURL = (url: string) => {
setTraceURLs([url]);
setDragOver(false);
setProcessingErrorMessage(null);
};
// Don't re-use blob file URLs on page load (results in Fetch error)
if (!newTraceURLs.some(url => url.startsWith('blob:')))
if (earlyTraceURL) {
(window as any).setTraceURL(earlyTraceURL);
} else if (!newTraceURLs.some(url => url.startsWith('blob:'))) {
// Don't re-use blob file URLs on page load (results in Fetch error)
setTraceURLs(newTraceURLs);
}, [setTraceURLs]);
}
}, []);
React.useEffect(() => {
(async () => {
@ -104,7 +113,8 @@ export const WorkbenchLoader: React.FunctionComponent<{
params.set('traceFileName', uploadedTraceNames[i]);
const response = await fetch(`contexts?${params.toString()}`);
if (!response.ok) {
setTraceURLs([]);
if (!isServer)
setTraceURLs([]);
setProcessingErrorMessage((await response.json()).error);
return;
}
@ -118,7 +128,7 @@ export const WorkbenchLoader: React.FunctionComponent<{
setModel(emptyModel);
}
})();
}, [traceURLs, uploadedTraceNames]);
}, [isServer, traceURLs, uploadedTraceNames]);
return <div className='vbox workbench' onDragOver={event => { event.preventDefault(); setDragOver(true); }}>
<div className='hbox header'>
@ -128,9 +138,9 @@ export const WorkbenchLoader: React.FunctionComponent<{
<div className='spacer'></div>
<ToolbarButton icon='color-mode' title='Toggle color mode' toggled={false} onClick={() => toggleTheme()}></ToolbarButton>
</div>
{!!progress.total && <div className='progress'>
<div className='inner-progress' style={{ width: (100 * progress.done / progress.total) + '%' }}></div>
</div>}
<div className='progress'>
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
</div>
<Workbench model={model} />
{fileForLocalModeError && <div className='drop-target'>
<div>Trace Viewer uses Service Workers to show traces. To view trace:</div>
@ -140,7 +150,7 @@ export const WorkbenchLoader: React.FunctionComponent<{
<div>3. Drop the trace from the download shelf into the page</div>
</div>
</div>}
{!dragOver && !fileForLocalModeError && (!traceURLs.length || processingErrorMessage) && <div className='drop-target'>
{!isServer && !dragOver && !fileForLocalModeError && (!traceURLs.length || processingErrorMessage) && <div className='drop-target'>
<div className='processing-error'>{processingErrorMessage}</div>
<div className='title'>Drop Playwright Trace to load</div>
<div>or</div>
@ -153,6 +163,9 @@ export const WorkbenchLoader: React.FunctionComponent<{
<div style={{ maxWidth: 400 }}>Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere,
it opens it locally.</div>
</div>}
{isServer && (!traceURLs.length || processingErrorMessage) && <div className='drop-target'>
<div className='title'>Select test to see the trace</div>
</div>}
{dragOver && <div className='drop-target'
onDragLeave={() => { setDragOver(false); }}
onDrop={event => handleDropEvent(event)}>
@ -162,3 +175,9 @@ export const WorkbenchLoader: React.FunctionComponent<{
};
export const emptyModel = new MultiTraceModel([]);
let earlyTraceURL: string | undefined = undefined;
(window as any).setTraceURL = (url: string) => {
earlyTraceURL = url;
};