Skip to content

Commit a717592

Browse files
authored
chore: unify expect message calculation (#37337)
1 parent bee11cb commit a717592

20 files changed

+420
-275
lines changed

packages/playwright/bundles/expect/src/expectBundleImpl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ export {
4545
EXPECTED_COLOR,
4646
INVERTED_COLOR,
4747
RECEIVED_COLOR,
48+
DIM_COLOR,
4849
printReceived,
4950
} from 'jest-matcher-utils';

packages/playwright/src/common/expectBundle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ export const matcherUtils = require('./expectBundleImpl').matcherUtils;
2121
export const EXPECTED_COLOR: typeof import('../../bundles/expect/node_modules/jest-matcher-utils/build').EXPECTED_COLOR = require('./expectBundleImpl').EXPECTED_COLOR;
2222
export const INVERTED_COLOR: typeof import('../../bundles/expect/node_modules/jest-matcher-utils/build').INVERTED_COLOR = require('./expectBundleImpl').INVERTED_COLOR;
2323
export const RECEIVED_COLOR: typeof import('../../bundles/expect/node_modules/jest-matcher-utils/build').RECEIVED_COLOR = require('./expectBundleImpl').RECEIVED_COLOR;
24+
export const DIM_COLOR: typeof import('../../bundles/expect/node_modules/jest-matcher-utils/build').DIM_COLOR = require('./expectBundleImpl').DIM_COLOR;
2425
export const printReceived: typeof import('../../bundles/expect/node_modules/jest-matcher-utils/build').printReceived = require('./expectBundleImpl').printReceived;

packages/playwright/src/matchers/matcherHint.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,62 @@
1515
*/
1616

1717
import { stringifyStackFrames } from 'playwright-core/lib/utils';
18+
import { DIM_COLOR, RECEIVED_COLOR, EXPECTED_COLOR } from '../common/expectBundle';
1819

1920
import type { ExpectMatcherState } from '../../types/test';
2021
import type { StackFrame } from '@protocol/channels';
2122
import type { Locator } from 'playwright-core';
2223

23-
export function matcherHint(state: ExpectMatcherState, locator: Locator | undefined, matcherName: string, expression: any, actual: any, matcherOptions: any, timeout: number | undefined, expectedReceivedString?: string, preventExtraStatIndent: boolean = false) {
24-
let header = state.utils.matcherHint(matcherName, expression, actual, matcherOptions).replace(/ \/\/ deep equality/, '') + ' failed\n\n';
25-
// Extra space added after locator and timeout to match Jest's received/expected output
26-
const extraSpace = preventExtraStatIndent ? '' : ' ';
27-
if (locator)
28-
header += `Locator: ${extraSpace}${String(locator)}\n`;
29-
if (expectedReceivedString)
30-
header += `${expectedReceivedString}\n`;
31-
if (timeout)
32-
header += `Timeout: ${extraSpace}${timeout}ms\n`;
33-
return header;
24+
type MatcherMessageDetails = {
25+
receiver?: string; // Assuming 'locator' when locator is provided, 'page' otherwise.
26+
matcherName: string;
27+
expectation: string;
28+
locator?: Locator;
29+
printedExpected?: string;
30+
printedReceived?: string;
31+
printedDiff?: string;
32+
timedOut?: boolean;
33+
timeout?: number;
34+
errorMessage?: string;
35+
log?: string[];
36+
};
37+
38+
export function formatMatcherMessage(state: ExpectMatcherState, details: MatcherMessageDetails) {
39+
const receiver = details.receiver ?? (details.locator ? 'locator' : 'page');
40+
let message = DIM_COLOR('expect(') + RECEIVED_COLOR(receiver)
41+
+ DIM_COLOR(')' + (state.promise ? '.' + state.promise : '') + (state.isNot ? '.not' : '') + '.')
42+
+ details.matcherName
43+
+ DIM_COLOR('(') + EXPECTED_COLOR(details.expectation) + DIM_COLOR(')')
44+
+ ' failed\n\n';
45+
46+
// Sometimes diff is actually expected + received. Turn it into two lines to
47+
// simplify alignment logic.
48+
const diffLines = details.printedDiff?.split('\n');
49+
if (diffLines?.length === 2) {
50+
details.printedExpected = diffLines[0];
51+
details.printedReceived = diffLines[1];
52+
details.printedDiff = undefined;
53+
}
54+
55+
const align = !details.errorMessage && details.printedExpected?.startsWith('Expected:')
56+
&& (!details.printedReceived || details.printedReceived.startsWith('Received:'));
57+
if (details.locator)
58+
message += `Locator: ${align ? ' ' : ''}${String(details.locator)}\n`;
59+
if (details.printedExpected)
60+
message += details.printedExpected + '\n';
61+
if (details.printedReceived)
62+
message += details.printedReceived + '\n';
63+
if (details.timedOut && details.timeout)
64+
message += `Timeout: ${align ? ' ' : ''}${details.timeout}ms\n`;
65+
if (details.printedDiff)
66+
message += details.printedDiff + '\n';
67+
if (details.errorMessage) {
68+
message += details.errorMessage;
69+
if (!details.errorMessage.endsWith('\n'))
70+
message += '\n';
71+
}
72+
message += callLogText(details.log);
73+
return message;
3474
}
3575

3676
export type MatcherResult<E, A> = {
@@ -71,3 +111,12 @@ export class ExpectError extends Error {
71111
export function isJestError(e: unknown): e is JestError {
72112
return e instanceof Error && 'matcherResult' in e;
73113
}
114+
115+
export const callLogText = (log: string[] | undefined) => {
116+
if (!log || !log.some(l => !!l))
117+
return '';
118+
return `
119+
Call log:
120+
${DIM_COLOR(log.join('\n'))}
121+
`;
122+
};

packages/playwright/src/matchers/matchers.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@
1717
import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils';
1818
import { colors } from 'playwright-core/lib/utils';
1919

20-
import { callLogText, expectTypes } from '../util';
20+
import { expectTypes } from '../util';
2121
import { toBeTruthy } from './toBeTruthy';
2222
import { toEqual } from './toEqual';
2323
import { toHaveURLWithPredicate } from './toHaveURL';
2424
import { toMatchText } from './toMatchText';
2525
import { takeFirst } from '../common/config';
2626
import { currentTestInfo } from '../common/globals';
2727
import { TestInfoImpl } from '../worker/testInfo';
28+
import { formatMatcherMessage } from './matcherHint';
2829

2930
import type { ExpectMatcherState } from '../../types/test';
3031
import type { TestStepInfoImpl } from '../worker/testInfo';
@@ -187,7 +188,7 @@ export function toContainText(
187188
return toMatchText.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => {
188189
const expectedText = serializeExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase });
189190
return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options.useInnerText, timeout });
190-
}, expected, options);
191+
}, expected, { ...options, matchSubstring: true });
191192
}
192193
}
193194

