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>
|
</img>
|
||||||
|
|
||||||
Notice how it highlights both, the DOM Node as well as the exact click position.
|
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', `
|
}).addHelpText('afterAll', `
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
$ show-trace trace/directory`);
|
$ show-trace trace/directory
|
||||||
|
$ show-trace https://example.com/trace.zip`);
|
||||||
|
|
||||||
if (!process.env.PW_CLI_TARGET_LANG) {
|
if (!process.env.PW_CLI_TARGET_LANG) {
|
||||||
let playwrightTestPackagePath = null;
|
let playwrightTestPackagePath = null;
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,13 @@ import { PersistentSnapshotStorage, TraceModel } from './traceModel';
|
||||||
import { ServerRouteHandler, HttpServer } from '../../../utils/httpServer';
|
import { ServerRouteHandler, HttpServer } from '../../../utils/httpServer';
|
||||||
import { SnapshotServer } from '../../snapshot/snapshotServer';
|
import { SnapshotServer } from '../../snapshot/snapshotServer';
|
||||||
import * as consoleApiSource from '../../../generated/consoleApiSource';
|
import * as consoleApiSource from '../../../generated/consoleApiSource';
|
||||||
import { isUnderTest } from '../../../utils/utils';
|
import { isUnderTest, download } from '../../../utils/utils';
|
||||||
import { internalCallMetadata } from '../../instrumentation';
|
import { internalCallMetadata } from '../../instrumentation';
|
||||||
import { ProgressController } from '../../progress';
|
import { ProgressController } from '../../progress';
|
||||||
import { BrowserContext } from '../../browserContext';
|
import { BrowserContext } from '../../browserContext';
|
||||||
import { registry } from '../../../utils/registry';
|
import { registry } from '../../../utils/registry';
|
||||||
import { installAppIcon } from '../../chromium/crApp';
|
import { installAppIcon } from '../../chromium/crApp';
|
||||||
|
import { debugLogger } from '../../../utils/debugLogger';
|
||||||
|
|
||||||
export class TraceViewer {
|
export class TraceViewer {
|
||||||
private _server: HttpServer;
|
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> {
|
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;
|
let stat;
|
||||||
try {
|
try {
|
||||||
stat = fs.statSync(tracePath);
|
stat = fs.statSync(tracePath);
|
||||||
|
|
@ -210,8 +228,6 @@ export async function showTraceViewer(tracePath: string, browserName: string, he
|
||||||
}
|
}
|
||||||
|
|
||||||
const zipFile = tracePath;
|
const zipFile = tracePath;
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `playwright-trace`));
|
|
||||||
process.on('exit', () => rimraf.sync(dir));
|
|
||||||
try {
|
try {
|
||||||
await extract(zipFile, { dir });
|
await extract(zipFile, { dir });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,7 @@ import extract from 'extract-zip';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import ProgressBar from 'progress';
|
import { existsAsync, download } from './utils';
|
||||||
import { downloadFile, existsAsync } from './utils';
|
|
||||||
import { debugLogger } from './debugLogger';
|
import { debugLogger } from './debugLogger';
|
||||||
|
|
||||||
export async function downloadBrowserWithProgressBar(title: string, browserDirectory: string, executablePath: string, downloadURL: string, downloadFileName: string): Promise<boolean> {
|
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;
|
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 url = downloadURL;
|
||||||
const zipPath = path.join(os.tmpdir(), downloadFileName);
|
const zipPath = path.join(os.tmpdir(), downloadFileName);
|
||||||
try {
|
try {
|
||||||
for (let attempt = 1, N = 3; attempt <= N; ++attempt) {
|
await download(url, zipPath, {
|
||||||
debugLogger.log('install', `downloading ${progressBarName} - attempt #${attempt}`);
|
progressBarName,
|
||||||
const { error } = await downloadFile(url, zipPath, { progressCallback: progress, log: debugLogger.log.bind(debugLogger, 'install') });
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debugLogger.log('install', `extracting archive`);
|
debugLogger.log('install', `extracting archive`);
|
||||||
debugLogger.log('install', `-- zip: ${zipPath}`);
|
debugLogger.log('install', `-- zip: ${zipPath}`);
|
||||||
debugLogger.log('install', `-- location: ${browserDirectory}`);
|
debugLogger.log('install', `-- location: ${browserDirectory}`);
|
||||||
|
|
@ -89,10 +55,6 @@ export async function downloadBrowserWithProgressBar(title: string, browserDirec
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMegabytes(bytes: number) {
|
|
||||||
const mb = bytes / 1024 / 1024;
|
|
||||||
return `${Math.round(mb * 10) / 10} Mb`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logPolitely(toBeLogged: string) {
|
export function logPolitely(toBeLogged: string) {
|
||||||
const logLevel = process.env.npm_config_loglevel;
|
const logLevel = process.env.npm_config_loglevel;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ const debugLoggerColorMap = {
|
||||||
'api': 45, // cyan
|
'api': 45, // cyan
|
||||||
'protocol': 34, // green
|
'protocol': 34, // green
|
||||||
'install': 34, // green
|
'install': 34, // green
|
||||||
|
'download': 34, // green
|
||||||
'browser': 0, // reset
|
'browser': 0, // reset
|
||||||
'proxy': 92, // purple
|
'proxy': 92, // purple
|
||||||
'error': 160, // red,
|
'error': 160, // red,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import { getProxyForUrl } from 'proxy-from-env';
|
||||||
import * as URL from 'url';
|
import * as URL from 'url';
|
||||||
import { getUbuntuVersionSync } from './ubuntuVersion';
|
import { getUbuntuVersionSync } from './ubuntuVersion';
|
||||||
import { NameValue } from '../protocol/channels';
|
import { NameValue } from '../protocol/channels';
|
||||||
|
import ProgressBar from 'progress';
|
||||||
|
|
||||||
// `https-proxy-agent` v5 is written in TypeScript and exposes generated types.
|
// `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
|
// 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 OnProgressCallback = (downloadedBytes: number, totalBytes: number) => void;
|
||||||
type DownloadFileLogger = (message: string) => 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 {
|
const {
|
||||||
progressCallback,
|
progressCallback,
|
||||||
log = () => {},
|
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}> {
|
export function spawnAsync(cmd: string, args: string[], options?: SpawnOptions): Promise<{stdout: string, stderr: string, code: number, error?: Error}> {
|
||||||
const process = spawn(cmd, args, options);
|
const process = spawn(cmd, args, options);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue