Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: allow opening empty trace viewer #5080

Merged
merged 1 commit into from
Jan 21, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:');
Expand Down
129 changes: 73 additions & 56 deletions src/cli/traceViewer/traceViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,66 @@ import { VideoTileGenerator } from './videoTileGenerator';

const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));

type TraceViewerDocument = {
resourcesDir: string;
model: TraceModel;
snapshotRouter: SnapshotRouter;
screenshotGenerator: ScreenshotGenerator;
videoTileGenerator: VideoTileGenerator;
};

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 _traceStorageDir: string;
private _traceModel: TraceModel;
private _snapshotRouter: SnapshotRouter;
private _screenshotGenerator: ScreenshotGenerator;
private _videoTileGenerator: VideoTileGenerator;
private _document: TraceViewerDocument | undefined;

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);
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() {
Expand All @@ -57,17 +96,19 @@ 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];
await snapshotFrame.goto('data:text/html,No snapshot available');
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];
Expand All @@ -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
Expand All @@ -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));
}
Expand All @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion src/cli/traceViewer/web/ui/propertiesTabbedPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/cli/traceViewer/web/web.webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
resolve: {
extensions: ['.ts', '.js', '.tsx', '.jsx']
},
devtool: 'source-map',
output: {
globalObject: 'self',
filename: '[name].bundle.js',
Expand Down
4 changes: 2 additions & 2 deletions src/client/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 6 additions & 12 deletions src/protocol/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?: {
Expand Down Expand Up @@ -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?: {
Expand Down Expand Up @@ -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?: {
Expand Down Expand Up @@ -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?: {
Expand Down Expand Up @@ -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?: {
Expand Down Expand Up @@ -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?: {
Expand Down
9 changes: 3 additions & 6 deletions src/protocol/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,7 @@ BrowserType:
- dark
- no-preference
acceptDownloads: boolean?
_traceResourcesPath: string?
_tracePath: string?
_traceDir: string?
recordVideo:
type: object?
properties:
Expand Down Expand Up @@ -445,8 +444,7 @@ Browser:
- light
- no-preference
acceptDownloads: boolean?
_traceResourcesPath: string?
_tracePath: string?
_traceDir: string?
recordVideo:
type: object?
properties:
Expand Down Expand Up @@ -2336,8 +2334,7 @@ AndroidDevice:
- light
- no-preference
acceptDownloads: boolean?
_traceResourcesPath: string?
_tracePath: string?
_traceDir: string?
recordVideo:
type: object?
properties:
Expand Down
9 changes: 3 additions & 6 deletions src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
3 changes: 1 addition & 2 deletions src/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,7 @@ export type BrowserContextOptions = {
path: string
},
proxy?: ProxySettings,
_tracePath?: string,
_traceResourcesPath?: string,
_traceDir?: string,
};

export type EnvArray = { name: string, value: string }[];
Expand Down
14 changes: 4 additions & 10 deletions src/trace/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down