mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	chore: show snapshot for test.step (#35445)
We don't take before/after snapshot for `test.step`. To approximate the snapshots we could take either snapshots from the nested actions or from the outer ones. The current logic is the following: **beforeSnapshot:** - `beforeSnapshot` is always taken from the last finished action before the step. It also works nice for the actions without nested actions, such as simple `expect(1).toBe(1);` **afterSnapshot:** - We always use `afterSnapshot` from a "nested" action, if there is one. It is exactly what we want for `test.step` and it is acceptable for other actions. - If there are no "nested" actions, use the `beforeSnapshot` - works best for simple `expect(a).toBe(b);` case - `test.step` without children with snapshot is likely a step with a bunch of `expect(a).toBe(b);` and the same logic as for single expect applies. Fixes https://github.com/microsoft/playwright/issues/35285
This commit is contained in:
		
							parent
							
								
									b92e81c205
								
							
						
					
					
						commit
						6c5f3bbe39
					
				@ -24,8 +24,9 @@ import type { ActionEntry, ContextEntry, PageEntry } from '../types/entries';
 | 
				
			|||||||
import type { StackFrame } from '@protocol/channels';
 | 
					import type { StackFrame } from '@protocol/channels';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const contextSymbol = Symbol('context');
 | 
					const contextSymbol = Symbol('context');
 | 
				
			||||||
const nextInContextSymbol = Symbol('next');
 | 
					const nextInContextSymbol = Symbol('nextInContext');
 | 
				
			||||||
const prevInListSymbol = Symbol('prev');
 | 
					const prevByEndTimeSymbol = Symbol('prevByEndTime');
 | 
				
			||||||
 | 
					const nextByStartTimeSymbol = Symbol('nextByStartTime');
 | 
				
			||||||
const eventsSymbol = Symbol('events');
 | 
					const eventsSymbol = Symbol('events');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type SourceLocation = {
 | 
					export type SourceLocation = {
 | 
				
			||||||
@ -190,6 +191,18 @@ function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) {
 | 
				
			|||||||
    const actions = mergeActionsAndUpdateTimingSameTrace(contexts);
 | 
					    const actions = mergeActionsAndUpdateTimingSameTrace(contexts);
 | 
				
			||||||
    result.push(...actions);
 | 
					    result.push(...actions);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  result.sort((a1, a2) => {
 | 
				
			||||||
 | 
					    if (a2.parentId === a1.callId)
 | 
				
			||||||
 | 
					      return 1;
 | 
				
			||||||
 | 
					    if (a1.parentId === a2.callId)
 | 
				
			||||||
 | 
					      return -1;
 | 
				
			||||||
 | 
					    return a1.endTime - a2.endTime;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (let i = 1; i < result.length; ++i)
 | 
				
			||||||
 | 
					    (result[i] as any)[prevByEndTimeSymbol] = result[i - 1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  result.sort((a1, a2) => {
 | 
					  result.sort((a1, a2) => {
 | 
				
			||||||
    if (a2.parentId === a1.callId)
 | 
					    if (a2.parentId === a1.callId)
 | 
				
			||||||
      return -1;
 | 
					      return -1;
 | 
				
			||||||
@ -198,8 +211,8 @@ function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) {
 | 
				
			|||||||
    return a1.startTime - a2.startTime;
 | 
					    return a1.startTime - a2.startTime;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for (let i = 1; i < result.length; ++i)
 | 
					  for (let i = 0; i + 1 < result.length; ++i)
 | 
				
			||||||
    (result[i] as any)[prevInListSymbol] = result[i - 1];
 | 
					    (result[i] as any)[nextByStartTimeSymbol] = result[i + 1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return result;
 | 
					  return result;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -355,8 +368,12 @@ function nextInContext(action: ActionTraceEvent): ActionTraceEvent {
 | 
				
			|||||||
  return (action as any)[nextInContextSymbol];
 | 
					  return (action as any)[nextInContextSymbol];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function prevInList(action: ActionTraceEvent): ActionTraceEvent {
 | 
					export function previousActionByEndTime(action: ActionTraceEvent): ActionTraceEvent {
 | 
				
			||||||
  return (action as any)[prevInListSymbol];
 | 
					  return (action as any)[prevByEndTimeSymbol];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function nextActionByStartTime(action: ActionTraceEvent): ActionTraceEvent {
 | 
				
			||||||
 | 
					  return (action as any)[nextByStartTimeSymbol];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function stats(action: ActionTraceEvent): { errors: number, warnings: number } {
 | 
					export function stats(action: ActionTraceEvent): { errors: number, warnings: number } {
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,7 @@
 | 
				
			|||||||
import './snapshotTab.css';
 | 
					import './snapshotTab.css';
 | 
				
			||||||
import * as React from 'react';
 | 
					import * as React from 'react';
 | 
				
			||||||
import type { ActionTraceEvent } from '@trace/trace';
 | 
					import type { ActionTraceEvent } from '@trace/trace';
 | 
				
			||||||
import { context, type MultiTraceModel, prevInList } from './modelUtil';
 | 
					import { context, type MultiTraceModel, nextActionByStartTime, previousActionByEndTime } from './modelUtil';
 | 
				
			||||||
import { Toolbar } from '@web/components/toolbar';
 | 
					import { Toolbar } from '@web/components/toolbar';
 | 
				
			||||||
import { ToolbarButton } from '@web/components/toolbarButton';
 | 
					import { ToolbarButton } from '@web/components/toolbarButton';
 | 
				
			||||||
import { clsx, useMeasure, useSetting } from '@web/uiUtils';
 | 
					import { clsx, useMeasure, useSetting } from '@web/uiUtils';
 | 
				
			||||||
@ -329,14 +329,40 @@ export function collectSnapshots(action: ActionTraceEvent | undefined): Snapshot
 | 
				
			|||||||
  if (!action)
 | 
					  if (!action)
 | 
				
			||||||
    return {};
 | 
					    return {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // if the action has no beforeSnapshot, use the last available afterSnapshot.
 | 
					 | 
				
			||||||
  let beforeSnapshot: Snapshot | undefined = action.beforeSnapshot ? { action, snapshotName: action.beforeSnapshot } : undefined;
 | 
					  let beforeSnapshot: Snapshot | undefined = action.beforeSnapshot ? { action, snapshotName: action.beforeSnapshot } : undefined;
 | 
				
			||||||
  let a = action;
 | 
					  if (!beforeSnapshot) {
 | 
				
			||||||
  while (!beforeSnapshot && a) {
 | 
					    // If the action has no beforeSnapshot, use the last available afterSnapshot.
 | 
				
			||||||
    a = prevInList(a);
 | 
					    for (let a = previousActionByEndTime(action); a; a = previousActionByEndTime(a)) {
 | 
				
			||||||
    beforeSnapshot = a?.afterSnapshot ? { action: a, snapshotName: a?.afterSnapshot } : undefined;
 | 
					      if (a.endTime <= action.startTime && a.afterSnapshot) {
 | 
				
			||||||
 | 
					        beforeSnapshot = { action: a, snapshotName: a.afterSnapshot };
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
  const afterSnapshot: Snapshot | undefined = action.afterSnapshot ? { action, snapshotName: action.afterSnapshot } : beforeSnapshot;
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let afterSnapshot: Snapshot | undefined = action.afterSnapshot ? { action, snapshotName: action.afterSnapshot } : undefined;
 | 
				
			||||||
 | 
					  if (!afterSnapshot) {
 | 
				
			||||||
 | 
					    let last: ActionTraceEvent | undefined;
 | 
				
			||||||
 | 
					    // - For test.step, we want to use the snapshot of the last nested action.
 | 
				
			||||||
 | 
					    // - For a regular action, we use snapshot of any overlapping in time action
 | 
				
			||||||
 | 
					    //   as a best effort.
 | 
				
			||||||
 | 
					    // - If there are no "nested" actions, use the beforeSnapshot which works best
 | 
				
			||||||
 | 
					    //   for simple `expect(a).toBe(b);` case. Also if the action doesn't have
 | 
				
			||||||
 | 
					    //   afterSnapshot, it likely doesn't have its own beforeSnapshot either,
 | 
				
			||||||
 | 
					    //   and we calculated it above from a previous action.
 | 
				
			||||||
 | 
					    for (let a = nextActionByStartTime(action); a && a.startTime <= action.endTime; a = nextActionByStartTime(a)) {
 | 
				
			||||||
 | 
					      if (a.endTime > action.endTime || !a.afterSnapshot)
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      if (last && last.endTime > a.endTime)
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      last = a;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (last)
 | 
				
			||||||
 | 
					      afterSnapshot = { action: last, snapshotName: last.afterSnapshot! };
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					      afterSnapshot = beforeSnapshot;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const actionSnapshot: Snapshot | undefined = action.inputSnapshot ? { action, snapshotName: action.inputSnapshot, hasInputTarget: true } : afterSnapshot;
 | 
					  const actionSnapshot: Snapshot | undefined = action.inputSnapshot ? { action, snapshotName: action.inputSnapshot, hasInputTarget: true } : afterSnapshot;
 | 
				
			||||||
  if (actionSnapshot)
 | 
					  if (actionSnapshot)
 | 
				
			||||||
    actionSnapshot.point = action.point;
 | 
					    actionSnapshot.point = action.point;
 | 
				
			||||||
 | 
				
			|||||||
@ -150,6 +150,61 @@ test('should show snapshots for sync assertions', async ({ runUITest }) => {
 | 
				
			|||||||
  ).toHaveText('Submit');
 | 
					  ).toHaveText('Submit');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('should show snapshots for steps', {
 | 
				
			||||||
 | 
					  annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/35285' }
 | 
				
			||||||
 | 
					}, async ({ runUITest }) => {
 | 
				
			||||||
 | 
					  const { page } = await runUITest({
 | 
				
			||||||
 | 
					    'a.test.ts': `
 | 
				
			||||||
 | 
					      import { test, expect } from '@playwright/test';
 | 
				
			||||||
 | 
					      test.beforeEach(async ({ page }) => {
 | 
				
			||||||
 | 
					        await page.setContent('<div>initial</div>');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      test('steps test', async ({ page }) => {
 | 
				
			||||||
 | 
					        await test.step('first', async () => {
 | 
				
			||||||
 | 
					          await page.setContent("<div>foo</div>");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await test.step('middle', async () => {
 | 
				
			||||||
 | 
					          await page.setContent("<div>bar</div>");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        await test.step('last', async () => {
 | 
				
			||||||
 | 
					          await page.setContent("<div>baz</div>");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    `,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await page.getByText('steps test').dblclick();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await expect(page.getByTestId('actions-tree')).toMatchAriaSnapshot(`
 | 
				
			||||||
 | 
					    - tree:
 | 
				
			||||||
 | 
					      - treeitem /Before Hooks \\d+[hmsp]+/
 | 
				
			||||||
 | 
					      - treeitem /first \\d+[hmsp]+/
 | 
				
			||||||
 | 
					      - treeitem /middle \\d+[hmsp]+/
 | 
				
			||||||
 | 
					      - treeitem /last \\d+[hmsp]+/
 | 
				
			||||||
 | 
					      - treeitem /After Hooks \\d+[hmsp]+/
 | 
				
			||||||
 | 
					  `);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await page.getByTestId('actions-tree').getByText('first').click();
 | 
				
			||||||
 | 
					  const snapshot = page.frameLocator('iframe.snapshot-visible[name=snapshot]').locator('div');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await page.getByText('After', { exact: true }).click();
 | 
				
			||||||
 | 
					  await expect(snapshot).toHaveText('foo');
 | 
				
			||||||
 | 
					  await page.getByText('Before', { exact: true }).click();
 | 
				
			||||||
 | 
					  await expect(snapshot).toHaveText('initial');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await page.getByTestId('actions-tree').getByText('middle').click();
 | 
				
			||||||
 | 
					  await page.getByText('After', { exact: true }).click();
 | 
				
			||||||
 | 
					  await expect(snapshot).toHaveText('bar');
 | 
				
			||||||
 | 
					  await page.getByText('Before', { exact: true }).click();
 | 
				
			||||||
 | 
					  await expect(snapshot).toHaveText('foo');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await page.getByTestId('actions-tree').getByText('last').click();
 | 
				
			||||||
 | 
					  await page.getByText('After', { exact: true }).click();
 | 
				
			||||||
 | 
					  await expect(snapshot).toHaveText('baz');
 | 
				
			||||||
 | 
					  await page.getByText('Before', { exact: true }).click();
 | 
				
			||||||
 | 
					  await expect(snapshot).toHaveText('bar');
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('should show image diff', async ({ runUITest }) => {
 | 
					test('should show image diff', async ({ runUITest }) => {
 | 
				
			||||||
  const { page } = await runUITest({
 | 
					  const { page } = await runUITest({
 | 
				
			||||||
    'playwright.config.js': `
 | 
					    'playwright.config.js': `
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user