mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: remove trace viewer (#3869)
This commit is contained in:
parent
dfbd1ceacc
commit
c20cbae529
1126
package-lock.json
generated
1126
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -29,8 +29,7 @@
|
||||
"typecheck-tests": "tsc -p ./test/",
|
||||
"roll-browser": "node utils/roll_browser.js",
|
||||
"coverage": "node test/checkCoverage.js",
|
||||
"check-deps": "node utils/check_deps.js",
|
||||
"show-trace": "node utils/showTestTraces.js"
|
||||
"check-deps": "node utils/check_deps.js"
|
||||
},
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
|
@ -46,7 +46,7 @@ export class ConsoleMessage extends ChannelOwner<channels.ConsoleMessageChannel,
|
||||
return this._initializer.location;
|
||||
}
|
||||
|
||||
[util.inspect.custom]() {
|
||||
[(util.inspect as any).custom]() {
|
||||
return this.text();
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,8 @@ import * as types from '../server/types';
|
||||
import { SnapshotData, takeSnapshotInFrame } from './snapshotterInjected';
|
||||
import { assert, calculateSha1, createGuid } from '../utils/utils';
|
||||
|
||||
export type SanpshotterResource = {
|
||||
export type SnapshotterResource = {
|
||||
pageId: string,
|
||||
frameId: string,
|
||||
url: string,
|
||||
contentType: string,
|
||||
@ -53,7 +54,8 @@ export type PageSnapshot = {
|
||||
|
||||
export interface SnapshotterDelegate {
|
||||
onBlob(blob: SnapshotterBlob): void;
|
||||
onResource(resource: SanpshotterResource): void;
|
||||
onResource(resource: SnapshotterResource): void;
|
||||
pageId(page: Page): string;
|
||||
}
|
||||
|
||||
export class Snapshotter {
|
||||
@ -75,11 +77,11 @@ export class Snapshotter {
|
||||
|
||||
private _onPage(page: Page) {
|
||||
this._eventListeners.push(helper.addEventListener(page, Page.Events.Response, (response: network.Response) => {
|
||||
this._saveResource(response).catch(e => debugLogger.log('error', e));
|
||||
this._saveResource(page, response).catch(e => debugLogger.log('error', e));
|
||||
}));
|
||||
}
|
||||
|
||||
private async _saveResource(response: network.Response) {
|
||||
private async _saveResource(page: Page, response: network.Response) {
|
||||
const isRedirect = response.status() >= 300 && response.status() <= 399;
|
||||
if (isRedirect)
|
||||
return;
|
||||
@ -98,7 +100,8 @@ export class Snapshotter {
|
||||
|
||||
const body = await response.body().catch(e => debugLogger.log('error', e));
|
||||
const sha1 = body ? calculateSha1(body) : 'none';
|
||||
const resource: SanpshotterResource = {
|
||||
const resource: SnapshotterResource = {
|
||||
pageId: this._delegate.pageId(page),
|
||||
frameId: response.frame()._id,
|
||||
url,
|
||||
contentType,
|
||||
|
@ -31,6 +31,7 @@ export type ContextDestroyedTraceEvent = {
|
||||
export type NetworkResourceTraceEvent = {
|
||||
type: 'resource',
|
||||
contextId: string,
|
||||
pageId: string,
|
||||
frameId: string,
|
||||
url: string,
|
||||
contentType: string,
|
||||
|
@ -1,299 +0,0 @@
|
||||
/**
|
||||
* 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 * as path from 'path';
|
||||
import * as util from 'util';
|
||||
import * as fs from 'fs';
|
||||
import type { NetworkResourceTraceEvent, ActionTraceEvent, ContextCreatedTraceEvent, ContextDestroyedTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent } from './traceTypes';
|
||||
import type { FrameSnapshot, PageSnapshot } from './snapshotter';
|
||||
import type { Browser, BrowserContext, Frame, Page, Route } from '../client/api';
|
||||
import type { Playwright } from '../client/playwright';
|
||||
|
||||
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
|
||||
type TraceEvent =
|
||||
ContextCreatedTraceEvent |
|
||||
ContextDestroyedTraceEvent |
|
||||
PageCreatedTraceEvent |
|
||||
PageDestroyedTraceEvent |
|
||||
NetworkResourceTraceEvent |
|
||||
ActionTraceEvent;
|
||||
|
||||
class TraceViewer {
|
||||
private _playwright: Playwright;
|
||||
private _traceStorageDir: string;
|
||||
private _traces: { traceFile: string, events: TraceEvent[] }[] = [];
|
||||
private _browserNames = new Set<string>();
|
||||
private _resourceEventsByUrl = new Map<string, NetworkResourceTraceEvent[]>();
|
||||
private _contextEventById = new Map<string, ContextCreatedTraceEvent>();
|
||||
private _contextById = new Map<string, BrowserContext>();
|
||||
|
||||
constructor(playwright: Playwright, traceStorageDir: string) {
|
||||
this._playwright = playwright;
|
||||
this._traceStorageDir = traceStorageDir;
|
||||
}
|
||||
|
||||
async load(traceFile: string) {
|
||||
// TODO: validate trace?
|
||||
const traceContent = await fsReadFileAsync(traceFile, 'utf8');
|
||||
const events = traceContent.split('\n').map(line => line.trim()).filter(line => !!line).map(line => JSON.parse(line));
|
||||
for (const event of events) {
|
||||
if (event.type === 'context-created')
|
||||
this._browserNames.add(event.browserName);
|
||||
if (event.type === 'resource') {
|
||||
let responseEvents = this._resourceEventsByUrl.get(event.url);
|
||||
if (!responseEvents) {
|
||||
responseEvents = [];
|
||||
this._resourceEventsByUrl.set(event.url, responseEvents);
|
||||
}
|
||||
responseEvents.push(event);
|
||||
}
|
||||
if (event.type === 'context-created')
|
||||
this._contextEventById.set(event.contextId, event);
|
||||
}
|
||||
this._traces.push({ traceFile, events });
|
||||
}
|
||||
|
||||
browserNames(): Set<string> {
|
||||
return this._browserNames;
|
||||
}
|
||||
|
||||
async show(browserName: string) {
|
||||
const browser = await this._playwright[browserName as ('chromium' | 'firefox' | 'webkit')].launch({ headless: false });
|
||||
const uiPage = await browser.newPage();
|
||||
await uiPage.exposeBinding('renderSnapshot', async (source, action: ActionTraceEvent) => {
|
||||
const snapshot = await fsReadFileAsync(path.join(this._traceStorageDir, action.snapshot!.sha1), 'utf8');
|
||||
const context = await this._ensureContext(browser, action.contextId);
|
||||
const page = await context.newPage();
|
||||
await this._renderSnapshot(page, JSON.parse(snapshot), action.contextId);
|
||||
});
|
||||
|
||||
const contextData: { [contextId: string]: { label: string, actions: ActionTraceEvent[] } } = {};
|
||||
for (const trace of this._traces) {
|
||||
let contextId = 0;
|
||||
for (const event of trace.events) {
|
||||
if (event.type !== 'action')
|
||||
continue;
|
||||
const contextEvent = this._contextEventById.get(event.contextId)!;
|
||||
if (contextEvent.browserName !== browserName)
|
||||
continue;
|
||||
let data = contextData[contextEvent.contextId];
|
||||
if (!data) {
|
||||
data = { label: trace.traceFile + ' :: context' + (++contextId), actions: [] };
|
||||
contextData[contextEvent.contextId] = data;
|
||||
}
|
||||
data.actions.push(event);
|
||||
}
|
||||
}
|
||||
await uiPage.evaluate(traces => {
|
||||
function createSection(parent: Element, title: string): HTMLDetailsElement {
|
||||
const details = document.createElement('details');
|
||||
details.style.paddingLeft = '10px';
|
||||
const summary = document.createElement('summary');
|
||||
summary.textContent = title;
|
||||
details.appendChild(summary);
|
||||
parent.appendChild(details);
|
||||
return details;
|
||||
}
|
||||
|
||||
function createField(parent: Element, text: string) {
|
||||
const div = document.createElement('div');
|
||||
div.style.whiteSpace = 'pre';
|
||||
div.textContent = text;
|
||||
parent.appendChild(div);
|
||||
}
|
||||
|
||||
for (const trace of traces) {
|
||||
const traceSection = createSection(document.body, trace.traceFile);
|
||||
traceSection.open = true;
|
||||
|
||||
const contextSections = new Map<string, Element>();
|
||||
const pageSections = new Map<string, Element>();
|
||||
|
||||
for (const event of trace.events) {
|
||||
if (event.type === 'context-created') {
|
||||
const contextSection = createSection(traceSection, event.contextId);
|
||||
contextSection.open = true;
|
||||
contextSections.set(event.contextId, contextSection);
|
||||
}
|
||||
if (event.type === 'page-created') {
|
||||
const contextSection = contextSections.get(event.contextId)!;
|
||||
const pageSection = createSection(contextSection, event.pageId);
|
||||
pageSection.open = true;
|
||||
pageSections.set(event.pageId, pageSection);
|
||||
}
|
||||
if (event.type === 'action') {
|
||||
const parentSection = event.pageId ? pageSections.get(event.pageId)! : contextSections.get(event.contextId)!;
|
||||
const actionSection = createSection(parentSection, event.action);
|
||||
if (event.label)
|
||||
createField(actionSection, `label: ${event.label}`);
|
||||
if (event.target)
|
||||
createField(actionSection, `target: ${event.target}`);
|
||||
if (event.value)
|
||||
createField(actionSection, `value: ${event.value}`);
|
||||
if (event.startTime && event.endTime)
|
||||
createField(actionSection, `duration: ${event.endTime - event.startTime}ms`);
|
||||
if (event.error) {
|
||||
const errorSection = createSection(actionSection, 'error');
|
||||
createField(errorSection, event.error);
|
||||
}
|
||||
if (event.stack) {
|
||||
const errorSection = createSection(actionSection, 'stack');
|
||||
createField(errorSection, event.stack);
|
||||
}
|
||||
if (event.logs && event.logs.length) {
|
||||
const errorSection = createSection(actionSection, 'logs');
|
||||
createField(errorSection, event.logs.join('\n'));
|
||||
}
|
||||
if (event.snapshot) {
|
||||
const button = document.createElement('button');
|
||||
button.style.display = 'block';
|
||||
button.textContent = `snapshot after (${event.snapshot.duration}ms)`;
|
||||
button.addEventListener('click', () => (window as any).renderSnapshot(event));
|
||||
actionSection.appendChild(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, this._traces);
|
||||
}
|
||||
|
||||
private async _ensureContext(browser: Browser, contextId: string): Promise<BrowserContext> {
|
||||
let context = this._contextById.get(contextId);
|
||||
if (!context) {
|
||||
const event = this._contextEventById.get(contextId)!;
|
||||
context = await browser.newContext({
|
||||
isMobile: event.isMobile,
|
||||
viewport: event.viewportSize || null,
|
||||
deviceScaleFactor: event.deviceScaleFactor,
|
||||
});
|
||||
this._contextById.set(contextId, context);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private async _readResource(event: NetworkResourceTraceEvent, overrideSha1: string | undefined) {
|
||||
try {
|
||||
const body = await fsReadFileAsync(path.join(this._traceStorageDir, overrideSha1 || event.sha1));
|
||||
return {
|
||||
contentType: event.contentType,
|
||||
body,
|
||||
headers: event.responseHeaders,
|
||||
};
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _renderSnapshot(page: Page, snapshot: PageSnapshot, contextId: string): Promise<void> {
|
||||
const frameBySrc = new Map<string, FrameSnapshot>();
|
||||
for (const frameSnapshot of snapshot.frames)
|
||||
frameBySrc.set(frameSnapshot.url, frameSnapshot);
|
||||
|
||||
const intercepted: Promise<any>[] = [];
|
||||
|
||||
const unknownUrls = new Set<string>();
|
||||
const unknown = (route: Route): void => {
|
||||
const url = route.request().url();
|
||||
if (!unknownUrls.has(url)) {
|
||||
console.log(`Request to unknown url: ${url}`); /* eslint-disable-line no-console */
|
||||
unknownUrls.add(url);
|
||||
}
|
||||
intercepted.push(route.abort());
|
||||
};
|
||||
|
||||
await page.route('**', async route => {
|
||||
const url = route.request().url();
|
||||
if (frameBySrc.has(url)) {
|
||||
const frameSnapshot = frameBySrc.get(url)!;
|
||||
intercepted.push(route.fulfill({
|
||||
contentType: 'text/html',
|
||||
body: Buffer.from(frameSnapshot.html),
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const frameSrc = route.request().frame().url();
|
||||
const frameSnapshot = frameBySrc.get(frameSrc);
|
||||
if (!frameSnapshot)
|
||||
return unknown(route);
|
||||
|
||||
// Find a matching resource from the same context, preferrably from the same frame.
|
||||
// Note: resources are stored without hash, but page may reference them with hash.
|
||||
let resource: NetworkResourceTraceEvent | null = null;
|
||||
for (const resourceEvent of this._resourceEventsByUrl.get(removeHash(url)) || []) {
|
||||
if (resourceEvent.contextId !== contextId)
|
||||
continue;
|
||||
if (resource && resourceEvent.frameId !== frameSnapshot.frameId)
|
||||
continue;
|
||||
resource = resourceEvent;
|
||||
if (resourceEvent.frameId === frameSnapshot.frameId)
|
||||
break;
|
||||
}
|
||||
if (!resource)
|
||||
return unknown(route);
|
||||
|
||||
// This particular frame might have a resource content override, for example when
|
||||
// stylesheet is modified using CSSOM.
|
||||
const resourceOverride = frameSnapshot.resourceOverrides.find(o => o.url === url);
|
||||
const overrideSha1 = resourceOverride ? resourceOverride.sha1 : undefined;
|
||||
const resourceData = await this._readResource(resource, overrideSha1);
|
||||
if (!resourceData)
|
||||
return unknown(route);
|
||||
const headers: { [key: string]: string } = {};
|
||||
for (const { name, value } of resourceData.headers)
|
||||
headers[name] = value;
|
||||
headers['Access-Control-Allow-Origin'] = '*';
|
||||
intercepted.push(route.fulfill({
|
||||
contentType: resourceData.contentType,
|
||||
body: resourceData.body,
|
||||
headers,
|
||||
}));
|
||||
});
|
||||
|
||||
await page.goto(snapshot.frames[0].url);
|
||||
await this._postprocessSnapshotFrame(snapshot, snapshot.frames[0], page.mainFrame());
|
||||
await Promise.all(intercepted);
|
||||
}
|
||||
|
||||
private async _postprocessSnapshotFrame(snapshot: PageSnapshot, frameSnapshot: FrameSnapshot, frame: Frame) {
|
||||
for (const childFrame of frame.childFrames()) {
|
||||
await childFrame.waitForLoadState();
|
||||
const url = childFrame.url();
|
||||
for (const childData of snapshot.frames) {
|
||||
if (url.endsWith(childData.url))
|
||||
await this._postprocessSnapshotFrame(snapshot, childData, childFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function showTraceViewer(playwright: Playwright, traceStorageDir: string, traceFiles: string[]) {
|
||||
const traceViewer = new TraceViewer(playwright, traceStorageDir);
|
||||
for (const traceFile of traceFiles)
|
||||
await traceViewer.load(traceFile);
|
||||
for (const browserName of traceViewer.browserNames())
|
||||
await traceViewer.show(browserName);
|
||||
}
|
||||
|
||||
function removeHash(url: string) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
u.hash = '';
|
||||
return u.toString();
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { BrowserContext } from '../server/browserContext';
|
||||
import type { SanpshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
|
||||
import type { SnapshotterResource as SnapshotterResource, SnapshotterBlob, SnapshotterDelegate } from './snapshotter';
|
||||
import { ContextCreatedTraceEvent, ContextDestroyedTraceEvent, NetworkResourceTraceEvent, ActionTraceEvent, PageCreatedTraceEvent, PageDestroyedTraceEvent } from './traceTypes';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
@ -118,10 +118,11 @@ class ContextTracer implements SnapshotterDelegate {
|
||||
this._writeArtifact(blob.sha1, blob.buffer);
|
||||
}
|
||||
|
||||
onResource(resource: SanpshotterResource): void {
|
||||
onResource(resource: SnapshotterResource): void {
|
||||
const event: NetworkResourceTraceEvent = {
|
||||
type: 'resource',
|
||||
contextId: this._contextId,
|
||||
pageId: resource.pageId,
|
||||
frameId: resource.frameId,
|
||||
url: resource.url,
|
||||
contentType: resource.contentType,
|
||||
@ -131,6 +132,10 @@ class ContextTracer implements SnapshotterDelegate {
|
||||
this._appendTraceEvent(event);
|
||||
}
|
||||
|
||||
pageId(page: Page): string {
|
||||
return this._pageToId.get(page)!;
|
||||
}
|
||||
|
||||
async captureSnapshot(page: Page, options: types.TimeoutOptions & { label?: string } = {}): Promise<void> {
|
||||
const snapshot = await this._takeSnapshot(page, options.timeout);
|
||||
if (!snapshot)
|
||||
|
@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const playwright = require('..');
|
||||
const { showTraceViewer } = require('../lib/trace/traceViewer');
|
||||
|
||||
if (process.argv.includes('--help')) {
|
||||
console.log(`Usage:`);
|
||||
console.log(` - npm run show-trace`);
|
||||
console.log(` Show traces from the last test run.`);
|
||||
console.log(` - npm run show-trace <test-results-directory>`);
|
||||
console.log(` Show traces from the downloaded test results.`);
|
||||
console.log(` - npm run show-trace <trace-file> <trace-storage-directory>`);
|
||||
console.log(` Show single trace file from the manual run.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let traceStorageDir, files;
|
||||
if (process.argv[3]) {
|
||||
files = [process.argv[2]];
|
||||
traceStorageDir = process.argv[3];
|
||||
} else {
|
||||
const testResultsDir = process.argv[2] || path.join(__dirname, '..', 'test-results');
|
||||
files = collectFiles(testResultsDir, '');
|
||||
traceStorageDir = path.join(testResultsDir, 'trace-storage');
|
||||
}
|
||||
console.log(`Found ${files.length} trace files`);
|
||||
showTraceViewer(playwright, traceStorageDir, files);
|
||||
|
||||
function collectFiles(dir) {
|
||||
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 (name.endsWith('.trace'))
|
||||
files.push(fullName);
|
||||
}
|
||||
return files;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user