Skip to content

Commit 8e46278

Browse files
authored
chore(expect): more consistent display of expect timeout error messages (microsoft#36543)
1 parent 04316f1 commit 8e46278

18 files changed

+285
-112
lines changed

packages/playwright/src/matchers/matcherHint.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,23 @@
1515
*/
1616

1717
import { stringifyStackFrames } from 'playwright-core/lib/utils';
18-
import { colors } from 'playwright-core/lib/utils';
1918

2019
import type { ExpectMatcherState } from '../../types/test';
2120
import type { StackFrame } from '@protocol/channels';
2221
import type { Locator } from 'playwright-core';
2322

2423
export const kNoElementsFoundError = '<element(s) not found>';
2524

26-
export function matcherHint(state: ExpectMatcherState, locator: Locator | undefined, matcherName: string, expression: any, actual: any, matcherOptions: any, timeout?: number) {
27-
let header = state.utils.matcherHint(matcherName, expression, actual, matcherOptions).replace(/ \/\/ deep equality/, '') + '\n\n';
28-
if (timeout)
29-
header = colors.red(`Timed out ${timeout}ms waiting for `) + header;
25+
export function matcherHint(state: ExpectMatcherState, locator: Locator | undefined, matcherName: string, expression: any, actual: any, matcherOptions: any, timeout: number | undefined, expectedReceivedString?: string, preventExtraStatIndent: boolean = false) {
26+
let header = state.utils.matcherHint(matcherName, expression, actual, matcherOptions).replace(/ \/\/ deep equality/, '') + ' failed\n\n';
27+
// Extra space added after locator and timeout to match Jest's received/expected output
28+
const extraSpace = preventExtraStatIndent ? '' : ' ';
3029
if (locator)
31-
header += `Locator: ${String(locator)}\n`;
30+
header += `Locator: ${extraSpace}${String(locator)}\n`;
31+
if (expectedReceivedString)
32+
header += `${expectedReceivedString}\n`;
33+
if (timeout)
34+
header += `Timeout: ${extraSpace}${timeout}ms\n`;
3235
return header;
3336
}
3437

packages/playwright/src/matchers/matchers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ export function toHaveClass(
262262
return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
263263
const expectedText = serializeExpectedTextValues(expected);
264264
return await locator._expect('to.have.class.array', { expectedText, isNot, timeout });
265-
}, expected, options);
265+
}, expected, options, true);
266266
} else {
267267
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
268268
const expectedText = serializeExpectedTextValues([expected]);
@@ -283,7 +283,7 @@ export function toContainClass(
283283
return toEqual.call(this, 'toContainClass', locator, 'Locator', async (isNot, timeout) => {
284284
const expectedText = serializeExpectedTextValues(expected);
285285
return await locator._expect('to.contain.class.array', { expectedText, isNot, timeout });
286-
}, expected, options);
286+
}, expected, options, true);
287287
} else {
288288
if (isRegExp(expected))
289289
throw new Error(`"expected" argument in toContainClass cannot be a RegExp value`);

packages/playwright/src/matchers/toBeTruthy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ export async function toBeTruthy(
6060
printedReceived = `Received: ${notFound ? kNoElementsFoundError : received}`;
6161
}
6262
const message = () => {
63-
const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined);
63+
const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined, `${printedExpected}\n${printedReceived}`);
6464
const logText = callLogText(log);
65-
return `${header}${printedExpected}\n${printedReceived}${logText}`;
65+
return `${header}${logText}`;
6666
};
6767
return {
6868
message,

packages/playwright/src/matchers/toEqual.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export async function toEqual<T>(
3535
query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>,
3636
expected: T,
3737
options: { timeout?: number, contains?: boolean } = {},
38+
messagePreventExtraStatIndent?: boolean
3839
): Promise<MatcherResult<any, any>> {
3940
expectTypes(receiver, [receiverType], matcherName);
4041

@@ -87,9 +88,9 @@ export async function toEqual<T>(
8788
);
8889
}
8990
const message = () => {
90-
const header = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
9191
const details = printedDiff || `${printedExpected}\n${printedReceived}`;
92-
return `${header}${details}${callLogText(log)}`;
92+
const header = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined, details, messagePreventExtraStatIndent);
93+
return `${header}${callLogText(log)}`;
9394
};
9495
// Passing the actual and expected objects so that a custom reporter
9596
// could access them, for example in order to display a custom visual diff,

