chore: add trace viewer file upload error handling (#10243)
This commit is contained in:
parent
fbb3c88f3c
commit
8fe3ea7972
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import * as consoleApiSource from '../../../generated/consoleApiSource';
|
||||
import { HttpServer } from '../../../utils/httpServer';
|
||||
import { findChromiumChannel } from '../../../utils/registry';
|
||||
|
|
@ -26,6 +27,10 @@ import { createPlaywright } from '../../playwright';
|
|||
import { ProgressController } from '../../progress';
|
||||
|
||||
export async function showTraceViewer(traceUrl: string, browserName: string, headless = false, port?: number): Promise<BrowserContext | undefined> {
|
||||
if (traceUrl && !traceUrl.startsWith('http://') && !traceUrl.startsWith('https://') && !fs.existsSync(traceUrl)) {
|
||||
console.error(`Trace file ${traceUrl} does not exist!`);
|
||||
process.exit(1);
|
||||
}
|
||||
const server = new HttpServer();
|
||||
server.routePrefix('/trace', (request, response) => {
|
||||
const url = new URL('http://localhost' + request.url!);
|
||||
|
|
|
|||
|
|
@ -60,17 +60,29 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
|||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
const traceUrl = new URL(url).searchParams.get('trace')!;
|
||||
const traceUrl = url.searchParams.get('trace')!;
|
||||
const { snapshotServer } = loadedTraces.get(traceUrl) || {};
|
||||
|
||||
if (relativePath === '/context') {
|
||||
const traceModel = await loadTrace(traceUrl, event.clientId, (done: number, total: number) => {
|
||||
client.postMessage({ method: 'progress', params: { done, total } });
|
||||
});
|
||||
return new Response(JSON.stringify(traceModel!.contextEntry), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
try {
|
||||
const traceModel = await loadTrace(traceUrl, event.clientId, (done: number, total: number) => {
|
||||
client.postMessage({ method: 'progress', params: { done, total } });
|
||||
});
|
||||
return new Response(JSON.stringify(traceModel!.contextEntry), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
const traceFileName = url.searchParams.get('traceFileName')!;
|
||||
return new Response(JSON.stringify({
|
||||
error: traceFileName ? `Could not load trace from ${traceFileName}. Make sure to upload a valid Playwright trace.` :
|
||||
`Could not load trace from ${traceUrl}. Make sure a valid Playwright Trace is accessible over this url.`,
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (relativePath.startsWith('/snapshotInfo/')) {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import type { CallMetadata } from '../../protocol/callMetadata';
|
|||
// @ts-ignore
|
||||
self.importScripts('zip.min.js');
|
||||
|
||||
const zipjs = (self as any).zip;
|
||||
const zipjs = (self as any).zip as typeof zip;
|
||||
|
||||
export class TraceModel {
|
||||
contextEntry: ContextEntry;
|
||||
|
|
@ -38,7 +38,7 @@ export class TraceModel {
|
|||
}
|
||||
|
||||
async load(traceURL: string, progress: (done: number, total: number) => void) {
|
||||
const zipReader = new zipjs.ZipReader(
|
||||
const zipReader = new zipjs.ZipReader( // @ts-ignore
|
||||
new zipjs.HttpReader(traceURL, { mode: 'cors' }),
|
||||
{ useWebWorkers: false }) as zip.ZipReader;
|
||||
let traceEntry: zip.Entry | undefined;
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||
const info = await response.json();
|
||||
if (!info.error)
|
||||
setSnapshotInfo(info);
|
||||
} else {
|
||||
// Reset to default if snapshotInfoUrl was removed
|
||||
setSnapshotInfo(defaultSnapshotInfo);
|
||||
}
|
||||
if (!iframeRef.current)
|
||||
return;
|
||||
|
|
@ -73,7 +76,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||
} catch (e) {
|
||||
}
|
||||
})();
|
||||
}, [iframeRef, snapshotUrl, snapshotInfoUrl, pointX, pointY]);
|
||||
}, [iframeRef, snapshotUrl, snapshotInfoUrl, pointX, pointY, defaultSnapshotInfo]);
|
||||
|
||||
const snapshotSize = snapshotInfo.viewport;
|
||||
const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height, 1);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,14 @@
|
|||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.drop-target .processing-error {
|
||||
font-size: 24px;
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin: 30px;
|
||||
}
|
||||
|
||||
.drop-target input {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ import { msToString } from '../../uiUtils';
|
|||
|
||||
export const Workbench: React.FunctionComponent<{
|
||||
}> = () => {
|
||||
const [traceURL, setTraceURL] = React.useState<string>(new URL(window.location.href).searchParams.get('trace')!);
|
||||
const [traceURL, setTraceURL] = React.useState<string>('');
|
||||
const [uploadedTraceName, setUploadedTraceName] = React.useState<string|null>(null);
|
||||
const [contextEntry, setContextEntry] = React.useState<ContextEntry>(emptyContext);
|
||||
const [selectedAction, setSelectedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
|
|
@ -40,17 +41,22 @@ export const Workbench: React.FunctionComponent<{
|
|||
const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState<string>('logs');
|
||||
const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 });
|
||||
const [dragOver, setDragOver] = React.useState<boolean>(false);
|
||||
const [processingErrorMessage, setProcessingErrorMessage] = React.useState<string|null>(null);
|
||||
|
||||
const processTraceFile = (file: File) => {
|
||||
const blobTraceURL = URL.createObjectURL(file);
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('trace', blobTraceURL);
|
||||
url.searchParams.set('traceFileName', file.name);
|
||||
const href = url.toString();
|
||||
// Snapshot loaders will inherit the trace url from the query parameters,
|
||||
// so set it here.
|
||||
window.history.pushState({}, '', href);
|
||||
setTraceURL(blobTraceURL);
|
||||
setUploadedTraceName(file.name);
|
||||
setSelectedAction(undefined);
|
||||
setDragOver(false);
|
||||
setProcessingErrorMessage(null);
|
||||
};
|
||||
|
||||
const handleDropEvent = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
|
|
@ -65,6 +71,13 @@ export const Workbench: React.FunctionComponent<{
|
|||
processTraceFile(event.target.files[0]);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const newTraceURL = new URL(window.location.href).searchParams.get('trace');
|
||||
// Don't re-use blob file URLs on page load (results in Fetch error)
|
||||
if (newTraceURL && !newTraceURL.startsWith('blob:'))
|
||||
setTraceURL(newTraceURL);
|
||||
}, [setTraceURL]);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (traceURL) {
|
||||
|
|
@ -74,7 +87,17 @@ export const Workbench: React.FunctionComponent<{
|
|||
};
|
||||
navigator.serviceWorker.addEventListener('message', swListener);
|
||||
setProgress({ done: 0, total: 1 });
|
||||
const contextEntry = (await fetch(`context?trace=${traceURL}`).then(response => response.json())) as ContextEntry;
|
||||
const params = new URLSearchParams();
|
||||
params.set('trace', traceURL);
|
||||
if (uploadedTraceName)
|
||||
params.set('traceFileName', uploadedTraceName);
|
||||
const response = await fetch(`context?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
setTraceURL('');
|
||||
setProcessingErrorMessage((await response.json()).error);
|
||||
return;
|
||||
}
|
||||
const contextEntry = await response.json() as ContextEntry;
|
||||
navigator.serviceWorker.removeEventListener('message', swListener);
|
||||
setProgress({ done: 0, total: 0 });
|
||||
modelUtil.indexModel(contextEntry);
|
||||
|
|
@ -162,7 +185,8 @@ export const Workbench: React.FunctionComponent<{
|
|||
{!!progress.total && <div className='progress'>
|
||||
<div className='inner-progress' style={{ width: (100 * progress.done / progress.total) + '%' }}></div>
|
||||
</div>}
|
||||
{!dragOver && !traceURL && <div className='drop-target'>
|
||||
{!dragOver && (!traceURL || processingErrorMessage) && <div className='drop-target'>
|
||||
<div className='processing-error'>{processingErrorMessage}</div>
|
||||
<div className='title'>Drop Playwright Trace to load</div>
|
||||
<div>or</div>
|
||||
<button onClick={() => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue