Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions packages/playwright-core/src/server/trace/test/DEPS.list

This file was deleted.

62 changes: 62 additions & 0 deletions packages/playwright-core/src/server/trace/viewer/traceParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import url from 'url';
import { ZipFile } from '../../utils/zipFile';
import type { TraceLoaderBackend } from '@isomorphic/trace/traceLoader';

export class ZipTraceLoaderBackend implements TraceLoaderBackend {
private _zipFile: ZipFile;
private _traceFile: string;

constructor(traceFile: string) {
this._traceFile = traceFile;
this._zipFile = new ZipFile(traceFile);
}

isLive() {
return false;
}

traceURL() {
return url.pathToFileURL(this._traceFile).toString();
}

async entryNames(): Promise<string[]> {
return await this._zipFile.entries();
}

async hasEntry(entryName: string): Promise<boolean> {
const entries = await this.entryNames();
return entries.includes(entryName);
}

async readText(entryName: string): Promise<string | undefined> {
try {
const buffer = await this._zipFile.read(entryName);
return buffer.toString('utf-8');
} catch {
}
}

async readBlob(entryName: string): Promise<Blob | undefined> {
try {
const buffer = await this._zipFile.read(entryName);
return new Blob([new Uint8Array(buffer)]);
} catch {
}
}
}
51 changes: 47 additions & 4 deletions packages/playwright-core/src/utils/isomorphic/trace/traceModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { getActionGroup } from '@isomorphic/protocolFormatter';
import { getActionGroup, renderTitleForCall } from '@isomorphic/protocolFormatter';

import type { Language } from '@isomorphic/locatorGenerators';
import type { ResourceSnapshot } from '@trace/snapshot';
Expand Down Expand Up @@ -50,7 +50,7 @@ export type ActionTreeItem = {
id: string;
children: ActionTreeItem[];
parent: ActionTreeItem | undefined;
action?: ActionTraceEventInContext;
action: ActionTraceEventInContext;
};

export type ErrorDescription = {
Expand Down Expand Up @@ -146,6 +146,20 @@ export class TraceModel {
return this.actions.filter(action => !action.group || filter.has(action.group));
}

renderActionTree(filter?: ActionGroup[]) {
const actions = this.filteredActions(filter ?? []);
const { rootItem } = buildActionTree(actions);
const actionTree: string[] = [];
const visit = (actionItem: ActionTreeItem, indent: string) => {
const title = renderTitleForCall({ ...actionItem.action, type: actionItem.action.class });
actionTree.push(`${indent}${title || actionItem.id}`);
for (const child of actionItem.children)
visit(child, indent + ' ');
};
rootItem.children.forEach(a => visit(a, ''));
return actionTree;
}

private _errorDescriptorsFromActions(): ErrorDescription[] {
const errors: ErrorDescription[] = [];
for (const action of this.actions || []) {
Expand Down Expand Up @@ -333,9 +347,11 @@ export function buildActionTree(actions: ActionTraceEventInContext[]): { rootIte
});
}

const rootItem: ActionTreeItem = { id: '', parent: undefined, children: [] };
const rootItem: ActionTreeItem = { action: { ...kFakeRootAction }, id: '', parent: undefined, children: [] };
for (const item of itemMap.values()) {
const parent = item.action!.parentId ? itemMap.get(item.action!.parentId) || rootItem : rootItem;
rootItem.action.startTime = Math.min(rootItem.action.startTime, item.action.startTime);
rootItem.action.endTime = Math.max(rootItem.action.endTime, item.action.endTime);
const parent = item.action.parentId ? itemMap.get(item.action.parentId) || rootItem : rootItem;
parent.children.push(item);
item.parent = parent;
}
Expand Down Expand Up @@ -411,3 +427,30 @@ function collectSources(actions: trace.ActionTraceEvent[], errorDescriptors: Err
}
return result;
}

const kFakeRootAction: ActionTraceEventInContext = {
type: 'action',
callId: '',
startTime: 0,
endTime: 0,
class: '',
method: '',
params: {},
log: [],
context: {
origin: 'library',
startTime: 0,
endTime: 0,
browserName: '',
wallTime: 0,
options: {},
pages: [],
resources: [],
actions: [],
events: [],
stdio: [],
errors: [],
hasSource: false,
contextId: '',
},
};
12 changes: 6 additions & 6 deletions packages/trace-viewer/src/ui/actionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,24 +68,24 @@ export const ActionList: React.FC<ActionListProps> = ({
}, [itemMap, selectedAction]);

const isError = React.useCallback((item: ActionTreeItem) => {
return !!item.action?.error?.message;
return !!item.action.error?.message;
}, []);

