Skip to content

Commit d893c00

Browse files
authored
feat: show locator for expect steps in report (#38218)
1 parent a764706 commit d893c00

File tree

5 files changed

+44
-30
lines changed

5 files changed

+44
-30
lines changed

packages/playwright/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,8 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
268268
// and connect it to the existing expect step.
269269
if (zone.apiName)
270270
data.apiName = zone.apiName;
271-
if (zone.title)
272-
data.title = zone.title;
271+
if (zone.shortTitle || zone.title)
272+
data.title = zone.shortTitle ?? zone.title;
273273
data.stepId = zone.stepId;
274274
return;
275275
}

packages/playwright/src/matchers/expect.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424

2525
import { ExpectError, isJestError } from './matcherHint';
2626
import {
27+
computeMatcherTitleSuffix,
2728
toBeAttached,
2829
toBeChecked,
2930
toBeDisabled,
@@ -55,7 +56,7 @@ import {
5556
toPass
5657
} from './matchers';
5758
import { toMatchAriaSnapshot } from './toMatchAriaSnapshot';
58-
import { toHaveScreenshot, toHaveScreenshotStepTitle, toMatchSnapshot } from './toMatchSnapshot';
59+
import { toHaveScreenshot, toMatchSnapshot } from './toMatchSnapshot';
5960
import {
6061
INVERTED_COLOR,
6162
RECEIVED_COLOR,
@@ -114,7 +115,7 @@ export const printReceivedStringContainExpectedResult = (
114115
type ExpectMessage = string | { message?: string };
115116

116117
function createMatchers(actual: unknown, info: ExpectMetaInfo, prefix: string[]): any {
117-
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(info, prefix));
118+
return new Proxy(expectLibrary(actual), new ExpectMetaInfoProxyHandler(actual, info, prefix));
118119
}
119120

120121
const userMatchersSymbol = Symbol('userMatchers');
@@ -300,10 +301,12 @@ type ExpectMetaInfo = {
300301
};
301302

302303
class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
304+
private _actual: any;
303305
private _info: ExpectMetaInfo;
304306
private _prefix: string[];
305307

306-
constructor(info: ExpectMetaInfo, prefix: string[]) {
308+
constructor(actual: any, info: ExpectMetaInfo, prefix: string[]) {
309+
this._actual = actual;
307310
this._info = { ...info };
308311
this._prefix = prefix;
309312
}
@@ -344,11 +347,11 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
344347
return matcher.call(target, ...args);
345348

346349
const customMessage = this._info.message || '';
347-
const argsSuffix = computeArgsSuffix(matcherName, args);
348-
349-
const defaultTitle = `${this._info.poll ? 'poll ' : ''}${this._info.isSoft ? 'soft ' : ''}${this._info.isNot ? 'not ' : ''}${matcherName}${argsSuffix}`;
350-
const title = customMessage || `Expect ${escapeWithQuotes(defaultTitle, '"')}`;
351-
const apiName = `expect${this._info.poll ? '.poll ' : ''}${this._info.isSoft ? '.soft ' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`;
350+
const suffixes = computeMatcherTitleSuffix(matcherName, this._actual, args);
351+
const defaultTitle = `${this._info.poll ? 'poll ' : ''}${this._info.isSoft ? 'soft ' : ''}${this._info.isNot ? 'not ' : ''}${matcherName}${suffixes.short || ''}`;
352+
const shortTitle = customMessage || `Expect ${escapeWithQuotes(defaultTitle, '"')}`;
353+
const longTitle = shortTitle + (suffixes.long || '');
354+
const apiName = `expect${this._info.poll ? '.poll ' : ''}${this._info.isSoft ? '.soft ' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${suffixes.short || ''}`;
352355

353356
// This looks like it is unnecessary, but it isn't - we need to filter
354357
// out all the frames that belong to the test runner from caught runtime errors.
@@ -359,7 +362,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
359362
const stepInfo = {
360363
category: 'expect' as const,
361364
apiName,
362-
title,
365+
title: longTitle,
366+
shortTitle,
363367
params: args[0] ? { expected: args[0] } : undefined,
364368
infectParentStepsWithError: this._info.isSoft,
365369
};
@@ -443,13 +447,6 @@ async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, p
443447
}
444448
}
445449

446-
function computeArgsSuffix(matcherName: string, args: any[]) {
447-
let value = '';
448-
if (matcherName === 'toHaveScreenshot')
449-
value = toHaveScreenshotStepTitle(...args);
450-
return value ? `(${value})` : '';
451-
}
452-
453450
export const expect: Expect<{}> = createExpect({}, [], {}).extend(customMatchers);
454451

455452
export function mergeExpects(...expects: any[]) {

packages/playwright/src/matchers/matchers.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils';
17+
import { asLocatorDescription, constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils';
1818
import { colors } from 'playwright-core/lib/utils';
1919

2020
import { expectTypes } from '../util';
2121
import { toBeTruthy } from './toBeTruthy';
2222
import { toEqual } from './toEqual';
2323
import { toHaveURLWithPredicate } from './toHaveURL';
2424
import { toMatchText } from './toMatchText';
25+
import { toHaveScreenshotStepTitle } from './toMatchSnapshot';
2526
import { takeFirst } from '../common/config';
2627
import { currentTestInfo } from '../common/globals';
2728
import { TestInfoImpl } from '../worker/testInfo';
@@ -35,6 +36,7 @@ import type { FrameExpectParams } from 'playwright-core/lib/client/types';
3536
export type ExpectMatcherStateInternal = ExpectMatcherState & { _stepInfo?: TestStepInfoImpl };
3637

3738
export interface LocatorEx extends Locator {
39+
_selector: string;
3840
_expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string }>;
3941
}
4042

@@ -490,3 +492,17 @@ export async function toPass(
490492
}
491493
return { pass: !this.isNot, message: () => '' };
492494
}
495+
496+
export function computeMatcherTitleSuffix(matcherName: string, receiver: any, args: any[]): { short?: string, long?: string } {
497+
if (matcherName === 'toHaveScreenshot') {
498+
const title = toHaveScreenshotStepTitle(...args);
499+
return { short: title ? `(${title})` : '' };
500+
}
501+
if (receiver && typeof receiver === 'object' && receiver.constructor?.name === 'Locator') {
502+
try {
503+
return { long: ' ' + asLocatorDescription('javascript', (receiver as LocatorEx)._selector) };
504+
} catch {
505+
}
506+
}
507+
return {};
508+
}

packages/playwright/src/worker/testInfo.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type TestStepCategory = 'expect' | 'fixture' | 'hook' | 'pw:api' | 'test.
3737

3838
interface TestStepData {
3939
title: string;
40+
shortTitle?: string;
4041
category: TestStepCategory;
4142
location?: Location;
4243
apiName?: string;
@@ -374,7 +375,7 @@ export class TestInfoImpl implements TestInfo {
374375
this._tracing.appendBeforeActionForStep({
375376
stepId,
376377
parentId: parentStep?.stepId,
377-
title: step.title,
378+
title: step.shortTitle ?? step.title,
378379
category: step.category,
379380
params: step.params,
380381
stack: step.location ? [step.location] : [],

tests/playwright-test/test-step.spec.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ test('should nest steps based on zones', async ({ runInlineTest }) => {
534534
}),
535535
test.step('parent2', async () => {
536536
await test.step('child2', async () => {
537-
await expect(page.locator('body')).toBeVisible();
537+
await expect(page.locator('body').describe('main element')).toBeVisible();
538538
});
539539
}),
540540
]);
@@ -562,7 +562,7 @@ test.step | child1 @ a.test.ts:23
562562
pw:api | Click locator('body') @ a.test.ts:24
563563
test.step | parent2 @ a.test.ts:27
564564
test.step | child2 @ a.test.ts:28
565-
expect | Expect "toBeVisible" @ a.test.ts:29
565+
expect | Expect "toBeVisible" main element @ a.test.ts:29
566566
hook |After Hooks
567567
hook | afterEach hook @ a.test.ts:15
568568
test.step | in afterEach @ a.test.ts:16
@@ -892,7 +892,7 @@ pw:api | Create context
892892
fixture | Fixture "page"
893893
pw:api | Create page
894894
pw:api |Set content @ a.test.ts:4
895-
expect |Checking color @ a.test.ts:6
895+
expect |Checking color locator('div') @ a.test.ts:6
896896
hook |After Hooks
897897
fixture | Fixture "page"
898898
fixture | Fixture "context"
@@ -1087,14 +1087,14 @@ fixture | Fixture "page"
10871087
pw:api | Create page
10881088
pw:api |Set content @ a.test.ts:4
10891089
expect |Expect "poll toBe" @ a.test.ts:13
1090-
expect | Expect "toHaveText" @ a.test.ts:7
1090+
expect | Expect "toHaveText" locator('div') @ a.test.ts:7
10911091
test.step | iteration 1 @ a.test.ts:9
1092-
expect | Expect "toBeVisible" @ a.test.ts:10
1092+
expect | Expect "toBeVisible" locator('div') @ a.test.ts:10
10931093
expect | Expect "toBe" @ a.test.ts:6
10941094
expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality
1095-
expect | Expect "toHaveText" @ a.test.ts:7
1095+
expect | Expect "toHaveText" locator('div') @ a.test.ts:7
10961096
test.step | iteration 2 @ a.test.ts:9
1097-
expect | Expect "toBeVisible" @ a.test.ts:10
1097+
expect | Expect "toBeVisible" locator('div') @ a.test.ts:10
10981098
expect | Expect "toBe" @ a.test.ts:6
10991099
hook |After Hooks
11001100
fixture | Fixture "page"
@@ -1451,7 +1451,7 @@ pw:api | Wait for event "response" @ a.test.ts:7
14511451
pw:api | Click locator('div') @ a.test.ts:14
14521452
pw:api | Get content @ a.test.ts:8
14531453
pw:api | Get content @ a.test.ts:9
1454-
expect | Expect "toContainText" @ a.test.ts:10
1454+
expect | Expect "toContainText" locator('div') @ a.test.ts:10
14551455
expect |Expect "toBe" @ a.test.ts:18
14561456
hook |After Hooks
14571457
fixture | Fixture "page"
@@ -1745,7 +1745,7 @@ pw:api | Create context
17451745
fixture | Fixture "page"
17461746
pw:api | Create page
17471747
pw:api |Set content @ a.test.ts:16
1748-
expect |Expect "toBeInvisible" @ a.test.ts:17
1748+
expect |Expect "toBeInvisible" locator('div') @ a.test.ts:17
17491749
expect | Expect "poll toBe" @ a.test.ts:7
17501750
pw:api | Query count locator('div').filter({ visible: true }) @ a.test.ts:7
17511751
expect | Expect "toBe" @ a.test.ts:7
@@ -1829,7 +1829,7 @@ pw:api | Navigate to "data:" @ a.test.ts:16
18291829
pw:api | Set content @ a.test.ts:22
18301830
test.step | inner step @ a.test.ts:23
18311831
pw:api | Navigate to "data:" @ a.test.ts:24
1832-
expect |Expect "toBeVisible" @ a.test.ts:32
1832+
expect |Expect "toBeVisible" locator('body') @ a.test.ts:32
18331833
expect |Expect "toBe" @ a.test.ts:33
18341834
expect |Expect "toBe" @ a.test.ts:34
18351835
expect |Expect "toBe" @ a.test.ts:35

0 commit comments

Comments
 (0)