packages/playwright/src/matchers/toHaveURL.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ export async function toHaveURLWithPredicate(
4242
throw new Error(
4343
[
4444
// Always display `expected` in expectation place
45-
matcherHint(this, undefined, matcherName, expression, undefined, matcherOptions),
45+
matcherHint(this, undefined, matcherName, expression, undefined, matcherOptions, undefined, undefined, true),
4646
`${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected')} value must be a string, regular expression, or predicate`,
47-
this.utils.printWithType('Expected', expected, this.utils.printExpected,),
47+
this.utils.printWithType('Expected', expected, this.utils.printExpected),
4848
].join('\n\n'),
4949
);
5050
}
@@ -118,7 +118,7 @@ function toHaveURLMessage(
118118
promise: state.promise,
119119
};
120120
const receivedString = received || '';
121-
const messagePrefix = matcherHint(state, undefined, matcherName, expression, undefined, matcherOptions, didTimeout ? timeout : undefined);
121+
const messagePrefix = matcherHint(state, undefined, matcherName, expression, undefined, matcherOptions, didTimeout ? timeout : undefined, undefined, true);
122122

123123
let printedReceived: string | undefined;
124124
let printedExpected: string | undefined;

packages/playwright/src/matchers/toMatchAriaSnapshot.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,15 @@ export async function toMatchAriaSnapshot(
9292
const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout });
9393
const typedReceived = received as MatcherReceived | typeof kNoElementsFoundError;
9494

