mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-15 06:02:57 +03:00
feat(cli): Support trace file URLs (#9030)
This commit is contained in:
parent
46b5c81f82
commit
3296c21a80
@ -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
|
||||
```
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user