From 8fe3ea79723c6cdd0b7ed399c2924e541bd2eb51 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 11 Nov 2021 21:31:19 +0100 Subject: [PATCH] chore: add trace viewer file upload error handling (#10243) --- .../src/server/trace/viewer/traceViewer.ts | 5 ++++ .../playwright-core/src/web/traceViewer/sw.ts | 28 ++++++++++++----- .../src/web/traceViewer/traceModel.ts | 4 +-- .../src/web/traceViewer/ui/snapshotTab.tsx | 5 +++- .../src/web/traceViewer/ui/workbench.css | 8 +++++ .../src/web/traceViewer/ui/workbench.tsx | 30 +++++++++++++++++-- 6 files changed, 66 insertions(+), 14 deletions(-) diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index aaf7827a3a..7df1ddcbf1 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -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 { + 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!); diff --git a/packages/playwright-core/src/web/traceViewer/sw.ts b/packages/playwright-core/src/web/traceViewer/sw.ts index 51ddfa8e42..6ed800ea24 100644 --- a/packages/playwright-core/src/web/traceViewer/sw.ts +++ b/packages/playwright-core/src/web/traceViewer/sw.ts @@ -60,17 +60,29 @@ async function doFetch(event: FetchEvent): Promise { 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/')) { diff --git a/packages/playwright-core/src/web/traceViewer/traceModel.ts b/packages/playwright-core/src/web/traceViewer/traceModel.ts index d32aa3fdc3..fc5570e952 100644 --- a/packages/playwright-core/src/web/traceViewer/traceModel.ts +++ b/packages/playwright-core/src/web/traceViewer/traceModel.ts @@ -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; diff --git a/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx b/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx index 062de81fbe..2a275070cc 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx +++ b/packages/playwright-core/src/web/traceViewer/ui/snapshotTab.tsx @@ -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); diff --git a/packages/playwright-core/src/web/traceViewer/ui/workbench.css b/packages/playwright-core/src/web/traceViewer/ui/workbench.css index 378add268e..40bfc19fe5 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/workbench.css +++ b/packages/playwright-core/src/web/traceViewer/ui/workbench.css @@ -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; } diff --git a/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx b/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx index d36d39bc3c..d8bb6d0a54 100644 --- a/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx +++ b/packages/playwright-core/src/web/traceViewer/ui/workbench.tsx @@ -32,7 +32,8 @@ import { msToString } from '../../uiUtils'; export const Workbench: React.FunctionComponent<{ }> = () => { - const [traceURL, setTraceURL] = React.useState(new URL(window.location.href).searchParams.get('trace')!); + const [traceURL, setTraceURL] = React.useState(''); + const [uploadedTraceName, setUploadedTraceName] = React.useState(null); const [contextEntry, setContextEntry] = React.useState(emptyContext); const [selectedAction, setSelectedAction] = React.useState(); const [highlightedAction, setHighlightedAction] = React.useState(); @@ -40,17 +41,22 @@ export const Workbench: React.FunctionComponent<{ const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState('logs'); const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 }); const [dragOver, setDragOver] = React.useState(false); + const [processingErrorMessage, setProcessingErrorMessage] = React.useState(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) => { @@ -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 &&
} - {!dragOver && !traceURL &&
+ {!dragOver && (!traceURL || processingErrorMessage) &&
+
{processingErrorMessage}
Drop Playwright Trace to load
or