95-
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
95+
const matcherHintWithExpect = (expectedReceivedString: string) => {
96+
return matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined, expectedReceivedString);
97+
};
98+
9699
const notFound = typedReceived === kNoElementsFoundError;
97100
if (notFound) {
98101
return {
99102
pass: this.isNot,
100-
message: () => messagePrefix + `Expected: ${this.utils.printExpected(expected)}\nReceived: ${EXPECTED_COLOR('<element not found>')}` + callLogText(log),
103+
message: () => matcherHintWithExpect(`Expected: ${this.utils.printExpected(expected)}\nReceived: ${EXPECTED_COLOR('<element not found>')}`) + callLogText(log),
101104
name: 'toMatchAriaSnapshot',
102105
expected,
103106
};
@@ -106,15 +109,13 @@ export async function toMatchAriaSnapshot(
106109
const receivedText = typedReceived.raw;
107110
const message = () => {
108111
if (pass) {
109-
if (notFound)
110-
return messagePrefix + `Expected: not ${this.utils.printExpected(expected)}\nReceived: ${receivedText}` + callLogText(log);
111-
const printedReceived = printReceivedStringContainExpectedSubstring(receivedText, receivedText.indexOf(expected), expected.length);
112-
return messagePrefix + `Expected: not ${this.utils.printExpected(expected)}\nReceived: ${printedReceived}` + callLogText(log);
112+
const receivedString = notFound ? receivedText : printReceivedStringContainExpectedSubstring(receivedText, receivedText.indexOf(expected), expected.length);
113+
const expectedReceivedString = `Expected: not ${this.utils.printExpected(expected)}\nReceived: ${receivedString}`;
114+
return matcherHintWithExpect(expectedReceivedString) + callLogText(log);
113115
} else {
114116
const labelExpected = `Expected`;
115-
if (notFound)
116-
return messagePrefix + `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${receivedText}` + callLogText(log);
117-
return messagePrefix + this.utils.printDiffOrStringify(expected, receivedText, labelExpected, 'Received', false) + callLogText(log);
117+
const expectedReceivedString = notFound ? `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${receivedText}` : this.utils.printDiffOrStringify(expected, receivedText, labelExpected, 'Received', false);
118+
return matcherHintWithExpect(expectedReceivedString) + callLogText(log);
118119
}
119120
};
120121

packages/playwright/src/matchers/toMatchSnapshot.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ export function toMatchSnapshot(
302302
return helper.handleMatching();
303303

304304
const receiver = isString(received) ? 'string' : 'Buffer';
305-
const header = matcherHint(this, undefined, 'toMatchSnapshot', receiver, undefined, undefined);
305+
const header = matcherHint(this, undefined, 'toMatchSnapshot', receiver, undefined, undefined, undefined);
306306
return helper.handleDifferent(received, expected, undefined, result.diff, header, result.errorMessage, undefined, this._stepInfo);
307307
}
308308

packages/playwright/src/matchers/toMatchText.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export async function toMatchText(
5151
) {
5252
// Same format as jest's matcherErrorMessage
5353
throw new Error([
54-
matcherHint(this, receiverType === 'Locator' ? receiver as Locator : undefined, matcherName, options.receiverLabel ?? receiver, expected, matcherOptions),
54+
matcherHint(this, receiverType === 'Locator' ? receiver as Locator : undefined, matcherName, options.receiverLabel ?? receiver, expected, matcherOptions, undefined, undefined, true),
5555
`${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected',)} value must be a string or regular expression`,
5656
this.utils.printWithType('Expected', expected, this.utils.printExpected)
5757
].join('\n\n'));
@@ -71,7 +71,6 @@ export async function toMatchText(
7171

7272
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
7373
const receivedString = received || '';
74-
const messagePrefix = matcherHint(this, receiverType === 'Locator' ? receiver as Locator : undefined, matcherName, options.receiverLabel ?? 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
7574
const notFound = received === kNoElementsFoundError;
7675

7776
let printedReceived: string | undefined;
@@ -109,7 +108,8 @@ export async function toMatchText(
109108

110109
const message = () => {
111110
const resultDetails = printedDiff ? printedDiff : printedExpected + '\n' + printedReceived;
112-
return messagePrefix + resultDetails + callLogText(log);
111+
const hints = matcherHint(this, receiverType === 'Locator' ? receiver as Locator : undefined, matcherName, options.receiverLabel ?? 'locator', undefined, matcherOptions, timedOut ? timeout : undefined, resultDetails, true);
112+
return hints + callLogText(log);
113113
};
114114

115115
return {

tests/page/expect-boolean.spec.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ test.describe('toBeChecked', () => {
5555
await page.setContent('<input type=checkbox></input>');
5656
const locator = page.locator('input');
5757
const error = await expect(locator).toBeChecked({ timeout: 1000 }).catch(e => e);
58-
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).toBeChecked()`);
58+
expect(stripAnsi(error.message)).toContain(`expect(locator).toBeChecked() failed
59+
60+
Locator: locator('input')
61+
Expected: checked
62+
Received: unchecked
63+
Timeout: 1000ms`);
5964
expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`);
6065
});
6166

@@ -75,7 +80,12 @@ test.describe('toBeChecked', () => {
7580
await page.setContent('<input type=checkbox checked></input>');
7681
const locator = page.locator('input');
7782
const error = await expect(locator).not.toBeChecked({ timeout: 1000 }).catch(e => e);
78-
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).not.toBeChecked()`);
83+
expect(stripAnsi(error.message)).toContain(`expect(locator).not.toBeChecked() failed
84+
85+
Locator: locator('input')
86+
Expected: not checked
87+
Received: checked
88+
Timeout: 1000ms`);
7989
expect(stripAnsi(error.message)).toContain(`- Expect "not toBeChecked" with timeout 1000ms`);
8090
expect(stripAnsi(error.message)).toContain(`locator resolved to <input checked type="checkbox"/>`);
8191
});
@@ -84,7 +94,12 @@ test.describe('toBeChecked', () => {
8494
await page.setContent('<input type=checkbox checked></input>');
8595
const locator = page.locator('input');
8696
const error = await expect(locator).toBeChecked({ checked: false, timeout: 1000 }).catch(e => e);
87-
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).toBeChecked({ checked: false })`);
97+
expect(stripAnsi(error.message)).toContain(`expect(locator).toBeChecked({ checked: false }) failed
98+
99+
Locator: locator('input')
100+
Expected: unchecked
101+
Received: checked
102+
Timeout: 1000ms`);
88103
expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`);
89104
expect(stripAnsi(error.message)).toContain(`locator resolved to <input checked type="checkbox"/>`);
90105
});
@@ -93,15 +108,25 @@ test.describe('toBeChecked', () => {
93108
await page.setContent('<input type=checkbox></input>');
94109
const locator = page.locator('input');
95110
const error = await expect(locator).toBeChecked({ indeterminate: true, timeout: 1000 }).catch(e => e);
96-
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).toBeChecked({ indeterminate: true })`);
111+
expect(stripAnsi(error.message)).toContain(`expect(locator).toBeChecked({ indeterminate: true }) failed
112+
113+
Locator: locator('input')
114+
Expected: indeterminate
115+
Received: unchecked
116+
Timeout: 1000ms`);
97117
expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`);
98118
});
99119

100120
test('fail missing', async ({ page }) => {
101121
await page.setContent('<div>no inputs here</div>');
102122
const locator2 = page.locator('input2');
103123
const error = await expect(locator2).not.toBeChecked({ timeout: 1000 }).catch(e => e);
104-
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).not.toBeChecked()`);
124+
expect(stripAnsi(error.message)).toContain(`expect(locator).not.toBeChecked() failed
125+
126+
Locator: locator('input2')
127+
Expected: not checked
128+
Received: <element(s) not found>
129+
Timeout: 1000ms`);
105130
expect(stripAnsi(error.message)).toContain(`- Expect "not toBeChecked" with timeout 1000ms`);
106131
expect(stripAnsi(error.message)).toContain(`- waiting for locator(\'input2\')`);
107132
});
@@ -439,7 +464,12 @@ test.describe('toBeHidden', () => {
439464
await page.setContent('<div></div>');
440465
const locator = page.locator('button');
441466
const error = await expect(locator).not.toBeHidden({ timeout: 1000 }).catch(e => e);
442-
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).not.toBeHidden()`);
467+
expect(stripAnsi(error.message)).toContain(`expect(locator).not.toBeHidden() failed
468+
469+
Locator: locator('button')
470+
Expected: not hidden
471+
Received: <element(s) not found>
472+
Timeout: 1000ms`);
443473
expect(stripAnsi(error.message)).toContain(`- Expect "not toBeHidden" with timeout 1000ms`);
444474
});
445475

0 commit comments

Comments
 (0)