mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: allow opening empty trace viewer (#5080)
This commit is contained in:
parent
16249ccbda
commit
4b5c876bbf
@ -140,11 +140,10 @@ program
|
||||
|
||||
if (process.env.PWTRACE) {
|
||||
program
|
||||
.command('show-trace <trace>')
|
||||
.command('show-trace [trace]')
|
||||
.description('Show trace viewer')
|
||||
.option('--resources <dir>', 'Directory with the shared trace artifacts')
|
||||
.action(function(trace, command) {
|
||||
showTraceViewer(command.resources, trace);
|
||||
showTraceViewer(trace);
|
||||
}).on('--help', function() {
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
|
@ -26,27 +26,66 @@ import { VideoTileGenerator } from './videoTileGenerator';
|
||||
|
||||
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
|
||||
|
||||
class TraceViewer {
|
||||
private _traceStorageDir: string;
|
||||
private _traceModel: TraceModel;
|
||||
private _snapshotRouter: SnapshotRouter;
|
||||
private _screenshotGenerator: ScreenshotGenerator;
|
||||
private _videoTileGenerator: VideoTileGenerator;
|
||||
type TraceViewerDocument = {
|
||||
resourcesDir: string;
|
||||
model: TraceModel;
|
||||
snapshotRouter: SnapshotRouter;
|
||||
screenshotGenerator: ScreenshotGenerator;
|
||||
videoTileGenerator: VideoTileGenerator;
|
||||
};
|
||||
|
||||
constructor(traceStorageDir: string) {
|
||||
this._traceStorageDir = traceStorageDir;
|
||||
this._snapshotRouter = new SnapshotRouter(traceStorageDir);
|
||||
this._traceModel = {
|
||||
contexts: [],
|
||||
};
|
||||
this._screenshotGenerator = new ScreenshotGenerator(traceStorageDir, this._traceModel);
|
||||
this._videoTileGenerator = new VideoTileGenerator(this._traceModel);
|
||||
const emptyModel: TraceModel = {
|
||||
contexts: [
|
||||
{
|
||||
startTime: 0,
|
||||
endTime: 1,
|
||||
created: {
|
||||
timestamp: Date.now(),
|
||||
type: 'context-created',
|
||||
browserName: 'none',
|
||||
contextId: '<empty>',
|
||||
deviceScaleFactor: 1,
|
||||
isMobile: false,
|
||||
viewportSize: { width: 800, height: 600 },
|
||||
},
|
||||
destroyed: {
|
||||
timestamp: Date.now(),
|
||||
type: 'context-destroyed',
|
||||
contextId: '<empty>',
|
||||
},
|
||||
name: '<empty>',
|
||||
filePath: '',
|
||||
pages: [],
|
||||
resourcesByUrl: new Map()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
class TraceViewer {
|
||||
private _document: TraceViewerDocument | undefined;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async load(filePath: string) {
|
||||
const traceContent = await fsReadFileAsync(filePath, 'utf8');
|
||||
const events = traceContent.split('\n').map(line => line.trim()).filter(line => !!line).map(line => JSON.parse(line)) as TraceEvent[];
|
||||
readTraceFile(events, this._traceModel, filePath);
|
||||
async load(traceDir: string) {
|
||||
const resourcesDir = path.join(traceDir, 'resources');
|
||||
const model = { contexts: [] };
|
||||
this._document = {
|
||||
model,
|
||||
resourcesDir,
|
||||
snapshotRouter: new SnapshotRouter(resourcesDir),
|
||||
screenshotGenerator: new ScreenshotGenerator(resourcesDir, model),
|
||||
videoTileGenerator: new VideoTileGenerator(model)
|
||||
};
|
||||
|
||||
for (const name of fs.readdirSync(traceDir)) {
|
||||
if (!name.endsWith('.trace'))
|
||||
continue;
|
||||
const filePath = path.join(traceDir, name);
|
||||
const traceContent = await fsReadFileAsync(filePath, 'utf8');
|
||||
const events = traceContent.split('\n').map(line => line.trim()).filter(line => !!line).map(line => JSON.parse(line)) as TraceEvent[];
|
||||
readTraceFile(events, model, filePath);
|
||||
}
|
||||
}
|
||||
|
||||
async show() {
|
||||
@ -57,6 +96,8 @@ class TraceViewer {
|
||||
return fs.readFileSync(path).toString();
|
||||
});
|
||||
await uiPage.exposeBinding('renderSnapshot', async (_, action: ActionTraceEvent) => {
|
||||
if (!this._document)
|
||||
return;
|
||||
try {
|
||||
if (!action.snapshot) {
|
||||
const snapshotFrame = uiPage.frames()[1];
|
||||
@ -64,10 +105,10 @@ class TraceViewer {
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = await fsReadFileAsync(path.join(this._traceStorageDir, action.snapshot!.sha1), 'utf8');
|
||||
const snapshot = await fsReadFileAsync(path.join(this._document.resourcesDir, action.snapshot!.sha1), 'utf8');
|
||||
const snapshotObject = JSON.parse(snapshot) as PageSnapshot;
|
||||
const contextEntry = this._traceModel.contexts.find(entry => entry.created.contextId === action.contextId)!;
|
||||
this._snapshotRouter.selectSnapshot(snapshotObject, contextEntry);
|
||||
const contextEntry = this._document.model.contexts.find(entry => entry.created.contextId === action.contextId)!;
|
||||
this._document.snapshotRouter.selectSnapshot(snapshotObject, contextEntry);
|
||||
|
||||
// TODO: fix Playwright bug where frame.name is lost (empty).
|
||||
const snapshotFrame = uiPage.frames()[1];
|
||||
@ -88,21 +129,21 @@ class TraceViewer {
|
||||
console.log(e); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
await uiPage.exposeBinding('getTraceModel', () => this._traceModel);
|
||||
await uiPage.exposeBinding('getTraceModel', () => this._document ? this._document.model : emptyModel);
|
||||
await uiPage.exposeBinding('getVideoMetaInfo', async (_, videoId: string) => {
|
||||
return this._videoTileGenerator.render(videoId);
|
||||
return this._document ? this._document.videoTileGenerator.render(videoId) : null;
|
||||
});
|
||||
await uiPage.route('**/*', (route, request) => {
|
||||
if (request.frame().parentFrame()) {
|
||||
this._snapshotRouter.route(route);
|
||||
if (request.frame().parentFrame() && this._document) {
|
||||
this._document.snapshotRouter.route(route);
|
||||
return;
|
||||
}
|
||||
const url = new URL(request.url());
|
||||
try {
|
||||
if (request.url().includes('action-preview')) {
|
||||
if (this._document && request.url().includes('action-preview')) {
|
||||
const fullPath = url.pathname.substring('/action-preview/'.length);
|
||||
const actionId = fullPath.substring(0, fullPath.indexOf('.png'));
|
||||
this._screenshotGenerator.generateScreenshot(actionId).then(body => {
|
||||
this._document.screenshotGenerator.generateScreenshot(actionId).then(body => {
|
||||
if (body)
|
||||
route.fulfill({ contentType: 'image/png', body });
|
||||
else
|
||||
@ -111,9 +152,9 @@ class TraceViewer {
|
||||
return;
|
||||
}
|
||||
let filePath: string;
|
||||
if (request.url().includes('video-tile')) {
|
||||
if (this._document && request.url().includes('video-tile')) {
|
||||
const fullPath = url.pathname.substring('/video-tile/'.length);
|
||||
filePath = this._videoTileGenerator.tilePath(fullPath);
|
||||
filePath = this._document.videoTileGenerator.tilePath(fullPath);
|
||||
} else {
|
||||
filePath = path.join(__dirname, 'web', url.pathname.substring(1));
|
||||
}
|
||||
@ -133,37 +174,13 @@ class TraceViewer {
|
||||
}
|
||||
}
|
||||
|
||||
export async function showTraceViewer(traceStorageDir: string | undefined, tracePath: string) {
|
||||
if (!fs.existsSync(tracePath))
|
||||
throw new Error(`${tracePath} does not exist`);
|
||||
|
||||
const files: string[] = fs.statSync(tracePath).isFile() ? [tracePath] : collectFiles(tracePath);
|
||||
|
||||
if (!traceStorageDir) {
|
||||
traceStorageDir = fs.statSync(tracePath).isFile() ? path.dirname(tracePath) : tracePath;
|
||||
|
||||
if (fs.existsSync(traceStorageDir + '/trace-resources'))
|
||||
traceStorageDir = traceStorageDir + '/trace-resources';
|
||||
}
|
||||
|
||||
const traceViewer = new TraceViewer(traceStorageDir);
|
||||
for (const filePath of files)
|
||||
await traceViewer.load(filePath);
|
||||
export async function showTraceViewer(traceDir: string) {
|
||||
const traceViewer = new TraceViewer();
|
||||
if (traceDir)
|
||||
await traceViewer.load(traceDir);
|
||||
await traceViewer.show();
|
||||
}
|
||||
|
||||
function collectFiles(dir: string): string[] {
|
||||
const files = [];
|
||||
for (const name of fs.readdirSync(dir)) {
|
||||
const fullName = path.join(dir, name);
|
||||
if (fs.lstatSync(fullName).isDirectory())
|
||||
files.push(...collectFiles(fullName));
|
||||
else if (fullName.endsWith('.trace'))
|
||||
files.push(fullName);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const extensionToMime: { [key: string]: string } = {
|
||||
'css': 'text/css',
|
||||
'html': 'text/html',
|
||||
|
@ -80,7 +80,7 @@ const SnapshotTab: React.FunctionComponent<{
|
||||
|
||||
React.useEffect(() => {
|
||||
if (actionEntry)
|
||||
window.renderSnapshot(actionEntry.action);
|
||||
(window as any).renderSnapshot(actionEntry.action);
|
||||
}, [actionEntry]);
|
||||
|
||||
const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height);
|
||||
|
@ -9,6 +9,7 @@ module.exports = {
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.tsx', '.jsx']
|
||||
},
|
||||
devtool: 'source-map',
|
||||
output: {
|
||||
globalObject: 'self',
|
||||
filename: '[name].bundle.js',
|
||||
|
@ -45,8 +45,8 @@ export class Browser extends ChannelOwner<channels.BrowserChannel, channels.Brow
|
||||
|
||||
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||
return this._wrapApiCall('browser.newContext', async () => {
|
||||
if (this._isRemote && options._tracePath)
|
||||
throw new Error(`"_tracePath" is not supported in connected browser`);
|
||||
if (this._isRemote && options._traceDir)
|
||||
throw new Error(`"_traceDir" is not supported in connected browser`);
|
||||
const contextOptions = await prepareBrowserContextOptions(options);
|
||||
const context = BrowserContext.from((await this._channel.newContext(contextOptions)).context);
|
||||
context._options = contextOptions;
|
||||
|
@ -297,8 +297,7 @@ export type BrowserTypeLaunchPersistentContextParams = {
|
||||
hasTouch?: boolean,
|
||||
colorScheme?: 'light' | 'dark' | 'no-preference',
|
||||
acceptDownloads?: boolean,
|
||||
_traceResourcesPath?: string,
|
||||
_tracePath?: string,
|
||||
_traceDir?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
size?: {
|
||||
@ -360,8 +359,7 @@ export type BrowserTypeLaunchPersistentContextOptions = {
|
||||
hasTouch?: boolean,
|
||||
colorScheme?: 'light' | 'dark' | 'no-preference',
|
||||
acceptDownloads?: boolean,
|
||||
_traceResourcesPath?: string,
|
||||
_tracePath?: string,
|
||||
_traceDir?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
size?: {
|
||||
@ -424,8 +422,7 @@ export type BrowserNewContextParams = {
|
||||
hasTouch?: boolean,
|
||||
colorScheme?: 'dark' | 'light' | 'no-preference',
|
||||
acceptDownloads?: boolean,
|
||||
_traceResourcesPath?: string,
|
||||
_tracePath?: string,
|
||||
_traceDir?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
size?: {
|
||||
@ -477,8 +474,7 @@ export type BrowserNewContextOptions = {
|
||||
hasTouch?: boolean,
|
||||
colorScheme?: 'dark' | 'light' | 'no-preference',
|
||||
acceptDownloads?: boolean,
|
||||
_traceResourcesPath?: string,
|
||||
_tracePath?: string,
|
||||
_traceDir?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
size?: {
|
||||
@ -2772,8 +2768,7 @@ export type AndroidDeviceLaunchBrowserParams = {
|
||||
hasTouch?: boolean,
|
||||
colorScheme?: 'dark' | 'light' | 'no-preference',
|
||||
acceptDownloads?: boolean,
|
||||
_traceResourcesPath?: string,
|
||||
_tracePath?: string,
|
||||
_traceDir?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
size?: {
|
||||
@ -2817,8 +2812,7 @@ export type AndroidDeviceLaunchBrowserOptions = {
|
||||
hasTouch?: boolean,
|
||||
colorScheme?: 'dark' | 'light' | 'no-preference',
|
||||
acceptDownloads?: boolean,
|
||||
_traceResourcesPath?: string,
|
||||
_tracePath?: string,
|
||||
_traceDir?: string,
|
||||
recordVideo?: {
|
||||
dir: string,
|
||||
size?: {
|
||||
|
@ -372,8 +372,7 @@ BrowserType:
|
||||
- dark
|
||||
- no-preference
|
||||
acceptDownloads: boolean?
|
||||
_traceResourcesPath: string?
|
||||
_tracePath: string?
|
||||
_traceDir: string?
|
||||
recordVideo:
|
||||
type: object?
|
||||
properties:
|
||||
@ -445,8 +444,7 @@ Browser:
|
||||
- light
|
||||
- no-preference
|
||||
acceptDownloads: boolean?
|
||||
_traceResourcesPath: string?
|
||||
_tracePath: string?
|
||||
_traceDir: string?
|
||||
recordVideo:
|
||||
type: object?
|
||||
properties:
|
||||
@ -2336,8 +2334,7 @@ AndroidDevice:
|
||||
- light
|
||||
- no-preference
|
||||
acceptDownloads: boolean?
|
||||
_traceResourcesPath: string?
|
||||
_tracePath: string?
|
||||
_traceDir: string?
|
||||
recordVideo:
|
||||
type: object?
|
||||
properties:
|
||||
|
@ -211,8 +211,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||
hasTouch: tOptional(tBoolean),
|
||||
colorScheme: tOptional(tEnum(['light', 'dark', 'no-preference'])),
|
||||
acceptDownloads: tOptional(tBoolean),
|
||||
_traceResourcesPath: tOptional(tString),
|
||||
_tracePath: tOptional(tString),
|
||||
_traceDir: tOptional(tString),
|
||||
recordVideo: tOptional(tObject({
|
||||
dir: tString,
|
||||
size: tOptional(tObject({
|
||||
@ -255,8 +254,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||
hasTouch: tOptional(tBoolean),
|
||||
colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])),
|
||||
acceptDownloads: tOptional(tBoolean),
|
||||
_traceResourcesPath: tOptional(tString),
|
||||
_tracePath: tOptional(tString),
|
||||
_traceDir: tOptional(tString),
|
||||
recordVideo: tOptional(tObject({
|
||||
dir: tString,
|
||||
size: tOptional(tObject({
|
||||
@ -1040,8 +1038,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||
hasTouch: tOptional(tBoolean),
|
||||
colorScheme: tOptional(tEnum(['dark', 'light', 'no-preference'])),
|
||||
acceptDownloads: tOptional(tBoolean),
|
||||
_traceResourcesPath: tOptional(tString),
|
||||
_tracePath: tOptional(tString),
|
||||
_traceDir: tOptional(tString),
|
||||
recordVideo: tOptional(tObject({
|
||||
dir: tString,
|
||||
size: tOptional(tObject({
|
||||
|
@ -248,8 +248,7 @@ export type BrowserContextOptions = {
|
||||
path: string
|
||||
},
|
||||
proxy?: ProxySettings,
|
||||
_tracePath?: string,
|
||||
_traceResourcesPath?: string,
|
||||
_traceDir?: string,
|
||||
};
|
||||
|
||||
export type EnvArray = { name: string, value: string }[];
|
||||
|
@ -43,17 +43,11 @@ class Tracer implements ContextListener {
|
||||
private _contextTracers = new Map<BrowserContext, ContextTracer>();
|
||||
|
||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||
let traceStorageDir: string;
|
||||
let tracePath: string;
|
||||
if (context._options._tracePath) {
|
||||
traceStorageDir = context._options._traceResourcesPath || path.join(path.dirname(context._options._tracePath), 'trace-resources');
|
||||
tracePath = context._options._tracePath;
|
||||
} else if (envTrace) {
|
||||
traceStorageDir = envTrace;
|
||||
tracePath = path.join(envTrace, createGuid() + '.trace');
|
||||
} else {
|
||||
const traceDir = envTrace || context._options._traceDir;
|
||||
if (!traceDir)
|
||||
return;
|
||||
}
|
||||
const traceStorageDir = path.join(traceDir, 'resources');
|
||||
const tracePath = path.join(traceDir, createGuid() + '.trace');
|
||||
const contextTracer = new ContextTracer(context, traceStorageDir, tracePath);
|
||||
this._contextTracers.set(context, contextTracer);
|
||||
}
|
||||
|
@ -20,14 +20,13 @@ import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
it('should record trace', async ({browser, testInfo, server}) => {
|
||||
const artifactsPath = testInfo.outputPath('');
|
||||
const tracePath = path.join(artifactsPath, 'playwright.trace');
|
||||
const context = await browser.newContext({ _tracePath: tracePath } as any);
|
||||
const traceDir = testInfo.outputPath('trace');
|
||||
const context = await browser.newContext({ _traceDir: traceDir } as any);
|
||||
const page = await context.newPage();
|
||||
const url = server.PREFIX + '/snapshot/snapshot-with-css.html';
|
||||
await page.goto(url);
|
||||
await context.close();
|
||||
|
||||
const tracePath = path.join(traceDir, fs.readdirSync(traceDir).find(n => n.endsWith('.trace')));
|
||||
const traceFileContent = await fs.promises.readFile(tracePath, 'utf8');
|
||||
const traceEvents = traceFileContent.split('\n').filter(line => !!line).map(line => JSON.parse(line)) as trace.TraceEvent[];
|
||||
|
||||
@ -47,5 +46,5 @@ it('should record trace', async ({browser, testInfo, server}) => {
|
||||
expect(gotoEvent.value).toBe(url);
|
||||
|
||||
expect(gotoEvent.snapshot).toBeTruthy();
|
||||
expect(fs.existsSync(path.join(artifactsPath, 'trace-resources', gotoEvent.snapshot!.sha1))).toBe(true);
|
||||
expect(fs.existsSync(path.join(traceDir, 'resources', gotoEvent.snapshot!.sha1))).toBe(true);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user