mirror of
https://github.com/microsoft/playwright.git
synced 2024-11-29 01:53:54 +03:00
feat(trace-viewer): show nework request source id (#30810)
<img width="1392" alt="image" src="https://github.com/microsoft/playwright/assets/9798949/dcfd4d71-4a41-48ac-9f24-2996200f966a"> Fixes https://github.com/microsoft/playwright/issues/28903
This commit is contained in:
parent
89cdf3d56e
commit
2734a05342
@ -157,6 +157,8 @@ function indexModel(context: ContextEntry) {
|
||||
}
|
||||
for (const event of context.events)
|
||||
(event as any)[contextSymbol] = context;
|
||||
for (const resource of context.resources)
|
||||
(resource as any)[contextSymbol] = context;
|
||||
}
|
||||
|
||||
function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) {
|
||||
@ -330,7 +332,7 @@ export function idForAction(action: ActionTraceEvent) {
|
||||
return `${action.pageId || 'none'}:${action.callId}`;
|
||||
}
|
||||
|
||||
export function context(action: ActionTraceEvent | trace.EventTraceEvent): ContextEntry {
|
||||
export function context(action: ActionTraceEvent | trace.EventTraceEvent | ResourceSnapshot): ContextEntry {
|
||||
return (action as any)[contextSymbol];
|
||||
}
|
||||
|
||||
|
@ -21,12 +21,14 @@ import './networkTab.css';
|
||||
import { NetworkResourceDetails } from './networkResourceDetails';
|
||||
import { bytesToString, msToString } from '@web/uiUtils';
|
||||
import { PlaceholderPanel } from './placeholderPanel';
|
||||
import type { MultiTraceModel } from './modelUtil';
|
||||
import { context, type MultiTraceModel } from './modelUtil';
|
||||
import { GridView, type RenderedGridCell } from '@web/components/gridView';
|
||||
import { SplitView } from '@web/components/splitView';
|
||||
import type { ContextEntry } from '../entries';
|
||||
|
||||
type NetworkTabModel = {
|
||||
resources: Entry[],
|
||||
contextIdMap: ContextIdMap,
|
||||
};
|
||||
|
||||
type RenderedEntry = {
|
||||
@ -39,6 +41,7 @@ type RenderedEntry = {
|
||||
start: number,
|
||||
route: string,
|
||||
resource: Entry,
|
||||
contextId: string,
|
||||
};
|
||||
type ColumnName = keyof RenderedEntry;
|
||||
type Sorting = { by: ColumnName, negate: boolean};
|
||||
@ -54,7 +57,8 @@ export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedT
|
||||
});
|
||||
return filtered;
|
||||
}, [model, selectedTime]);
|
||||
return { resources };
|
||||
const contextIdMap = React.useMemo(() => new ContextIdMap(model), [model]);
|
||||
return { resources, contextIdMap };
|
||||
}
|
||||
|
||||
export const NetworkTab: React.FunctionComponent<{
|
||||
@ -66,11 +70,11 @@ export const NetworkTab: React.FunctionComponent<{
|
||||
const [selectedEntry, setSelectedEntry] = React.useState<RenderedEntry | undefined>(undefined);
|
||||
|
||||
const { renderedEntries } = React.useMemo(() => {
|
||||
const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries));
|
||||
const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries, networkModel.contextIdMap));
|
||||
if (sorting)
|
||||
sort(renderedEntries, sorting);
|
||||
return { renderedEntries };
|
||||
}, [networkModel.resources, sorting, boundaries]);
|
||||
}, [networkModel.resources, networkModel.contextIdMap, sorting, boundaries]);
|
||||
|
||||
if (!networkModel.resources.length)
|
||||
return <PlaceholderPanel text='No network calls' />;
|
||||
@ -81,7 +85,7 @@ export const NetworkTab: React.FunctionComponent<{
|
||||
selectedItem={selectedEntry}
|
||||
onSelected={item => setSelectedEntry(item)}
|
||||
onHighlighted={item => onEntryHovered(item?.resource)}
|
||||
columns={selectedEntry ? ['name'] : ['name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route']}
|
||||
columns={visibleColumns(!!selectedEntry, renderedEntries)}
|
||||
columnTitle={columnTitle}
|
||||
columnWidth={columnWidth}
|
||||
isError={item => item.status.code >= 400}
|
||||
@ -100,6 +104,8 @@ export const NetworkTab: React.FunctionComponent<{
|
||||
};
|
||||
|
||||
const columnTitle = (column: ColumnName) => {
|
||||
if (column === 'contextId')
|
||||
return 'Source';
|
||||
if (column === 'name')
|
||||
return 'Name';
|
||||
if (column === 'method')
|
||||
@ -128,10 +134,28 @@ const columnWidth = (column: ColumnName) => {
|
||||
return 60;
|
||||
if (column === 'contentType')
|
||||
return 200;
|
||||
if (column === 'contextId')
|
||||
return 60;
|
||||
return 100;
|
||||
};
|
||||
|
||||
function visibleColumns(entrySelected: boolean, renderedEntries: RenderedEntry[]): (keyof RenderedEntry)[] {
|
||||
if (entrySelected)
|
||||
return ['name'];
|
||||
const columns: (keyof RenderedEntry)[] = [];
|
||||
if (hasMultipleContexts(renderedEntries))
|
||||
columns.push('contextId');
|
||||
columns.push('name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route');
|
||||
return columns;
|
||||
}
|
||||
|
||||
const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell => {
|
||||
if (column === 'contextId') {
|
||||
return {
|
||||
body: entry.contextId,
|
||||
title: entry.name.url,
|
||||
};
|
||||
}
|
||||
if (column === 'name') {
|
||||
return {
|
||||
body: entry.name.name,
|
||||
@ -159,7 +183,57 @@ const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell
|
||||
return { body: '' };
|
||||
};
|
||||
|
||||
const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry => {
|
||||
class ContextIdMap {
|
||||
private _pagerefToShortId = new Map<string, string>();
|
||||
private _contextToId = new Map<ContextEntry, string>();
|
||||
private _lastPageId = 0;
|
||||
private _lastApiRequestContextId = 0;
|
||||
|
||||
constructor(model: MultiTraceModel | undefined) {}
|
||||
|
||||
contextId(resource: Entry): string {
|
||||
if (resource.pageref)
|
||||
return this._pageId(resource.pageref);
|
||||
else if (resource._apiRequest)
|
||||
return this._apiRequestContextId(resource);
|
||||
return '';
|
||||
}
|
||||
|
||||
private _pageId(pageref: string): string {
|
||||
let shortId = this._pagerefToShortId.get(pageref);
|
||||
if (!shortId) {
|
||||
++this._lastPageId;
|
||||
shortId = 'page#' + this._lastPageId;
|
||||
this._pagerefToShortId.set(pageref, shortId);
|
||||
}
|
||||
return shortId;
|
||||
}
|
||||
|
||||
private _apiRequestContextId(resource: Entry): string {
|
||||
const contextEntry = context(resource);
|
||||
if (!contextEntry)
|
||||
return '';
|
||||
let contextId = this._contextToId.get(contextEntry);
|
||||
if (!contextId) {
|
||||
++this._lastApiRequestContextId;
|
||||
contextId = 'api#' + this._lastApiRequestContextId;
|
||||
this._contextToId.set(contextEntry, contextId);
|
||||
}
|
||||
return contextId;
|
||||
}
|
||||
}
|
||||
|
||||
function hasMultipleContexts(renderedEntries: RenderedEntry[]): boolean {
|
||||
const contextIds = new Set<string>();
|
||||
for (const entry of renderedEntries) {
|
||||
contextIds.add(entry.contextId);
|
||||
if (contextIds.size > 1)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const renderEntry = (resource: Entry, boundaries: Boundaries, contextIdGenerator: ContextIdMap): RenderedEntry => {
|
||||
const routeStatus = formatRouteStatus(resource);
|
||||
let resourceName: string;
|
||||
try {
|
||||
@ -184,7 +258,8 @@ const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry =>
|
||||
size: resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize,
|
||||
start: resource._monotonicTime! - boundaries.minimum,
|
||||
route: routeStatus,
|
||||
resource
|
||||
resource,
|
||||
contextId: contextIdGenerator.contextId(resource),
|
||||
};
|
||||
};
|
||||
|
||||
@ -249,4 +324,7 @@ function comparator(sortBy: ColumnName) {
|
||||
return a.route.localeCompare(b.route);
|
||||
};
|
||||
}
|
||||
|
||||
if (sortBy === 'contextId')
|
||||
return (a: RenderedEntry, b: RenderedEntry) => a.contextId.localeCompare(b.contextId);
|
||||
}
|
||||
|
@ -285,3 +285,24 @@ test('should reveal errors in the sourcetab', async ({ runUITest }) => {
|
||||
await page.getByText('a.spec.ts:4', { exact: true }).click();
|
||||
await expect(page.locator('.source-line-running')).toContainText(`throw new Error('Oh my');`);
|
||||
});
|
||||
|
||||
test('should show request source context id', async ({ runUITest, server }) => {
|
||||
const { page } = await runUITest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('pass', async ({ page, context, request }) => {
|
||||
await page.goto('${server.EMPTY_PAGE}');
|
||||
const page2 = await context.newPage();
|
||||
await page2.goto('${server.EMPTY_PAGE}');
|
||||
await request.get('${server.EMPTY_PAGE}');
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
await page.getByText('pass').dblclick();
|
||||
await page.getByText('Network', { exact: true }).click();
|
||||
await expect(page.locator('span').filter({ hasText: 'Source' })).toBeVisible();
|
||||
await expect(page.getByText('page#1')).toBeVisible();
|
||||
await expect(page.getByText('page#2')).toBeVisible();
|
||||
await expect(page.getByText('api#1')).toBeVisible();
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user