mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix(trace): render items under expect.toPass (#24016)
Fixes: https://github.com/microsoft/playwright/issues/23942
This commit is contained in:
parent
9f1f737acb
commit
df57fb594c
@ -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 };
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}`;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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',
|
||||
]);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user