mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-12 11:50:22 +03:00
chore: trace viewer server for vscode (#23383)
This commit is contained in:
parent
5cd271a2a7
commit
658b1dfea3
@ -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) {
|
||||
|
@ -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.`);
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user