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.
|
// 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 () => {
|
return (async () => {
|
||||||
try {
|
try {
|
||||||
const expectZone: ExpectZone = { title: defaultTitle, wallTime };
|
const expectZone: ExpectZone = { title: defaultTitle, wallTime };
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import { asLocator } from '@isomorphic/locatorGenerators';
|
||||||
import type { Language } from '@isomorphic/locatorGenerators';
|
import type { Language } from '@isomorphic/locatorGenerators';
|
||||||
import type { TreeState } from '@web/components/treeView';
|
import type { TreeState } from '@web/components/treeView';
|
||||||
import { TreeView } from '@web/components/treeView';
|
import { TreeView } from '@web/components/treeView';
|
||||||
import type { ActionTraceEventInContext } from './modelUtil';
|
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
|
||||||
|
|
||||||
export interface ActionListProps {
|
export interface ActionListProps {
|
||||||
actions: ActionTraceEventInContext[],
|
actions: ActionTraceEventInContext[],
|
||||||
|
|
@ -35,13 +35,6 @@ export interface ActionListProps {
|
||||||
isLive?: boolean,
|
isLive?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionTreeItem = {
|
|
||||||
id: string;
|
|
||||||
children: ActionTreeItem[];
|
|
||||||
parent: ActionTreeItem | undefined;
|
|
||||||
action?: ActionTraceEventInContext;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ActionTreeView = TreeView<ActionTreeItem>;
|
const ActionTreeView = TreeView<ActionTreeItem>;
|
||||||
|
|
||||||
export const ActionList: React.FC<ActionListProps> = ({
|
export const ActionList: React.FC<ActionListProps> = ({
|
||||||
|
|
@ -54,26 +47,7 @@ export const ActionList: React.FC<ActionListProps> = ({
|
||||||
isLive,
|
isLive,
|
||||||
}) => {
|
}) => {
|
||||||
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
||||||
const { rootItem, itemMap } = React.useMemo(() => {
|
const { rootItem, itemMap } = React.useMemo(() => modelUtil.buildActionTree(actions), [actions]);
|
||||||
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 { selectedItem } = React.useMemo(() => {
|
const { selectedItem } = React.useMemo(() => {
|
||||||
const selectedItem = selectedAction ? itemMap.get(selectedAction.callId) : undefined;
|
const selectedItem = selectedAction ? itemMap.get(selectedAction.callId) : undefined;
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,13 @@ export type ActionTraceEventInContext = ActionTraceEvent & {
|
||||||
context: ContextEntry;
|
context: ContextEntry;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActionTreeItem = {
|
||||||
|
id: string;
|
||||||
|
children: ActionTreeItem[];
|
||||||
|
parent: ActionTreeItem | undefined;
|
||||||
|
action?: ActionTraceEventInContext;
|
||||||
|
};
|
||||||
|
|
||||||
export class MultiTraceModel {
|
export class MultiTraceModel {
|
||||||
readonly startTime: number;
|
readonly startTime: number;
|
||||||
readonly endTime: number;
|
readonly endTime: number;
|
||||||
|
|
@ -159,6 +166,27 @@ function mergeActions(contexts: ContextEntry[]) {
|
||||||
return result;
|
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) {
|
export function idForAction(action: ActionTraceEvent) {
|
||||||
return `${action.pageId || 'none'}:${action.callId}`;
|
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 type { StackFrame } from '../../packages/protocol/src/channels';
|
||||||
import { parseClientSideCallMetadata } from '../../packages/playwright-core/lib/utils/isomorphic/traceUtils';
|
import { parseClientSideCallMetadata } from '../../packages/playwright-core/lib/utils/isomorphic/traceUtils';
|
||||||
import { TraceModel } from '../../packages/trace-viewer/src/traceModel';
|
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';
|
import type { ActionTraceEvent, EventTraceEvent, TraceEvent } from '@trace/trace';
|
||||||
|
|
||||||
export async function attachFrame(page: Page, frameId: string, url: string): Promise<Frame> {
|
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);
|
.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 backend = new TraceBackend(file);
|
||||||
const traceModel = new TraceModel();
|
const traceModel = new TraceModel();
|
||||||
await traceModel.load(backend, () => {});
|
await traceModel.load(backend, () => {});
|
||||||
const model = new MultiTraceModel(traceModel.contextEntries);
|
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 {
|
return {
|
||||||
apiNames: model.actions.map(a => a.apiName),
|
apiNames: model.actions.map(a => a.apiName),
|
||||||
resources: backend.entries,
|
resources: backend.entries,
|
||||||
|
|
@ -177,6 +186,7 @@ export async function parseTrace(file: string): Promise<{ resources: Map<string,
|
||||||
events: model.events,
|
events: model.events,
|
||||||
model,
|
model,
|
||||||
traceModel,
|
traceModel,
|
||||||
|
actionTree,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,34 +144,34 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline
|
||||||
expect(result.passed).toBe(2);
|
expect(result.passed).toBe(2);
|
||||||
|
|
||||||
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'reuse-one', 'trace.zip'));
|
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'reuse-one', 'trace.zip'));
|
||||||
expect(trace1.apiNames).toEqual([
|
expect(trace1.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
'fixture: browser',
|
' fixture: browser',
|
||||||
'browserType.launch',
|
' browserType.launch',
|
||||||
'fixture: context',
|
' fixture: context',
|
||||||
'fixture: page',
|
' fixture: page',
|
||||||
'browserContext.newPage',
|
' browserContext.newPage',
|
||||||
'page.setContent',
|
'page.setContent',
|
||||||
'page.click',
|
'page.click',
|
||||||
'After Hooks',
|
'After Hooks',
|
||||||
'fixture: page',
|
' fixture: page',
|
||||||
'fixture: context',
|
' fixture: context',
|
||||||
]);
|
]);
|
||||||
expect(trace1.traceModel.storage().snapshotsForTest().length).toBeGreaterThan(0);
|
expect(trace1.traceModel.storage().snapshotsForTest().length).toBeGreaterThan(0);
|
||||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-one', 'trace-1.zip'))).toBe(false);
|
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'));
|
const trace2 = await parseTrace(testInfo.outputPath('test-results', 'reuse-two', 'trace.zip'));
|
||||||
expect(trace2.apiNames).toEqual([
|
expect(trace2.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
'fixture: context',
|
' fixture: context',
|
||||||
'fixture: page',
|
' fixture: page',
|
||||||
'expect.toBe',
|
'expect.toBe',
|
||||||
'page.setContent',
|
'page.setContent',
|
||||||
'page.fill',
|
'page.fill',
|
||||||
'locator.click',
|
'locator.click',
|
||||||
'After Hooks',
|
'After Hooks',
|
||||||
'fixture: page',
|
' fixture: page',
|
||||||
'fixture: context',
|
' fixture: context',
|
||||||
]);
|
]);
|
||||||
expect(trace2.traceModel.storage().snapshotsForTest().length).toBeGreaterThan(0);
|
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);
|
expect(result.failed).toBe(1);
|
||||||
// One trace file for request context and one for each APIRequestContext
|
// One trace file for request context and one for each APIRequestContext
|
||||||
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'));
|
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'));
|
||||||
expect(trace1.apiNames).toEqual([
|
expect(trace1.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
'fixture: request',
|
' fixture: request',
|
||||||
'apiRequest.newContext',
|
' apiRequest.newContext',
|
||||||
'tracing.start',
|
' tracing.start',
|
||||||
'fixture: browser',
|
' fixture: browser',
|
||||||
'browserType.launch',
|
' browserType.launch',
|
||||||
'fixture: context',
|
' fixture: context',
|
||||||
'browser.newContext',
|
' browser.newContext',
|
||||||
'tracing.start',
|
' tracing.start',
|
||||||
'fixture: page',
|
' fixture: page',
|
||||||
'browserContext.newPage',
|
' browserContext.newPage',
|
||||||
'page.goto',
|
'page.goto',
|
||||||
'apiRequestContext.get',
|
'apiRequestContext.get',
|
||||||
'After Hooks',
|
'After Hooks',
|
||||||
'fixture: page',
|
' fixture: page',
|
||||||
'fixture: context',
|
' fixture: context',
|
||||||
'fixture: request',
|
' fixture: request',
|
||||||
'tracing.stopChunk',
|
' tracing.stopChunk',
|
||||||
'apiRequestContext.dispose',
|
' apiRequestContext.dispose',
|
||||||
]);
|
]);
|
||||||
const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-api-pass', 'trace.zip'));
|
const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-api-pass', 'trace.zip'));
|
||||||
expect(trace2.apiNames).toEqual([
|
expect(trace2.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
'apiRequest.newContext',
|
'apiRequest.newContext',
|
||||||
'tracing.start',
|
'tracing.start',
|
||||||
|
|
@ -117,25 +117,25 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
|
||||||
'After Hooks',
|
'After Hooks',
|
||||||
]);
|
]);
|
||||||
const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip'));
|
const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip'));
|
||||||
expect(trace3.apiNames).toEqual([
|
expect(trace3.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
'fixture: request',
|
' fixture: request',
|
||||||
'apiRequest.newContext',
|
' apiRequest.newContext',
|
||||||
'tracing.start',
|
' tracing.start',
|
||||||
'fixture: context',
|
' fixture: context',
|
||||||
'browser.newContext',
|
' browser.newContext',
|
||||||
'tracing.start',
|
' tracing.start',
|
||||||
'fixture: page',
|
' fixture: page',
|
||||||
'browserContext.newPage',
|
' browserContext.newPage',
|
||||||
'page.goto',
|
'page.goto',
|
||||||
'apiRequestContext.get',
|
'apiRequestContext.get',
|
||||||
'expect.toBe',
|
'expect.toBe',
|
||||||
'After Hooks',
|
'After Hooks',
|
||||||
'fixture: page',
|
' fixture: page',
|
||||||
'fixture: context',
|
' fixture: context',
|
||||||
'fixture: request',
|
' fixture: request',
|
||||||
'tracing.stopChunk',
|
' tracing.stopChunk',
|
||||||
'apiRequestContext.dispose',
|
' apiRequestContext.dispose',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -321,28 +321,28 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip'));
|
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip'));
|
||||||
|
|
||||||
expect(trace1.apiNames).toEqual([
|
expect(trace1.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
'fixture: browser',
|
' fixture: browser',
|
||||||
'browserType.launch',
|
' browserType.launch',
|
||||||
'fixture: context',
|
' fixture: context',
|
||||||
'browser.newContext',
|
' browser.newContext',
|
||||||
'tracing.start',
|
' tracing.start',
|
||||||
'fixture: page',
|
' fixture: page',
|
||||||
'browserContext.newPage',
|
' browserContext.newPage',
|
||||||
'page.goto',
|
'page.goto',
|
||||||
'After Hooks',
|
'After Hooks',
|
||||||
'fixture: page',
|
' fixture: page',
|
||||||
'fixture: context',
|
' fixture: context',
|
||||||
'attach \"trace\"',
|
' attach \"trace\"',
|
||||||
'afterAll hook',
|
' afterAll hook',
|
||||||
'fixture: request',
|
' fixture: request',
|
||||||
'apiRequest.newContext',
|
' apiRequest.newContext',
|
||||||
'tracing.start',
|
' tracing.start',
|
||||||
'apiRequestContext.get',
|
' apiRequestContext.get',
|
||||||
'fixture: request',
|
' fixture: request',
|
||||||
'tracing.stopChunk',
|
' tracing.stopChunk',
|
||||||
'apiRequestContext.dispose',
|
' apiRequestContext.dispose',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const error = await parseTrace(testInfo.outputPath('test-results', 'a-test-2', 'trace.zip')).catch(e => e);
|
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',
|
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…
Reference in a new issue