@@ -262,7 +263,7 @@ export function toHaveClass(
262263
return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
263264
const expectedText = serializeExpectedTextValues(expected);
264265
return await locator._expect('to.have.class.array', { expectedText, isNot, timeout });
265-
}, expected, options, true);
266+
}, expected, options);
266267
} else {
267268
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
268269
const expectedText = serializeExpectedTextValues([expected]);
@@ -283,7 +284,7 @@ export function toContainClass(
283284
return toEqual.call(this, 'toContainClass', locator, 'Locator', async (isNot, timeout) => {
284285
const expectedText = serializeExpectedTextValues(expected);
285286
return await locator._expect('to.contain.class.array', { expectedText, isNot, timeout });
286-
}, expected, options, true);
287+
}, expected, options);
287288
} else {
288289
if (isRegExp(expected))
289290
throw new Error(`"expected" argument in toContainClass cannot be a RegExp value`);
@@ -408,7 +409,7 @@ export function toHaveTitle(
408409
return toMatchText.call(this, 'toHaveTitle', page, 'Page', async (isNot, timeout) => {
409410
const expectedText = serializeExpectedTextValues([expected], { normalizeWhiteSpace: true });
410411
return await (page.mainFrame() as FrameEx)._expect('to.have.title', { expectedText, isNot, timeout });
411-
}, expected, { receiverLabel: 'page', ...options });
412+
}, expected, options);
412413
}
413414

414415
export function toHaveURL(
@@ -426,7 +427,7 @@ export function toHaveURL(
426427
return toMatchText.call(this, 'toHaveURL', page, 'Page', async (isNot, timeout) => {
427428
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase });
428429
return await (page.mainFrame() as FrameEx)._expect('to.have.url', { expectedText, isNot, timeout });
429-
}, expected, { receiverLabel: 'page', ...options });
430+
}, expected, options);
430431
}
431432

432433
export async function toBeOK(
@@ -443,9 +444,12 @@ export async function toBeOK(
443444
isTextEncoding ? response.text() : null
444445
]) : [];
445446

446-
const message = () => this.utils.matcherHint(matcherName, undefined, '', { isNot: this.isNot }) +
447-
callLogText(log) +
448-
(text === null ? '' : `\nResponse text:\n${colors.dim(text?.substring(0, 1000) || '')}`);
447+
const message = () => formatMatcherMessage(this, {
448+
matcherName,
449+
receiver: 'response',
450+
expectation: '',
451+
log,
452+
}) + (text === null ? '' : `\nResponse text:\n${colors.dim(text?.substring(0, 1000) || '')}`);
449453

450454
const pass = response.ok();
451455
return { message, pass };

packages/playwright/src/matchers/toBeTruthy.ts

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

17-
import { callLogText, expectTypes } from '../util';
18-
import { matcherHint } from './matcherHint';
17+
import { expectTypes } from '../util';
18+
import { formatMatcherMessage } from './matcherHint';
1919
import { runBrowserBackendOnError } from '../mcp/test/browserBackend';
2020

