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