Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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!);
Expand Down
28 changes: 20 additions & 8 deletions packages/playwright-core/src/web/traceViewer/sw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Comment thread
mxschmitt marked this conversation as resolved.
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/')) {
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/web/traceViewer/traceModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
mxschmitt marked this conversation as resolved.
}
if (!iframeRef.current)
return;
Expand All @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions packages/playwright-core/src/web/traceViewer/ui/workbench.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
30 changes: 27 additions & 3 deletions packages/playwright-core/src/web/traceViewer/ui/workbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,31 @@ 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>();
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
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>) => {
Expand All @@ -65,6 +71,13 @@ export const Workbench: React.FunctionComponent<{
processTraceFile(event.target.files[0]);
};

React.useEffect(() => {
Comment thread
mxschmitt marked this conversation as resolved.
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]);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean useEffect around setTraceURL, wouldn't it run only once?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, as the comment says its about if you open trace viewer with a blob URL. Usually old tab, forced reload etc.


React.useEffect(() => {
(async () => {
if (traceURL) {
Expand All @@ -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);
Expand Down Expand Up @@ -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={() => {
Expand Down