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
22 changes: 22 additions & 0 deletions docs/src/trace-viewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,25 @@ Here is what the typical Action snapshot looks like:
</img>

Notice how it highlights both, the DOM Node as well as the exact click position.


## Viewing remote Traces

You can open remote traces using it's URL.
They could be generated in a CI run and makes it easy to view the remote trace without having to manually download the file.

```bash js
npx playwright show-trace https://example.com/trace.zip
```

```bash java
mvn exec:java -e -Dexec.mainClass=com.microsoft.playwright.CLI -Dexec.args="show-trace https://example.com/trace.zip"
```

```bash python
playwright show-trace https://example.com/trace.zip
```

```bash csharp
playwright show-trace https://example.com/trace.zip
```
3 changes: 2 additions & 1 deletion src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ program
}).addHelpText('afterAll', `
Examples:

$ show-trace trace/directory`);
$ show-trace trace/directory
$ show-trace https://example.com/trace.zip`);

if (!process.env.PW_CLI_TARGET_LANG) {
let playwrightTestPackagePath = null;
Expand Down
22 changes: 19 additions & 3 deletions src/server/trace/viewer/traceViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ import { PersistentSnapshotStorage, TraceModel } from './traceModel';
import { ServerRouteHandler, HttpServer } from '../../../utils/httpServer';
import { SnapshotServer } from '../../snapshot/snapshotServer';
import * as consoleApiSource from '../../../generated/consoleApiSource';
import { isUnderTest } from '../../../utils/utils';
import { isUnderTest, download } from '../../../utils/utils';
import { internalCallMetadata } from '../../instrumentation';
import { ProgressController } from '../../progress';
import { BrowserContext } from '../../browserContext';
import { registry } from '../../../utils/registry';
import { installAppIcon } from '../../chromium/crApp';
import { debugLogger } from '../../../utils/debugLogger';

export class TraceViewer {
private _server: HttpServer;
Expand Down Expand Up @@ -196,6 +197,23 @@ async function appendTraceEvents(model: TraceModel, file: string) {
}

export async function showTraceViewer(tracePath: string, browserName: string, headless = false): Promise<BrowserContext | undefined> {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `playwright-trace`));
process.on('exit', () => rimraf.sync(dir));

if (/^https?:\/\//i.test(tracePath)){
const downloadZipPath = path.join(dir, 'trace.zip');
try {
await download(tracePath, downloadZipPath, {
Comment thread
sidharthv96 marked this conversation as resolved.
progressBarName: tracePath,
log: debugLogger.log.bind(debugLogger, 'download')
});
} catch (error) {
console.log(`${error?.message || ''}`); // eslint-disable-line no-console
return;
}
tracePath = downloadZipPath;
}

let stat;
try {
stat = fs.statSync(tracePath);
Expand All @@ -210,8 +228,6 @@ export async function showTraceViewer(tracePath: string, browserName: string, he
}

const zipFile = tracePath;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `playwright-trace`));
process.on('exit', () => rimraf.sync(dir));
try {
await extract(zipFile, { dir });
} catch (e) {
Expand Down
48 changes: 5 additions & 43 deletions src/utils/browserFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ import extract from 'extract-zip';
import fs from 'fs';
import os from 'os';
import path from 'path';
import ProgressBar from 'progress';
import { downloadFile, existsAsync } from './utils';
import { existsAsync, download } from './utils';
import { debugLogger } from './debugLogger';

export async function downloadBrowserWithProgressBar(title: string, browserDirectory: string, executablePath: string, downloadURL: string, downloadFileName: string): Promise<boolean> {
Expand All @@ -31,46 +30,13 @@ export async function downloadBrowserWithProgressBar(title: string, browserDirec
return false;
}

let progressBar: ProgressBar;
let lastDownloadedBytes = 0;

function progress(downloadedBytes: number, totalBytes: number) {
if (!process.stderr.isTTY)
return;
if (!progressBar) {
progressBar = new ProgressBar(`Downloading ${progressBarName} - ${toMegabytes(totalBytes)} [:bar] :percent :etas `, {
complete: '=',
incomplete: ' ',
width: 20,
total: totalBytes,
});
}
const delta = downloadedBytes - lastDownloadedBytes;
lastDownloadedBytes = downloadedBytes;
progressBar.tick(delta);
}

const url = downloadURL;
const zipPath = path.join(os.tmpdir(), downloadFileName);
try {
for (let attempt = 1, N = 3; attempt <= N; ++attempt) {
debugLogger.log('install', `downloading ${progressBarName} - attempt #${attempt}`);
const { error } = await downloadFile(url, zipPath, { progressCallback: progress, log: debugLogger.log.bind(debugLogger, 'install') });
if (!error) {
debugLogger.log('install', `SUCCESS downloading ${progressBarName}`);
break;
}
const errorMessage = typeof error === 'object' && typeof error.message === 'string' ? error.message : '';
debugLogger.log('install', `attempt #${attempt} - ERROR: ${errorMessage}`);
if (attempt < N && (errorMessage.includes('ECONNRESET') || errorMessage.includes('ETIMEDOUT'))) {
// Maximum delay is 3rd retry: 1337.5ms
const millis = (Math.random() * 200) + (250 * Math.pow(1.5, attempt));
debugLogger.log('install', `sleeping ${millis}ms before retry...`);
await new Promise(c => setTimeout(c, millis));
} else {
throw error;
}
}
await download(url, zipPath, {
progressBarName,
log: debugLogger.log.bind(debugLogger, 'install')
});
debugLogger.log('install', `extracting archive`);
debugLogger.log('install', `-- zip: ${zipPath}`);
debugLogger.log('install', `-- location: ${browserDirectory}`);
Expand All @@ -89,10 +55,6 @@ export async function downloadBrowserWithProgressBar(title: string, browserDirec
return true;
}

function toMegabytes(bytes: number) {
const mb = bytes / 1024 / 1024;
return `${Math.round(mb * 10) / 10} Mb`;
}

export function logPolitely(toBeLogged: string) {
const logLevel = process.env.npm_config_loglevel;
Expand Down
1 change: 1 addition & 0 deletions src/utils/debugLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const debugLoggerColorMap = {
'api': 45, // cyan
'protocol': 34, // green
'install': 34, // green
'download': 34, // green
'browser': 0, // reset
'proxy': 92, // purple
'error': 160, // red,
Expand Down
73 changes: 72 additions & 1 deletion src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { getProxyForUrl } from 'proxy-from-env';
import * as URL from 'url';
import { getUbuntuVersionSync } from './ubuntuVersion';
import { NameValue } from '../protocol/channels';
import ProgressBar from 'progress';

// `https-proxy-agent` v5 is written in TypeScript and exposes generated types.
// However, as of June 2020, its types are generated with tsconfig that enables
Expand Down Expand Up @@ -115,7 +116,7 @@ export function fetchData(params: HTTPRequestParams, onError?: (response: http.I
type OnProgressCallback = (downloadedBytes: number, totalBytes: number) => void;
type DownloadFileLogger = (message: string) => void;

export function downloadFile(url: string, destinationPath: string, options: {progressCallback?: OnProgressCallback, log?: DownloadFileLogger} = {}): Promise<{error: any}> {
function downloadFile(url: string, destinationPath: string, options: {progressCallback?: OnProgressCallback, log?: DownloadFileLogger} = {}): Promise<{error: any}> {
const {
progressCallback,
log = () => {},
Expand Down Expand Up @@ -155,6 +156,76 @@ export function downloadFile(url: string, destinationPath: string, options: {pro
}
}

export async function download(
url: string,
destination: string,
options: {
progressBarName?: string,
retryCount?: number
log?: DownloadFileLogger
} = {}
) {
const { progressBarName = 'file', retryCount = 3, log = () => {} } = options;
for (let attempt = 1; attempt <= retryCount; ++attempt) {
log(
`downloading ${progressBarName} - attempt #${attempt}`
);
const { error } = await downloadFile(url, destination, {
progressCallback: getDownloadProgress(progressBarName),
log,
});
if (!error) {
log(`SUCCESS downloading ${progressBarName}`);
break;
}
const errorMessage = error?.message || '';
log(`attempt #${attempt} - ERROR: ${errorMessage}`);
if (
attempt < retryCount &&
(errorMessage.includes('ECONNRESET') ||
errorMessage.includes('ETIMEDOUT'))
) {
// Maximum default delay is 3rd retry: 1337.5ms
const millis = Math.random() * 200 + 250 * Math.pow(1.5, attempt);
log(`sleeping ${millis}ms before retry...`);
await new Promise(c => setTimeout(c, millis));
} else {
throw error;
}
}
}

function getDownloadProgress(progressBarName: string): OnProgressCallback {
let progressBar: ProgressBar;
let lastDownloadedBytes = 0;

return (downloadedBytes: number, totalBytes: number) => {
if (!process.stderr.isTTY)
return;
if (!progressBar) {
progressBar = new ProgressBar(
`Downloading ${progressBarName} - ${toMegabytes(
totalBytes
)} [:bar] :percent :etas `,
{
complete: '=',
incomplete: ' ',
width: 20,
total: totalBytes,
}
);
}
const delta = downloadedBytes - lastDownloadedBytes;
lastDownloadedBytes = downloadedBytes;
progressBar.tick(delta);
};
}

function toMegabytes(bytes: number) {
const mb = bytes / 1024 / 1024;
return `${Math.round(mb * 10) / 10} Mb`;
}

export function spawnAsync(cmd: string, args: string[], options?: SpawnOptions): Promise<{stdout: string, stderr: string, code: number, error?: Error}> {
const process = spawn(cmd, args, options);

Expand Down