feat(cli): Support trace file URLs (#9030)

This commit is contained in:
Sidharth Vinod 2021-10-01 19:38:41 +05:30 committed by GitHub
parent 46b5c81f82
commit 3296c21a80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 121 additions and 48 deletions

View File

@ -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
```

View File

@ -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;

View File

@ -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;
@ -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, {
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);
@ -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) {

View File

@ -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> {
@ -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}`);
@ -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;

View File

@ -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,

View File

@ -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
@ -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 = () => {},
@ -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);