const onAccepted = React.useCallback((item: ActionTreeItem) => {
return setSelectedTime({ minimum: item.action!.startTime, maximum: item.action!.endTime });
return setSelectedTime({ minimum: item.action.startTime, maximum: item.action.endTime });
}, [setSelectedTime]);

const render = React.useCallback((item: ActionTreeItem) => {
const showAttachments = !!revealActionAttachment && !!item.action?.attachments?.length;
return renderAction(item.action!, { sdkLanguage, revealConsole, revealActionAttachment: () => revealActionAttachment?.(item.action!.callId), isLive, showDuration: true, showBadges: true, showAttachments });
const showAttachments = !!revealActionAttachment && !!item.action.attachments?.length;
return renderAction(item.action, { sdkLanguage, revealConsole, revealActionAttachment: () => revealActionAttachment?.(item.action.callId), isLive, showDuration: true, showBadges: true, showAttachments });
}, [isLive, revealConsole, revealActionAttachment, sdkLanguage]);

const isVisible = React.useCallback((item: ActionTreeItem) => {
return !selectedTime || !item.action || (item.action!.startTime <= selectedTime.maximum && item.action!.endTime >= selectedTime.minimum);
return !selectedTime || !item.action || (item.action.startTime <= selectedTime.maximum && item.action.endTime >= selectedTime.minimum);
}, [selectedTime]);

const onSelectedAction = React.useCallback((item: ActionTreeItem) => {
onSelected?.(item.action!);
onSelected?.(item.action);
}, [onSelected]);

