Skip to content

[pull] main from microsoft:main #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions packages/playwright/src/matchers/matcherHint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@
*/

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

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

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

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

Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/matchers/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ export function toHaveClass(
return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues(expected);
return await locator._expect('to.have.class.array', { expectedText, isNot, timeout });
}, expected, options);
}, expected, options, true);
} else {
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues([expected]);
Expand All @@ -283,7 +283,7 @@ export function toContainClass(
return toEqual.call(this, 'toContainClass', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues(expected);
return await locator._expect('to.contain.class.array', { expectedText, isNot, timeout });
}, expected, options);
}, expected, options, true);
} else {
if (isRegExp(expected))
throw new Error(`"expected" argument in toContainClass cannot be a RegExp value`);
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/matchers/toBeTruthy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ export async function toBeTruthy(
printedReceived = `Received: ${notFound ? kNoElementsFoundError : received}`;
}
const message = () => {
const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined);
const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined, `${printedExpected}\n${printedReceived}`);
const logText = callLogText(log);
return `${header}${printedExpected}\n${printedReceived}${logText}`;
return `${header}${logText}`;
};
return {
message,
Expand Down
5 changes: 3 additions & 2 deletions packages/playwright/src/matchers/toEqual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export async function toEqual<T>(
query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>,
expected: T,
options: { timeout?: number, contains?: boolean } = {},
messagePreventExtraStatIndent?: boolean
): Promise<MatcherResult<any, any>> {
expectTypes(receiver, [receiverType], matcherName);

Expand Down Expand Up @@ -87,9 +88,9 @@ export async function toEqual<T>(
);
}
const message = () => {
const header = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
const details = printedDiff || `${printedExpected}\n${printedReceived}`;
return `${header}${details}${callLogText(log)}`;
const header = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined, details, messagePreventExtraStatIndent);
return `${header}${callLogText(log)}`;
};
// Passing the actual and expected objects so that a custom reporter
// could access them, for example in order to display a custom visual diff,
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright/src/matchers/toHaveURL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ export async function toHaveURLWithPredicate(
throw new Error(
[
// Always display `expected` in expectation place
matcherHint(this, undefined, matcherName, expression, undefined, matcherOptions),
matcherHint(this, undefined, matcherName, expression, undefined, matcherOptions, undefined, undefined, true),
`${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected')} value must be a string, regular expression, or predicate`,
this.utils.printWithType('Expected', expected, this.utils.printExpected,),
this.utils.printWithType('Expected', expected, this.utils.printExpected),
].join('\n\n'),
);
}
Expand Down Expand Up @@ -118,7 +118,7 @@ function toHaveURLMessage(
promise: state.promise,
};
const receivedString = received || '';
const messagePrefix = matcherHint(state, undefined, matcherName, expression, undefined, matcherOptions, didTimeout ? timeout : undefined);
const messagePrefix = matcherHint(state, undefined, matcherName, expression, undefined, matcherOptions, didTimeout ? timeout : undefined, undefined, true);

let printedReceived: string | undefined;
let printedExpected: string | undefined;
Expand Down
19 changes: 10 additions & 9 deletions packages/playwright/src/matchers/toMatchAriaSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,15 @@ export async function toMatchAriaSnapshot(
const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout });
const typedReceived = received as MatcherReceived | typeof kNoElementsFoundError;

const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
const matcherHintWithExpect = (expectedReceivedString: string) => {
return matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined, expectedReceivedString);
};