2121
import type { MatcherResult } from './matcherHint';
@@ -25,19 +25,14 @@ import type { Locator } from 'playwright-core';
2525
export async function toBeTruthy(
2626
this: ExpectMatcherState,
2727
matcherName: string,
28-
receiver: Locator,
28+
locator: Locator,
2929
receiverType: string,
3030
expected: string,
3131
arg: string,
3232
query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, log?: string[], received?: any, timedOut?: boolean, errorMessage?: string }>,
3333
options: { timeout?: number } = {},
3434
): Promise<MatcherResult<any, any>> {
35-
expectTypes(receiver, [receiverType], matcherName);
36-
37-
const matcherOptions = {
38-
isNot: this.isNot,
39-
promise: this.promise,
40-
};
35+
expectTypes(locator, [receiverType], matcherName);
4136

4237
const timeout = options.timeout ?? this.timeout;
4338

@@ -56,18 +51,26 @@ export async function toBeTruthy(
5651
let printedExpected: string | undefined;
5752
if (pass) {
5853
printedExpected = `Expected: not ${expected}`;
59-
printedReceived = errorMessage ?? `Received: ${expected}`;
54+
printedReceived = errorMessage ? '' : `Received: ${expected}`;
6055
} else {
6156
printedExpected = `Expected: ${expected}`;
62-
printedReceived = errorMessage ?? `Received: ${received}`;
57+
printedReceived = errorMessage ? '' : `Received: ${received}`;
6358
}
6459
const message = () => {
65-
const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined, `${printedExpected}\n${printedReceived}`);
66-
const logText = callLogText(log);
67-
return `${header}${logText}`;
60+
return formatMatcherMessage(this, {
61+
matcherName,
62+
expectation: arg,
63+
locator,
64+
timeout,
65+
timedOut,
66+
printedExpected,
67+
printedReceived,
68+
errorMessage,
69+
log,
70+
});
6871
};
6972

70-
await runBrowserBackendOnError(receiver.page(), message);
73+
await runBrowserBackendOnError(locator.page(), message);
7174

7275
return {
7376
message,

packages/playwright/src/matchers/toEqual.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
import { isRegExp } from 'playwright-core/lib/utils';
1818

19-
import { callLogText, expectTypes } from '../util';
20-
import { matcherHint } from './matcherHint';
19+
import { expectTypes } from '../util';
20+
import { formatMatcherMessage } from './matcherHint';
2121
import { runBrowserBackendOnError } from '../mcp/test/browserBackend';
2222

2323
import type { MatcherResult } from './matcherHint';
@@ -31,20 +31,13 @@ const RECEIVED_LABEL = 'Received';
3131
export async function toEqual<T>(
3232
this: ExpectMatcherState,
3333
matcherName: string,
34-
receiver: Locator,
34+
locator: Locator,
3535
receiverType: string,
3636
query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean, errorMessage?: string }>,
3737
expected: T,
3838
options: { timeout?: number, contains?: boolean } = {},
39-
messagePreventExtraStatIndent?: boolean
4039
): Promise<MatcherResult<any, any>> {
41-
expectTypes(receiver, [receiverType], matcherName);
42-
43-
const matcherOptions = {
44-
comment: options.contains ? '' : 'deep equality',
45-
isNot: this.isNot,
46-
promise: this.promise,
47-
};
40+
expectTypes(locator, [receiverType], matcherName);
4841

4942
const timeout = options.timeout ?? this.timeout;
5043

@@ -64,10 +57,9 @@ export async function toEqual<T>(
6457
let printedDiff: string | undefined;
6558
if (pass) {
6659
printedExpected = `Expected: not ${this.utils.printExpected(expected)}`;
67-
printedReceived = errorMessage ?? `Received: ${this.utils.printReceived(received)}`;
60+
printedReceived = errorMessage ? '' : `Received: ${this.utils.printReceived(received)}`;
6861
} else if (errorMessage) {
6962
printedExpected = `Expected: ${this.utils.printExpected(expected)}`;
70-
printedReceived = errorMessage;
7163
} else if (Array.isArray(expected) && Array.isArray(received)) {
7264
const normalizedExpected = expected.map((exp, index) => {
7365
const rec = received[index];
@@ -93,12 +85,21 @@ export async function toEqual<T>(
9385
);
9486
}
9587
const message = () => {
96-
const details = printedDiff || `${printedExpected}\n${printedReceived}`;
97-
const header = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined, details, messagePreventExtraStatIndent);
98-
return `${header}${callLogText(log)}`;
88+
return formatMatcherMessage(this, {
89+
matcherName,
90+
expectation: 'expected',
91+
locator,
92+
timeout,
93+
timedOut,
94+
printedExpected,
95+
printedReceived,
96+
printedDiff,
97+
errorMessage,
98+
log,
99+
});
99100
};
100101

101-
await runBrowserBackendOnError(receiver.page(), message);
102+
await runBrowserBackendOnError(locator.page(), message);
102103

103104
// Passing the actual and expected objects so that a custom reporter
104105
// could access them, for example in order to display a custom visual diff,

0 commit comments

Comments
 (0)