fix(trace): render items under expect.toPass (#24016)

Fixes: https://github.com/microsoft/playwright/issues/23942
This commit is contained in:
Pavel Feldman 2023-07-05 11:20:28 -07:00 committed by GitHub
parent 9f1f737acb
commit df57fb594c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 148 additions and 94 deletions

View File

@ -296,7 +296,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
};
// Process the async matchers separately to preserve the zones in the stacks.
if (this._info.isPoll || matcherName in customAsyncMatchers) {
if (this._info.isPoll || (matcherName in customAsyncMatchers && matcherName !== 'toPass')) {
return (async () => {
try {
const expectZone: ExpectZone = { title: defaultTitle, wallTime };

View File

@ -23,7 +23,7 @@ import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators';
import type { TreeState } from '@web/components/treeView';
import { TreeView } from '@web/components/treeView';
import type { ActionTraceEventInContext } from './modelUtil';
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
export interface ActionListProps {
actions: ActionTraceEventInContext[],
@ -35,13 +35,6 @@ export interface ActionListProps {
isLive?: boolean,
}
type ActionTreeItem = {
id: string;
children: ActionTreeItem[];
parent: ActionTreeItem | undefined;
action?: ActionTraceEventInContext;
};
const ActionTreeView = TreeView<ActionTreeItem>;
export const ActionList: React.FC<ActionListProps> = ({
@ -54,26 +47,7 @@ export const ActionList: React.FC<ActionListProps> = ({
isLive,
}) => {
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
const { rootItem, itemMap } = React.useMemo(() => {
const itemMap = new Map<string, ActionTreeItem>();
for (const action of actions) {
itemMap.set(action.callId, {
id: action.callId,
parent: undefined,
children: [],
action,
});
}
const rootItem: ActionTreeItem = { id: '', parent: undefined, children: [] };
for (const item of itemMap.values()) {
const parent = item.action!.parentId ? itemMap.get(item.action!.parentId) || rootItem : rootItem;
parent.children.push(item);
item.parent = parent;
}
return { rootItem, itemMap };
}, [actions]);
const { rootItem, itemMap } = React.useMemo(() => modelUtil.buildActionTree(actions), [actions]);
const { selectedItem } = React.useMemo(() => {
const selectedItem = selectedAction ? itemMap.get(selectedAction.callId) : undefined;

View File

@ -41,6 +41,13 @@ export type ActionTraceEventInContext = ActionTraceEvent & {
context: ContextEntry;
};
export type ActionTreeItem = {
id: string;
children: ActionTreeItem[];
parent: ActionTreeItem | undefined;
action?: ActionTraceEventInContext;
};
export class MultiTraceModel {
readonly startTime: number;
readonly endTime: number;
@ -159,6 +166,27 @@ function mergeActions(contexts: ContextEntry[]) {
return result;
}
export function buildActionTree(actions: ActionTraceEventInContext[]): { rootItem: ActionTreeItem, itemMap: Map<string, ActionTreeItem> } {
const itemMap = new Map<string, ActionTreeItem>();
for (const action of actions) {
itemMap.set(action.callId, {
id: action.callId,
parent: undefined,
children: [],
action,
});
}
const rootItem: ActionTreeItem = { id: '', parent: undefined, children: [] };
for (const item of itemMap.values()) {
const parent = item.action!.parentId ? itemMap.get(item.action!.parentId) || rootItem : rootItem;
parent.children.push(item);
item.parent = parent;
}
return { rootItem, itemMap };
}
export function idForAction(action: ActionTraceEvent) {
return `${action.pageId || 'none'}:${action.callId}`;
}

View File

@ -20,7 +20,8 @@ import type { TraceModelBackend } from '../../packages/trace-viewer/src/traceMod
import type { StackFrame } from '../../packages/protocol/src/channels';
import { parseClientSideCallMetadata } from '../../packages/playwright-core/lib/utils/isomorphic/traceUtils';
import { TraceModel } from '../../packages/trace-viewer/src/traceModel';
import { MultiTraceModel } from '../../packages/trace-viewer/src/ui/modelUtil';
import type { ActionTreeItem } from '../../packages/trace-viewer/src/ui/modelUtil';
import { buildActionTree, MultiTraceModel } from '../../packages/trace-viewer/src/ui/modelUtil';
import type { ActionTraceEvent, EventTraceEvent, TraceEvent } from '@trace/trace';
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
@ -165,11 +166,19 @@ function eventsToActions(events: ActionTraceEvent[]): string[] {
.map(e => e.apiName);
}
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: EventTraceEvent[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel }> {
export async function parseTrace(file: string): Promise<{ resources: Map<string, Buffer>, events: EventTraceEvent[], actions: ActionTraceEvent[], apiNames: string[], traceModel: TraceModel, model: MultiTraceModel, actionTree: string[] }> {
const backend = new TraceBackend(file);
const traceModel = new TraceModel();
await traceModel.load(backend, () => {});
const model = new MultiTraceModel(traceModel.contextEntries);
const { rootItem } = buildActionTree(model.actions);
const actionTree: string[] = [];
const visit = (actionItem: ActionTreeItem, indent: string) => {
actionTree.push(`${indent}${actionItem.action?.apiName || actionItem.id}`);
for (const child of actionItem.children)
visit(child, indent + ' ');
};
rootItem.children.forEach(a => visit(a, ''));
return {
apiNames: model.actions.map(a => a.apiName),
resources: backend.entries,
@ -177,6 +186,7 @@ export async function parseTrace(file: string): Promise<{ resources: Map<string,
events: model.events,
model,
traceModel,
actionTree,
};
}

View File

@ -144,34 +144,34 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline
expect(result.passed).toBe(2);
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'reuse-one', 'trace.zip'));
expect(trace1.apiNames).toEqual([
expect(trace1.actionTree).toEqual([
'Before Hooks',
'fixture: browser',
'browserType.launch',
'fixture: context',
'fixture: page',
'browserContext.newPage',
' fixture: browser',
' browserType.launch',
' fixture: context',
' fixture: page',
' browserContext.newPage',
'page.setContent',
'page.click',
'After Hooks',
'fixture: page',
'fixture: context',
' fixture: page',
' fixture: context',
]);
expect(trace1.traceModel.storage().snapshotsForTest().length).toBeGreaterThan(0);
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-one', 'trace-1.zip'))).toBe(false);
const trace2 = await parseTrace(testInfo.outputPath('test-results', 'reuse-two', 'trace.zip'));
expect(trace2.apiNames).toEqual([
expect(trace2.actionTree).toEqual([
'Before Hooks',
'fixture: context',
'fixture: page',
' fixture: context',
' fixture: page',
'expect.toBe',
'page.setContent',
'page.fill',
'locator.click',
'After Hooks',
'fixture: page',
'fixture: context',
' fixture: page',
' fixture: context',
]);
expect(trace2.traceModel.storage().snapshotsForTest().length).toBeGreaterThan(0);
});

View File

@ -87,29 +87,29 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
expect(result.failed).toBe(1);
// One trace file for request context and one for each APIRequestContext
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'));
expect(trace1.apiNames).toEqual([
expect(trace1.actionTree).toEqual([
'Before Hooks',
'fixture: request',
'apiRequest.newContext',
'tracing.start',
'fixture: browser',
'browserType.launch',
'fixture: context',
'browser.newContext',
'tracing.start',
'fixture: page',
'browserContext.newPage',
' fixture: request',
' apiRequest.newContext',
' tracing.start',
' fixture: browser',
' browserType.launch',
' fixture: context',
' browser.newContext',
' tracing.start',
' fixture: page',
' browserContext.newPage',
'page.goto',
'apiRequestContext.get',
'After Hooks',
'fixture: page',
'fixture: context',
'fixture: request',
'tracing.stopChunk',
'apiRequestContext.dispose',
' fixture: page',
' fixture: context',
' fixture: request',
' tracing.stopChunk',
' apiRequestContext.dispose',
]);
const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-api-pass', 'trace.zip'));
expect(trace2.apiNames).toEqual([
expect(trace2.actionTree).toEqual([
'Before Hooks',
'apiRequest.newContext',
'tracing.start',
@ -117,25 +117,25 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
'After Hooks',
]);
const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip'));
expect(trace3.apiNames).toEqual([
expect(trace3.actionTree).toEqual([
'Before Hooks',
'fixture: request',
'apiRequest.newContext',
'tracing.start',
'fixture: context',
'browser.newContext',
'tracing.start',
'fixture: page',
'browserContext.newPage',
' fixture: request',
' apiRequest.newContext',
' tracing.start',
' fixture: context',
' browser.newContext',
' tracing.start',
' fixture: page',
' browserContext.newPage',
'page.goto',
'apiRequestContext.get',
'expect.toBe',
'After Hooks',
'fixture: page',
'fixture: context',
'fixture: request',
'tracing.stopChunk',
'apiRequestContext.dispose',
' fixture: page',
' fixture: context',
' fixture: request',
' tracing.stopChunk',
' apiRequestContext.dispose',
]);
});
@ -321,28 +321,28 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve
expect(result.failed).toBe(1);
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip'));
expect(trace1.apiNames).toEqual([
expect(trace1.actionTree).toEqual([
'Before Hooks',
'fixture: browser',
'browserType.launch',
'fixture: context',
'browser.newContext',
'tracing.start',
'fixture: page',
'browserContext.newPage',
' fixture: browser',
' browserType.launch',
' fixture: context',
' browser.newContext',
' tracing.start',
' fixture: page',
' browserContext.newPage',
'page.goto',
'After Hooks',
'fixture: page',
'fixture: context',
'attach \"trace\"',
'afterAll hook',
'fixture: request',
'apiRequest.newContext',
'tracing.start',
'apiRequestContext.get',
'fixture: request',
'tracing.stopChunk',
'apiRequestContext.dispose',
' fixture: page',
' fixture: context',
' attach \"trace\"',
' afterAll hook',
' fixture: request',
' apiRequest.newContext',
' tracing.start',
' apiRequestContext.get',
' fixture: request',
' tracing.stopChunk',
' apiRequestContext.dispose',
]);
const error = await parseTrace(testInfo.outputPath('test-results', 'a-test-2', 'trace.zip')).catch(e => e);
@ -608,3 +608,45 @@ test('should record with custom page fixture', async ({ runInlineTest }, testInf
type: 'frame-snapshot',
}));
});
test('should expand expect.toPass', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = { use: { trace: { mode: 'on' } } };
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('pass', async ({ page }) => {
let i = 0;
await expect(async () => {
await page.goto('data:text/html,Hello world');
expect(i++).toBe(2);
}).toPass();
});
`,
}, { workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'));
expect(trace.actionTree).toEqual([
'Before Hooks',
' fixture: browser',
' browserType.launch',
' fixture: context',
' browser.newContext',
' tracing.start',
' fixture: page',
' browserContext.newPage',
'expect.toPass',
' page.goto',
' expect.toBe',
' page.goto',
' expect.toBe',
' page.goto',
' expect.toBe',
'After Hooks',
' fixture: page',
' fixture: context',
]);
});