chore: add trace viewer file upload error handling (#10243)

This commit is contained in:
Max Schmitt 2021-11-11 21:31:19 +01:00 committed by GitHub
parent fbb3c88f3c
commit 8fe3ea7972
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 66 additions and 14 deletions

View file

@ -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!);

View file

@ -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/')) {

View file

@ -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;

View file

@ -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);

View file

@ -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;
}

View file

@ -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={() => {