const notFound = typedReceived === kNoElementsFoundError;
if (notFound) {
return {
pass: this.isNot,
message: () => messagePrefix + `Expected: ${this.utils.printExpected(expected)}\nReceived: ${EXPECTED_COLOR('<element not found>')}` + callLogText(log),
message: () => matcherHintWithExpect(`Expected: ${this.utils.printExpected(expected)}\nReceived: ${EXPECTED_COLOR('<element not found>')}`) + callLogText(log),
name: 'toMatchAriaSnapshot',
expected,
};
Expand All @@ -106,15 +109,13 @@ export async function toMatchAriaSnapshot(
const receivedText = typedReceived.raw;
const message = () => {
if (pass) {
if (notFound)
return messagePrefix + `Expected: not ${this.utils.printExpected(expected)}\nReceived: ${receivedText}` + callLogText(log);
const printedReceived = printReceivedStringContainExpectedSubstring(receivedText, receivedText.indexOf(expected), expected.length);
return messagePrefix + `Expected: not ${this.utils.printExpected(expected)}\nReceived: ${printedReceived}` + callLogText(log);
const receivedString = notFound ? receivedText : printReceivedStringContainExpectedSubstring(receivedText, receivedText.indexOf(expected), expected.length);
const expectedReceivedString = `Expected: not ${this.utils.printExpected(expected)}\nReceived: ${receivedString}`;
return matcherHintWithExpect(expectedReceivedString) + callLogText(log);
} else {
const labelExpected = `Expected`;
if (notFound)
return messagePrefix + `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${receivedText}` + callLogText(log);
return messagePrefix + this.utils.printDiffOrStringify(expected, receivedText, labelExpected, 'Received', false) + callLogText(log);
const expectedReceivedString = notFound ? `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${receivedText}` : this.utils.printDiffOrStringify(expected, receivedText, labelExpected, 'Received', false);
return matcherHintWithExpect(expectedReceivedString) + callLogText(log);
}
};

Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/matchers/toMatchSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ export function toMatchSnapshot(
return helper.handleMatching();

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

Expand Down
6 changes: 3 additions & 3 deletions packages/playwright/src/matchers/toMatchText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export async function toMatchText(
) {
// Same format as jest's matcherErrorMessage
throw new Error([
matcherHint(this, receiverType === 'Locator' ? receiver as Locator : undefined, matcherName, options.receiverLabel ?? receiver, expected, matcherOptions),
matcherHint(this, receiverType === 'Locator' ? receiver as Locator : undefined, matcherName, options.receiverLabel ?? receiver, expected, matcherOptions, undefined, undefined, true),
`${colors.bold('Matcher error')}: ${EXPECTED_COLOR('expected',)} value must be a string or regular expression`,
this.utils.printWithType('Expected', expected, this.utils.printExpected)
].join('\n\n'));
Expand All @@ -71,7 +71,6 @@ export async function toMatchText(

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

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

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

return {
Expand Down
42 changes: 36 additions & 6 deletions tests/page/expect-boolean.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ test.describe('toBeChecked', () => {
await page.setContent('<input type=checkbox></input>');
const locator = page.locator('input');
const error = await expect(locator).toBeChecked({ timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).toBeChecked()`);
expect(stripAnsi(error.message)).toContain(`expect(locator).toBeChecked() failed

Locator: locator('input')
Expected: checked
Received: unchecked
Timeout: 1000ms`);
expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`);
});

Expand All @@ -75,7 +80,12 @@ test.describe('toBeChecked', () => {
await page.setContent('<input type=checkbox checked></input>');
const locator = page.locator('input');
const error = await expect(locator).not.toBeChecked({ timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).not.toBeChecked()`);
expect(stripAnsi(error.message)).toContain(`expect(locator).not.toBeChecked() failed

Locator: locator('input')
Expected: not checked
Received: checked
Timeout: 1000ms`);
expect(stripAnsi(error.message)).toContain(`- Expect "not toBeChecked" with timeout 1000ms`);
expect(stripAnsi(error.message)).toContain(`locator resolved to <input checked type="checkbox"/>`);
});
Expand All @@ -84,7 +94,12 @@ test.describe('toBeChecked', () => {
await page.setContent('<input type=checkbox checked></input>');
const locator = page.locator('input');
const error = await expect(locator).toBeChecked({ checked: false, timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).toBeChecked({ checked: false })`);
expect(stripAnsi(error.message)).toContain(`expect(locator).toBeChecked({ checked: false }) failed

Locator: locator('input')
Expected: unchecked
Received: checked
Timeout: 1000ms`);
expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`);
expect(stripAnsi(error.message)).toContain(`locator resolved to <input checked type="checkbox"/>`);
});
Expand All @@ -93,15 +108,25 @@ test.describe('toBeChecked', () => {
await page.setContent('<input type=checkbox></input>');
const locator = page.locator('input');
const error = await expect(locator).toBeChecked({ indeterminate: true, timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).toBeChecked({ indeterminate: true })`);
expect(stripAnsi(error.message)).toContain(`expect(locator).toBeChecked({ indeterminate: true }) failed

Locator: locator('input')
Expected: indeterminate
Received: unchecked
Timeout: 1000ms`);
expect(stripAnsi(error.message)).toContain(`- Expect "toBeChecked" with timeout 1000ms`);
});

test('fail missing', async ({ page }) => {
await page.setContent('<div>no inputs here</div>');
const locator2 = page.locator('input2');
const error = await expect(locator2).not.toBeChecked({ timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).not.toBeChecked()`);
expect(stripAnsi(error.message)).toContain(`expect(locator).not.toBeChecked() failed

Locator: locator('input2')
Expected: not checked
Received: <element(s) not found>
Timeout: 1000ms`);
expect(stripAnsi(error.message)).toContain(`- Expect "not toBeChecked" with timeout 1000ms`);
expect(stripAnsi(error.message)).toContain(`- waiting for locator(\'input2\')`);
});
Expand Down Expand Up @@ -439,7 +464,12 @@ test.describe('toBeHidden', () => {
await page.setContent('<div></div>');
const locator = page.locator('button');
const error = await expect(locator).not.toBeHidden({ timeout: 1000 }).catch(e => e);
expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).not.toBeHidden()`);
expect(stripAnsi(error.message)).toContain(`expect(locator).not.toBeHidden() failed

Locator: locator('button')
Expected: not hidden
Received: <element(s) not found>
Timeout: 1000ms`);
expect(stripAnsi(error.message)).toContain(`- Expect "not toBeHidden" with timeout 1000ms`);
});

Expand Down
Loading
Loading