const onHighlightedAction = React.useCallback((item: ActionTreeItem | undefined) => {
Expand Down
84 changes: 7 additions & 77 deletions tests/config/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@

import type { Locator, Frame, Page } from 'playwright-core';
import { ZipFile } from '../../packages/playwright-core/lib/server/utils/zipFile';
import type { TraceLoaderBackend } from '../../packages/playwright-core/src/utils/isomorphic/trace/traceLoader';
import type { StackFrame } from '../../packages/protocol/src/channels';
import { parseClientSideCallMetadata } from '../../packages/playwright-core/lib/utils/isomorphic/traceUtils';
import { TraceLoader } from '../../packages/playwright-core/src/utils/isomorphic/trace/traceLoader';
import type { ActionTreeItem } from '../../packages/playwright-core/src/utils/isomorphic/trace/traceModel';
import { buildActionTree, TraceModel } from '../../packages/playwright-core/src/utils/isomorphic/trace/traceModel';
import type { ActionTraceEvent, ConsoleMessageTraceEvent, EventTraceEvent, TraceEvent } from '@trace/trace';
import { TraceModel } from '../../packages/playwright-core/src/utils/isomorphic/trace/traceModel';
import type { ActionTraceEvent, TraceEvent } from '@trace/trace';
import { renderTitleForCall } from '../../packages/playwright-core/lib/utils/isomorphic/protocolFormatter';
import { ZipTraceLoaderBackend } from '../../packages/playwright-core/lib/server/trace/viewer/traceParser';
import type { SnapshotStorage } from '../../packages/playwright-core/src/utils/isomorphic/trace/snapshotStorage';

export type BoundingBox = Awaited<ReturnType<Locator['boundingBox']>>;

Expand Down Expand Up @@ -165,31 +165,11 @@ export async function parseTraceRaw(file: string): Promise<{ events: any[], reso
};
}

export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: (EventTraceEvent | ConsoleMessageTraceEvent)[], actions: ActionTraceEvent[], titles: string[], loader: TraceLoader, model: TraceModel, actionTree: string[], errors: string[] }> {
const backend = new TraceBackend(file);
export async function parseTrace(file: string): Promise<{ snapshots: SnapshotStorage, model: TraceModel }> {
const backend = new ZipTraceLoaderBackend(file);
const loader = new TraceLoader();
await loader.load(backend, () => {});
const model = new TraceModel(file, loader.contextEntries);
const actions = model.filteredActions([]);
const { rootItem } = buildActionTree(actions);
const actionTree: string[] = [];
const visit = (actionItem: ActionTreeItem, indent: string) => {
const title = renderTitleForCall({ ...actionItem.action, type: actionItem.action.class });
actionTree.push(`${indent}${title || actionItem.id}`);
for (const child of actionItem.children)
visit(child, indent + ' ');
};
rootItem.children.forEach(a => visit(a, ''));
return {
titles: actions.map(a => renderTitleForCall({ ...a, type: a.class })),
resources: backend.entries,
actions,
events: model.events,
errors: model.errors.map(e => e.message),
model,
loader,
actionTree,
};
return { model: new TraceModel(file, loader.contextEntries), snapshots: loader.storage() };
}

export async function parseHar(file: string): Promise<Map<string, Buffer>> {
Expand Down Expand Up @@ -247,53 +227,3 @@ const ansiRegex = new RegExp('[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(
export function stripAnsi(str: string): string {
return str.replace(ansiRegex, '');
}
class TraceBackend implements TraceLoaderBackend {
private _fileName: string;
private _entriesPromise: Promise<Map<string, Buffer>>;
readonly entries = new Map<string, Buffer>();

constructor(fileName: string) {
this._fileName = fileName;
this._entriesPromise = this._readEntries();
}

private async _readEntries(): Promise<Map<string, Buffer>> {
const zipFS = new ZipFile(this._fileName);
for (const entry of await zipFS.entries())
this.entries.set(entry, await zipFS.read(entry));
zipFS.close();
return this.entries;
}

isLive() {
return false;
}

traceURL() {
return 'file://' + this._fileName;
}

async entryNames(): Promise<string[]> {
const entries = await this._entriesPromise;
return [...entries.keys()];
}

async hasEntry(entryName: string): Promise<boolean> {
const entries = await this._entriesPromise;
return entries.has(entryName);
}

async readText(entryName: string): Promise<string | undefined> {
const entries = await this._entriesPromise;
const entry = entries.get(entryName);
if (!entry)
return;
return entry.toString();
}

async readBlob(entryName: string) {
const entries = await this._entriesPromise;
const entry = entries.get(entryName);
return entry as any;
}
}
4 changes: 2 additions & 2 deletions tests/library/browsertype-connect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import * as path from 'path';
import { getUserAgent, getPlaywrightVersion } from '../../packages/playwright-core/lib/server/utils/userAgent';
import WebSocket from 'ws';
import { expect, playwrightTest } from '../config/browserTest';
import { parseTrace, suppressCertificateWarning, rafraf } from '../config/utils';
import { parseTraceRaw, suppressCertificateWarning, rafraf } from '../config/utils';
import formidable from 'formidable';
import type { Browser, ConnectOptions } from 'playwright-core';
import { createHttpServer } from '../../packages/playwright-core/lib/server/utils/network';
Expand Down Expand Up @@ -658,7 +658,7 @@ for (const kind of ['launchServer', 'run-server'] as const) {
await context.close();
await browser.close();

const { resources } = await parseTrace(testInfo.outputPath('trace1.zip'));
const { resources } = await parseTraceRaw(testInfo.outputPath('trace1.zip'));
const sourceNames = Array.from(resources.keys()).filter(k => k.endsWith('.txt'));
expect(sourceNames.length).toBe(1);
const sourceFile = resources.get(sourceNames[0]);
Expand Down
5 changes: 2 additions & 3 deletions tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,10 +733,9 @@ test('empty adopted style sheets should not prevent node refs', async ({ page })
await page.context().tracing.stop({ path: traceFile });

const trace = await parseTrace(traceFile);
const snapshots = trace.loader.storage();
const secondEvaluate = trace.actions.findLast(a => a.method === 'evaluateExpression');
const secondEvaluate = trace.model.actions.findLast(a => a.method === 'evaluateExpression');
expect(secondEvaluate.beforeSnapshot).toBeTruthy();
const snapshot = snapshots.snapshotByName(snapshots.snapshotsForTest()[0], secondEvaluate.beforeSnapshot);
const snapshot = trace.snapshots.snapshotByName(trace.snapshots.snapshotsForTest()[0], secondEvaluate.beforeSnapshot);
// Second snapshot should be just a copy of the first one.
expect(snapshot.snapshot().html).toEqual([[1, 9]]);
});
Expand Down
18 changes: 9 additions & 9 deletions tests/playwright-test/playwright.connect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,18 +213,18 @@ test('should record trace', async ({ runInlineTest }) => {
expect(fs.existsSync(test.info().outputPath('test-results', 'a-pass', 'trace.zip'))).toBe(false);

const trace = await parseTrace(test.info().outputPath('test-results', 'a-fail', 'trace.zip'));
expect(trace.titles).toEqual([
expect(trace.model.renderActionTree()).toEqual([
'Before Hooks',
'Fixture "context"',
'Create context',
'Fixture "page"',
'Create page',
' Fixture "context"',
' Create context',
' Fixture "page"',
' Create page',
'Expect "toBe"',
'After Hooks',
'Fixture "page"',
'Fixture "context"',
'Close context',
' Fixture "page"',
' Fixture "context"',
' Close context',
'Worker Cleanup',
'Fixture "browser"',
' Fixture "browser"',
]);
});
Loading
Loading