chore(trace-viewer): improve progress indicator when loading trace (#36678)

Co-authored-by: Simon Knott <info@simonknott.de>
This commit is contained in:
Adam Gastineau 2025-07-18 10:56:49 -07:00 committed by GitHub
parent 1592353d47
commit 248d29ed78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 101 additions and 17 deletions

View File

@ -20,7 +20,8 @@ export interface DialogProps {
className?: string;
style?: React.CSSProperties;
open: boolean;
width: number;
isModal?: boolean;
width?: number;
verticalOffset?: number;
requestClose?: () => void;
anchor?: React.RefObject<HTMLElement>;
@ -31,6 +32,7 @@ export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
className,
style: externalStyle,
open,
isModal,
width,
verticalOffset,
requestClose,
@ -52,7 +54,7 @@ export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
position: 'fixed',
margin: 0,
top: bounds.bottom + (verticalOffset ?? 0),
left: buildTopLeftCoord(bounds, width),
left: buildTopLeftCoord(bounds, width ?? 0),
width,
zIndex: 1,
...externalStyle
@ -96,12 +98,24 @@ export const Dialog: React.FC<React.PropsWithChildren<DialogProps>> = ({
};
}, []);
React.useLayoutEffect(() => {
if (!dialogRef.current)
return;
if (open) {
if (isModal)
dialogRef.current.showModal();
else
dialogRef.current.show();
} else {
dialogRef.current.close();
}
}, [open, isModal]);
return (
open && (
<dialog ref={dialogRef} style={style} className={className} data-testid={dataTestId} open>
{children}
</dialog>
)
<dialog ref={dialogRef} style={style} className={className} data-testid={dataTestId}>
{children}
</dialog>
);
};

View File

@ -65,17 +65,40 @@ body.dark-mode .drop-target {
cursor: pointer;
}
.progress {
flex: none;
.progress-dialog {
width: 400px;
inset: 0;
border: none;
outline: none;
background-color: var(--vscode-sideBar-background);
}
.progress-dialog::backdrop {
background-color: rgba(0, 0, 0, 0.4);
}
.progress-content {
padding: 16px;
}
.progress-content .title {
/* This is set in common.css */
background-color: unset;
font-size: 18px;
font-weight: bold;
padding: 0;
}
.progress-wrapper {
background-color: var(--vscode-commandCenter-activeBackground);
width: 100%;
height: 3px;
margin-top: -3px;
z-index: 10;
margin-top: 16px;
margin-bottom: 8px;
}
.inner-progress {
background-color: var(--vscode-progressBar-background);
height: 100%;
height: 4px;
}
.header {

View File

@ -21,6 +21,7 @@ import './workbenchLoader.css';
import { Workbench } from './workbench';
import { TestServerConnection, WebSocketTestServerTransport } from '@testIsomorphic/testServerConnection';
import { SettingsToolbarButton } from './settingsToolbarButton';
import { Dialog } from './shared/dialog';
export const WorkbenchLoader: React.FunctionComponent<{
}> = () => {
@ -32,6 +33,7 @@ export const WorkbenchLoader: React.FunctionComponent<{
const [dragOver, setDragOver] = React.useState<boolean>(false);
const [processingErrorMessage, setProcessingErrorMessage] = React.useState<string | null>(null);
const [fileForLocalModeError, setFileForLocalModeError] = React.useState<string | null>(null);
const [showProgressDialog, setShowProgressDialog] = React.useState<boolean>(false);
const processTraceFiles = React.useCallback((files: FileList) => {
const blobUrls = [];
@ -167,6 +169,20 @@ export const WorkbenchLoader: React.FunctionComponent<{
})();
}, [isServer, traceURLs, uploadedTraceNames]);
const showLoading = progress.done !== progress.total && progress.total !== 0;
React.useEffect(() => {
if (showLoading) {
const timeout = setTimeout(() => {
setShowProgressDialog(true);
}, 200);
return () => clearTimeout(timeout);
} else {
setShowProgressDialog(false);
}
}, [showLoading]);
const showFileUploadDropArea = !!(!isServer && !dragOver && !fileForLocalModeError && (!traceURLs.length || processingErrorMessage));
return <div className='vbox workbench-loader' onDragOver={event => { event.preventDefault(); setDragOver(true); }}>
@ -179,9 +195,6 @@ export const WorkbenchLoader: React.FunctionComponent<{
<div className='spacer'></div>
<SettingsToolbarButton />
</div>
<div className='progress'>
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
</div>
<Workbench model={model} inert={showFileUploadDropArea} />
{fileForLocalModeError && <div className='drop-target'>
<div>Trace Viewer uses Service Workers to show traces. To view trace:</div>
@ -191,6 +204,14 @@ export const WorkbenchLoader: React.FunctionComponent<{
<div>3. Drop the trace from the download shelf into the page</div>
</div>
</div>}
<Dialog open={showProgressDialog} isModal={true} className='progress-dialog'>
<div className='progress-content'>
<div className='title' role='heading' aria-level={1}>Loading Playwright Trace...</div>
<div className='progress-wrapper'>
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
</div>
</div>
</Dialog>
{showFileUploadDropArea && <div className='drop-target'>
<div className='processing-error' role='alert'>{processingErrorMessage}</div>
<div className='title' role='heading' aria-level={1}>Drop Playwright Trace to load</div>

View File

@ -21,6 +21,7 @@ import type { TraceViewerFixtures } from '../config/traceViewerFixtures';
import { traceViewerFixtures } from '../config/traceViewerFixtures';
import fs from 'fs';
import path from 'path';
import type http from 'http';
import { pathToFileURL } from 'url';
import { expect, playwrightTest } from '../config/browserTest';
import type { FrameLocator } from '@playwright/test';
@ -1914,3 +1915,27 @@ test('should render locator descriptions', async ({ runAndTrace, page }) => {
- treeitem /Click.*input.*first/
`);
});
test('should load trace from HTTP with progress indicator', async ({ showTraceViewer, server }) => {
const [traceViewer, res] = await Promise.all([
showTraceViewer([server.PREFIX]),
new Promise<http.ServerResponse>(resolve => {
server.setRoute('/', (req, res) => resolve(res));
}),
]);
const file = await fs.promises.readFile(traceFile);
const dialog = traceViewer.page.locator('dialog', { hasText: 'Loading' });
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Content-Length', file.byteLength);
res.writeHead(200);
await expect(dialog).not.toBeVisible({ timeout: 100 });
// Should become visible after ~200ms
await expect(dialog).toBeVisible();
res.end(file);
await expect(dialog).not.toBeVisible();
await expect(traceViewer.actionTitles).toContainText([/Create page/]);
});

View File

@ -620,7 +620,8 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await showReport();
await page.getByRole('link', { name: 'passes' }).click();
await page.click('img');
await expect(page.locator('.workbench-loader .title')).toHaveText('a.test.js:3 passes');
await expect(page.locator('.progress-dialog')).toBeHidden();
await expect(page.locator('.workbench-loader > .header > .title')).toHaveText('a.test.js:3 passes');
});
test('should show multi trace source', async ({ runInlineTest, page, server, showReport }) => {