From 9fe037fb63a87844e94bc4111d1a0717b22f4623 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 16 Sep 2023 14:24:10 -0700 Subject: [PATCH] chore: populate matcherResult in web assertions (#27133) Ref https://github.com/microsoft/playwright/issues/26929 --- .../playwright-core/src/client/locator.ts | 4 +- packages/playwright/src/matchers/expect.ts | 10 - .../playwright/src/matchers/matcherHint.ts | 20 +- packages/playwright/src/matchers/matchers.ts | 45 ++-- .../playwright/src/matchers/toBeTruthy.ts | 20 +- packages/playwright/src/matchers/toEqual.ts | 21 +- .../playwright/src/matchers/toMatchText.ts | 26 +- tests/page/expect-matcher-result.spec.ts | 247 ++++++++++++++++++ tests/page/expect-timeout.spec.ts | 8 +- tests/page/locator-convenience.spec.ts | 8 +- .../playwright-test/expect-configure.spec.ts | 17 +- tests/playwright-test/expect.spec.ts | 17 +- tests/playwright-test/watch.spec.ts | 2 +- 13 files changed, 357 insertions(+), 88 deletions(-) create mode 100644 tests/page/expect-matcher-result.spec.ts diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 179ad53857..fb811ea861 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -18,7 +18,7 @@ import type * as structs from '../../types/structs'; import type * as api from '../../types/types'; import type * as channels from '@protocol/channels'; import * as util from 'util'; -import { isString, monotonicTime } from '../utils'; +import { asLocator, isString, monotonicTime } from '../utils'; import { ElementHandle } from './elementHandle'; import type { Frame } from './frame'; import type { FilePayload, FrameExpectOptions, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types'; @@ -355,7 +355,7 @@ export class Locator implements api.Locator { } toString() { - return `Locator@${this._selector}`; + return asLocator('javascript', this._selector, undefined, true); } } diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index b6c63888bc..a628af9caa 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -265,16 +265,6 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { const message = jestError.message; if (customMessage) { const messageLines = message.split('\n'); - // Jest adds something like the following error to all errors: - // expect(received).toBe(expected); // Object.is equality - const uselessMatcherLineIndex = messageLines.findIndex((line: string) => /expect.*\(.*received.*\)/.test(line)); - if (uselessMatcherLineIndex !== -1) { - // if there's a newline after the matcher text, then remove it as well. - if (uselessMatcherLineIndex + 1 < messageLines.length && messageLines[uselessMatcherLineIndex + 1].trim() === '') - messageLines.splice(uselessMatcherLineIndex, 2); - else - messageLines.splice(uselessMatcherLineIndex, 1); - } const newMessage = [ customMessage, '', diff --git a/packages/playwright/src/matchers/matcherHint.ts b/packages/playwright/src/matchers/matcherHint.ts index 34f859732b..fbdb10ce07 100644 --- a/packages/playwright/src/matchers/matcherHint.ts +++ b/packages/playwright/src/matchers/matcherHint.ts @@ -16,10 +16,22 @@ import { colors } from 'playwright-core/lib/utilsBundle'; import type { ExpectMatcherContext } from './expect'; +import type { Locator } from 'playwright-core'; -export function matcherHint(state: ExpectMatcherContext, matcherName: string, a: any, b: any, matcherOptions: any, timeout?: number) { - const message = state.utils.matcherHint(matcherName, a, b, matcherOptions); +export function matcherHint(state: ExpectMatcherContext, 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) - return colors.red(`Timed out ${timeout}ms waiting for `) + message; - return message; + header = colors.red(`Timed out ${timeout}ms waiting for `) + header; + if (locator) + header += `Locator: ${locator}\n`; + return header; } + +export type MatcherResult = { + locator: Locator; + name: string; + expected: E; + message: () => string; + pass: boolean; + actual?: A; +}; diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index 81fdf8c15f..b71bc47c6e 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -39,8 +39,11 @@ export function toBeAttached( locator: LocatorEx, options?: { attached?: boolean, timeout?: number }, ) { - return toBeTruthy.call(this, 'toBeAttached', locator, 'Locator', async (isNot, timeout) => { - const attached = !options || options.attached === undefined || options.attached === true; + const attached = !options || options.attached === undefined || options.attached === true; + const expected = attached ? 'attached' : 'detached'; + const unexpected = attached ? 'detached' : 'attached'; + const arg = attached ? '' : '{ attached: false }'; + return toBeTruthy.call(this, 'toBeAttached', locator, 'Locator', expected, unexpected, arg, async (isNot, timeout) => { return await locator._expect(attached ? 'to.be.attached' : 'to.be.detached', { isNot, timeout }); }, options); } @@ -50,8 +53,11 @@ export function toBeChecked( locator: LocatorEx, options?: { checked?: boolean, timeout?: number }, ) { - return toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', async (isNot, timeout) => { - const checked = !options || options.checked === undefined || options.checked === true; + const checked = !options || options.checked === undefined || options.checked === true; + const expected = checked ? 'checked' : 'unchecked'; + const unexpected = checked ? 'unchecked' : 'checked'; + const arg = checked ? '' : '{ checked: false }'; + return toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', expected, unexpected, arg, async (isNot, timeout) => { return await locator._expect(checked ? 'to.be.checked' : 'to.be.unchecked', { isNot, timeout }); }, options); } @@ -61,7 +67,7 @@ export function toBeDisabled( locator: LocatorEx, options?: { timeout?: number }, ) { - return toBeTruthy.call(this, 'toBeDisabled', locator, 'Locator', async (isNot, timeout) => { + return toBeTruthy.call(this, 'toBeDisabled', locator, 'Locator', 'disabled', 'enabled', '', async (isNot, timeout) => { return await locator._expect('to.be.disabled', { isNot, timeout }); }, options); } @@ -71,8 +77,11 @@ export function toBeEditable( locator: LocatorEx, options?: { editable?: boolean, timeout?: number }, ) { - return toBeTruthy.call(this, 'toBeEditable', locator, 'Locator', async (isNot, timeout) => { - const editable = !options || options.editable === undefined || options.editable === true; + const editable = !options || options.editable === undefined || options.editable === true; + const expected = editable ? 'editable' : 'readOnly'; + const unexpected = editable ? 'readOnly' : 'editable'; + const arg = editable ? '' : '{ editable: false }'; + return toBeTruthy.call(this, 'toBeEditable', locator, 'Locator', expected, unexpected, arg, async (isNot, timeout) => { return await locator._expect(editable ? 'to.be.editable' : 'to.be.readonly', { isNot, timeout }); }, options); } @@ -82,7 +91,7 @@ export function toBeEmpty( locator: LocatorEx, options?: { timeout?: number }, ) { - return toBeTruthy.call(this, 'toBeEmpty', locator, 'Locator', async (isNot, timeout) => { + return toBeTruthy.call(this, 'toBeEmpty', locator, 'Locator', 'empty', 'notEmpty', '', async (isNot, timeout) => { return await locator._expect('to.be.empty', { isNot, timeout }); }, options); } @@ -92,8 +101,11 @@ export function toBeEnabled( locator: LocatorEx, options?: { enabled?: boolean, timeout?: number }, ) { - return toBeTruthy.call(this, 'toBeEnabled', locator, 'Locator', async (isNot, timeout) => { - const enabled = !options || options.enabled === undefined || options.enabled === true; + const enabled = !options || options.enabled === undefined || options.enabled === true; + const expected = enabled ? 'enabled' : 'disabled'; + const unexpected = enabled ? 'disabled' : 'enabled'; + const arg = enabled ? '' : '{ enabled: false }'; + return toBeTruthy.call(this, 'toBeEnabled', locator, 'Locator', expected, unexpected, arg, async (isNot, timeout) => { return await locator._expect(enabled ? 'to.be.enabled' : 'to.be.disabled', { isNot, timeout }); }, options); } @@ -103,7 +115,7 @@ export function toBeFocused( locator: LocatorEx, options?: { timeout?: number }, ) { - return toBeTruthy.call(this, 'toBeFocused', locator, 'Locator', async (isNot, timeout) => { + return toBeTruthy.call(this, 'toBeFocused', locator, 'Locator', 'focused', 'inactive', '', async (isNot, timeout) => { return await locator._expect('to.be.focused', { isNot, timeout }); }, options); } @@ -113,7 +125,7 @@ export function toBeHidden( locator: LocatorEx, options?: { timeout?: number }, ) { - return toBeTruthy.call(this, 'toBeHidden', locator, 'Locator', async (isNot, timeout) => { + return toBeTruthy.call(this, 'toBeHidden', locator, 'Locator', 'hidden', 'visible', '', async (isNot, timeout) => { return await locator._expect('to.be.hidden', { isNot, timeout }); }, options); } @@ -123,8 +135,11 @@ export function toBeVisible( locator: LocatorEx, options?: { visible?: boolean, timeout?: number }, ) { - return toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', async (isNot, timeout) => { - const visible = !options || options.visible === undefined || options.visible === true; + const visible = !options || options.visible === undefined || options.visible === true; + const expected = visible ? 'visible' : 'hidden'; + const unexpected = visible ? 'hidden' : 'visible'; + const arg = visible ? '' : '{ visible: false }'; + return toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', expected, unexpected, arg, async (isNot, timeout) => { return await locator._expect(visible ? 'to.be.visible' : 'to.be.hidden', { isNot, timeout }); }, options); } @@ -134,7 +149,7 @@ export function toBeInViewport( locator: LocatorEx, options?: { timeout?: number, ratio?: number }, ) { - return toBeTruthy.call(this, 'toBeInViewport', locator, 'Locator', async (isNot, timeout) => { + return toBeTruthy.call(this, 'toBeInViewport', locator, 'Locator', 'in viewport', 'outside viewport', '', async (isNot, timeout) => { return await locator._expect('to.be.in.viewport', { isNot, expectedNumber: options?.ratio, timeout }); }, options); } diff --git a/packages/playwright/src/matchers/toBeTruthy.ts b/packages/playwright/src/matchers/toBeTruthy.ts index 8a4b2aed2a..fe4ad7c73c 100644 --- a/packages/playwright/src/matchers/toBeTruthy.ts +++ b/packages/playwright/src/matchers/toBeTruthy.ts @@ -16,17 +16,22 @@ import { expectTypes, callLogText } from '../util'; import { matcherHint } from './matcherHint'; +import type { MatcherResult } from './matcherHint'; import { currentExpectTimeout } from '../common/globals'; import type { ExpectMatcherContext } from './expect'; +import type { Locator } from 'playwright-core'; export async function toBeTruthy( this: ExpectMatcherContext, matcherName: string, - receiver: any, + receiver: Locator, receiverType: string, + expected: string, + unexpected: string, + arg: string, query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, log?: string[], received?: any, timedOut?: boolean }>, options: { timeout?: number } = {}, -) { +): Promise> { expectTypes(receiver, [receiverType], matcherName); const matcherOptions = { @@ -35,12 +40,13 @@ export async function toBeTruthy( }; const timeout = currentExpectTimeout(options); - const { matches, log, timedOut } = await query(!!this.isNot, timeout); - + const actual = matches ? expected : unexpected; const message = () => { - return matcherHint(this, matcherName, undefined, '', matcherOptions, timedOut ? timeout : undefined) + callLogText(log); + const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined); + const logText = callLogText(log); + return matches ? `${header}Expected: not ${expected}\nReceived: ${expected}${logText}` : + `${header}Expected: ${expected}\nReceived: ${unexpected}${logText}`; }; - - return { message, pass: matches }; + return { locator: receiver, message, pass: matches, actual, name: matcherName, expected }; } diff --git a/packages/playwright/src/matchers/toEqual.ts b/packages/playwright/src/matchers/toEqual.ts index 788afb1548..6067532343 100644 --- a/packages/playwright/src/matchers/toEqual.ts +++ b/packages/playwright/src/matchers/toEqual.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import { expectTypes } from '../util'; -import { callLogText } from '../util'; +import { expectTypes, callLogText } from '../util'; import { matcherHint } from './matcherHint'; +import type { MatcherResult } from './matcherHint'; import { currentExpectTimeout } from '../common/globals'; import type { ExpectMatcherContext } from './expect'; +import type { Locator } from 'playwright-core'; // Omit colon and one or more spaces, so can call getLabelPrinter. const EXPECTED_LABEL = 'Expected'; @@ -30,12 +31,12 @@ const isExpand = (expand?: boolean): boolean => expand !== false; export async function toEqual( this: ExpectMatcherContext, matcherName: string, - receiver: any, + receiver: Locator, receiverType: string, query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>, expected: T, options: { timeout?: number, contains?: boolean } = {}, -) { +): Promise> { expectTypes(receiver, [receiverType], matcherName); const matcherOptions = { @@ -50,15 +51,11 @@ export async function toEqual( const message = pass ? () => - matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) + - '\n\n' + + matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + `Expected: not ${this.utils.printExpected(expected)}\n` + - (this.utils.stringify(expected) !== this.utils.stringify(received) - ? `Received: ${this.utils.printReceived(received)}` - : '') + callLogText(log) + `Received: ${this.utils.printReceived(received)}` + callLogText(log) : () => - matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) + - '\n\n' + + matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + this.utils.printDiffOrStringify( expected, received, @@ -70,5 +67,5 @@ export async function toEqual( // Passing the actual and expected objects so that a custom reporter // could access them, for example in order to display a custom visual diff, // or create a different error message - return { actual: received, expected, message, name: matcherName, pass }; + return { locator: receiver, actual: received, expected, message, name: matcherName, pass }; } diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index 91365b0118..b21fc063aa 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -24,17 +24,19 @@ import { printReceivedStringContainExpectedSubstring } from './expect'; import { matcherHint } from './matcherHint'; +import type { MatcherResult } from './matcherHint'; import { currentExpectTimeout } from '../common/globals'; +import type { Locator } from 'playwright-core'; export async function toMatchText( this: ExpectMatcherContext, matcherName: string, - receiver: any, + receiver: Locator, receiverType: string, query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: string, log?: string[], timedOut?: boolean }>, expected: string | RegExp, options: { timeout?: number, matchSubstring?: boolean } = {}, -) { +): Promise> { expectTypes(receiver, [receiverType], matcherName); const matcherOptions = { @@ -48,7 +50,7 @@ export async function toMatchText( ) { throw new Error( this.utils.matcherErrorMessage( - matcherHint(this, matcherName, undefined, undefined, matcherOptions), + matcherHint(this, receiver, matcherName, receiver, expected, matcherOptions), `${this.utils.EXPECTED_COLOR( 'expected', )} value must be a string or regular expression`, @@ -65,16 +67,14 @@ export async function toMatchText( const message = pass ? () => typeof expected === 'string' - ? matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) + - '\n\n' + + ? matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\n` + `Received string: ${printReceivedStringContainExpectedSubstring( receivedString, receivedString.indexOf(expected), expected.length, )}` + callLogText(log) - : matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) + - '\n\n' + + : matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + `Expected pattern: not ${this.utils.printExpected(expected)}\n` + `Received string: ${printReceivedStringContainExpectedResult( receivedString, @@ -88,8 +88,7 @@ export async function toMatchText( const labelReceived = 'Received string'; return ( - matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) + - '\n\n' + + matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + this.utils.printDiffOrStringify( expected, receivedString, @@ -99,7 +98,14 @@ export async function toMatchText( )) + callLogText(log); }; - return { message, pass }; + return { + locator: receiver, + name: matcherName, + expected, + message, + pass, + actual: received, + }; } export function toExpectedTextValues(items: (string | RegExp)[], options: { matchSubstring?: boolean, normalizeWhiteSpace?: boolean, ignoreCase?: boolean } = {}): ExpectedTextValue[] { diff --git a/tests/page/expect-matcher-result.spec.ts b/tests/page/expect-matcher-result.spec.ts new file mode 100644 index 0000000000..1c2b599a92 --- /dev/null +++ b/tests/page/expect-matcher-result.spec.ts @@ -0,0 +1,247 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { stripAnsi } from '../config/utils'; +import { test, expect } from './pageTest'; + +test('toMatchText-based assertions should have matcher result', async ({ page }) => { + await page.setContent('
Text content
'); + const locator = page.locator('#node'); + + { + const e = await expect(locator).toHaveText(/Text2/, { timeout: 1 }).catch(e => e); + e.matcherResult.message = stripAnsi(e.matcherResult.message); + expect.soft(e.matcherResult).toEqual({ + locator: expect.any(Object), + actual: 'Text content', + expected: /Text2/, + message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`), + name: 'toHaveText', + pass: false, + }); + + expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).toHaveText(expected) + +Locator: locator('#node') +Expected pattern: /Text2/ +Received string: \"Text content\" +Call log`); + + } + + { + const e = await expect(locator).not.toHaveText(/Text/, { timeout: 1 }).catch(e => e); + e.matcherResult.message = stripAnsi(e.matcherResult.message); + expect.soft(e.matcherResult).toEqual({ + locator: expect.any(Object), + actual: 'Text content', + expected: /Text/, + message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveText(expected)`), + name: 'toHaveText', + pass: true, + }); + expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).not.toHaveText(expected) + +Locator: locator('#node') +Expected pattern: not /Text/ +Received string: \"Text content\" +Call log`); + + } + +}); + +test('toBeTruthy-based assertions should have matcher result', async ({ page }) => { + await page.setContent('
Text content
'); + + { + const e = await expect(page.locator('#node2')).toBeVisible({ timeout: 1 }).catch(e => e); + e.matcherResult.message = stripAnsi(e.matcherResult.message); + expect.soft(e.matcherResult).toEqual({ + locator: expect.any(Object), + actual: 'hidden', + expected: 'visible', + message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toBeVisible()`), + name: 'toBeVisible', + pass: false, + }); + + expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).toBeVisible() + +Locator: locator('#node2') +Expected: visible +Received: hidden +Call log`); + + } + + { + const e = await expect(page.locator('#node')).not.toBeVisible({ timeout: 1 }).catch(e => e); + e.matcherResult.message = stripAnsi(e.matcherResult.message); + expect.soft(e.matcherResult).toEqual({ + locator: expect.any(Object), + actual: 'visible', + expected: 'visible', + message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toBeVisible()`), + name: 'toBeVisible', + pass: true, + }); + + expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).not.toBeVisible() + +Locator: locator('#node') +Expected: not visible +Received: visible +Call log`); + + } +}); + +test('toEqual-based assertions should have matcher result', async ({ page }) => { + await page.setContent('
Text content
'); + + { + const e = await expect(page.locator('#node2')).toHaveCount(1, { timeout: 1 }).catch(e => e); + e.matcherResult.message = stripAnsi(e.matcherResult.message); + expect.soft(e.matcherResult).toEqual({ + locator: expect.any(Object), + actual: 0, + expected: 1, + message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveCount(expected)`), + name: 'toHaveCount', + pass: false, + }); + + expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).toHaveCount(expected) + +Locator: locator('#node2') +Expected: 1 +Received: 0 +Call log`); + } + + { + const e = await expect(page.locator('#node')).not.toHaveCount(1, { timeout: 1 }).catch(e => e); + e.matcherResult.message = stripAnsi(e.matcherResult.message); + expect.soft(e.matcherResult).toEqual({ + locator: expect.any(Object), + actual: 1, + expected: 1, + message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveCount(expected)`), + name: 'toHaveCount', + pass: true, + }); + + expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).not.toHaveCount(expected) + +Locator: locator('#node') +Expected: not 1 +Received: 1 +Call log`); + + } +}); + +test('toBeChecked({ checked: false }) should have expected: false', async ({ page }) => { + await page.setContent(` + + + `); + + { + const e = await expect(page.locator('#unchecked')).toBeChecked({ timeout: 1 }).catch(e => e); + e.matcherResult.message = stripAnsi(e.matcherResult.message); + expect.soft(e.matcherResult).toEqual({ + locator: expect.any(Object), + actual: 'unchecked', + expected: 'checked', + message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toBeChecked()`), + name: 'toBeChecked', + pass: false, + }); + + expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).toBeChecked() + +Locator: locator('#unchecked') +Expected: checked +Received: unchecked +Call log`); + + } + + { + const e = await expect(page.locator('#checked')).not.toBeChecked({ timeout: 1 }).catch(e => e); + e.matcherResult.message = stripAnsi(e.matcherResult.message); + expect.soft(e.matcherResult).toEqual({ + locator: expect.any(Object), + actual: 'checked', + expected: 'checked', + message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toBeChecked()`), + name: 'toBeChecked', + pass: true, + }); + + expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).not.toBeChecked() + +Locator: locator('#checked') +Expected: not checked +Received: checked +Call log`); + + } + + { + const e = await expect(page.locator('#checked')).toBeChecked({ checked: false, timeout: 1 }).catch(e => e); + e.matcherResult.message = stripAnsi(e.matcherResult.message); + expect.soft(e.matcherResult).toEqual({ + locator: expect.any(Object), + actual: 'checked', + expected: 'unchecked', + message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toBeChecked({ checked: false })`), + name: 'toBeChecked', + pass: false, + }); + + expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).toBeChecked({ checked: false }) + +Locator: locator('#checked') +Expected: unchecked +Received: checked +Call log`); + + } + + { + const e = await expect(page.locator('#unchecked')).not.toBeChecked({ checked: false, timeout: 1 }).catch(e => e); + e.matcherResult.message = stripAnsi(e.matcherResult.message); + expect.soft(e.matcherResult).toEqual({ + locator: expect.any(Object), + actual: 'unchecked', + expected: 'unchecked', + message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toBeChecked({ checked: false })`), + name: 'toBeChecked', + pass: true, + }); + + expect.soft(stripAnsi(e.toString())).toContain(`Error: Timed out 1ms waiting for expect(locator).not.toBeChecked({ checked: false }) + +Locator: locator('#unchecked') +Expected: not unchecked +Received: unchecked +Call log`); + + } +}); diff --git a/tests/page/expect-timeout.spec.ts b/tests/page/expect-timeout.spec.ts index 887a6583b6..1dd83daca4 100644 --- a/tests/page/expect-timeout.spec.ts +++ b/tests/page/expect-timeout.spec.ts @@ -20,25 +20,25 @@ import { test, expect } from './pageTest'; test('should print timed out error message', async ({ page }) => { await page.setContent('
Text content
'); const error = await expect(page.locator('no-such-thing')).toHaveText('hey', { timeout: 1000 }).catch(e => e); - expect(stripAnsi(error.message)).toContain('Timed out 1000ms waiting for expect(received).toHaveText(expected)'); + expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).toHaveText(expected)`); }); test('should print timed out error message when value does not match', async ({ page }) => { await page.setContent('
Text content
'); const error = await expect(page.locator('div')).toHaveText('hey', { timeout: 1000 }).catch(e => e); - expect(stripAnsi(error.message)).toContain('Timed out 1000ms waiting for expect(received).toHaveText(expected)'); + expect(stripAnsi(error.message)).toContain(`Timed out 1000ms waiting for expect(locator).toHaveText(expected)`); }); test('should print timed out error message with impossible timeout', async ({ page }) => { await page.setContent('
Text content
'); const error = await expect(page.locator('no-such-thing')).toHaveText('hey', { timeout: 1 }).catch(e => e); - expect(stripAnsi(error.message)).toContain('Timed out 1ms waiting for expect(received).toHaveText(expected)'); + expect(stripAnsi(error.message)).toContain(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`); }); test('should print timed out error message when value does not match with impossible timeout', async ({ page }) => { await page.setContent('
Text content
'); const error = await expect(page.locator('div')).toHaveText('hey', { timeout: 1 }).catch(e => e); - expect(stripAnsi(error.message)).toContain('Timed out 1ms waiting for expect(received).toHaveText(expected)'); + expect(stripAnsi(error.message)).toContain(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`); }); test('should not print timed out error message when page closes', async ({ page }) => { diff --git a/tests/page/locator-convenience.spec.ts b/tests/page/locator-convenience.spec.ts index fa58badac8..143bd526d2 100644 --- a/tests/page/locator-convenience.spec.ts +++ b/tests/page/locator-convenience.spec.ts @@ -24,10 +24,10 @@ it('should have a nice preview', async ({ page, server }) => { const check = page.locator('#check'); const text = await inner.evaluateHandle(e => e.firstChild); await page.evaluate(() => 1); // Give them a chance to calculate the preview. - expect(String(outer)).toBe('Locator@#outer'); - expect(String(inner)).toBe('Locator@#outer >> #inner'); - expect(String(text)).toBe('JSHandle@#text=Text,↵more text'); - expect(String(check)).toBe('Locator@#check'); + expect.soft(String(outer)).toBe(`locator('#outer')`); + expect.soft(String(inner)).toBe(`locator('#outer').locator('#inner')`); + expect.soft(String(text)).toBe(`JSHandle@#text=Text,↵more text`); + expect.soft(String(check)).toBe(`locator('#check')`); }); it('getAttribute should work', async ({ page, server }) => { diff --git a/tests/playwright-test/expect-configure.spec.ts b/tests/playwright-test/expect-configure.spec.ts index a43fefaa7d..14b864400a 100644 --- a/tests/playwright-test/expect-configure.spec.ts +++ b/tests/playwright-test/expect-configure.spec.ts @@ -47,11 +47,9 @@ test('should configure message', async ({ runInlineTest }) => { }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(0); - expect(result.output).toContain([ - ` Error: x-foo must be visible`, - ``, - ` Call log:`, - ].join('\n')); + expect(result.output).toContain('Error: x-foo must be visible'); + expect(result.output).toContain(`Timed out 1ms waiting for expect(locator).toBeVisible()`); + expect(result.output).toContain('Call log:'); }); test('should prefer local message', async ({ runInlineTest }) => { @@ -66,11 +64,10 @@ test('should prefer local message', async ({ runInlineTest }) => { }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(0); - expect(result.output).toContain([ - ` Error: overridden`, - ``, - ` Call log:`, - ].join('\n')); + + expect(result.output).toContain('Error: overridden'); + expect(result.output).toContain(`Timed out 1ms waiting for expect(locator).toBeVisible()`); + expect(result.output).toContain('Call log:'); }); test('should configure soft', async ({ runInlineTest }) => { diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index 04f6928635..e5dcd266b5 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -81,8 +81,8 @@ test('should include custom error message', async ({ runInlineTest }) => { expect(result.exitCode).toBe(1); expect(result.passed).toBe(0); expect(result.output).toContain([ - ` Error: one plus one is two!`, - ``, + ` Error: one plus one is two!\n`, + ` expect(received).toEqual(expected) // deep equality\n`, ` Expected: 3`, ` Received: 2`, ].join('\n')); @@ -99,11 +99,10 @@ test('should include custom error message with web-first assertions', async ({ r }); expect(result.exitCode).toBe(1); expect(result.passed).toBe(0); - expect(result.output).toContain([ - ` Error: x-foo must be visible`, - ``, - ` Call log:`, - ].join('\n')); + + expect(result.output).toContain('Error: x-foo must be visible'); + expect(result.output).toContain(`Timed out 1ms waiting for expect(locator).toBeVisible()`); + expect(result.output).toContain('Call log:'); }); test('should work with generic matchers', async ({ runTSC }) => { @@ -627,7 +626,7 @@ test('should print pending operations for toHaveText', async ({ runInlineTest }) expect(result.exitCode).toBe(1); const output = result.output; expect(output).toContain('Pending operations:'); - expect(output).toContain('expect(received).toHaveText(expected)'); + expect(output).toContain(`expect(locator).toHaveText(expected)`); expect(output).toContain('Expected string: "Text"'); expect(output).toContain('Received string: ""'); expect(output).toContain('waiting for locator(\'no-such-thing\')'); @@ -677,7 +676,7 @@ test('should not print timed out error message when test times out', async ({ ru const output = result.output; expect(output).toContain('Test timeout of 3000ms exceeded'); expect(output).not.toContain('Timed out 5000ms waiting for expect'); - expect(output).toContain('Error: expect(received).toHaveText(expected)'); + expect(output).toContain(`Error: expect(locator).toHaveText(expected)`); }); test('should not leak long expect message strings', async ({ runInlineTest }) => { diff --git a/tests/playwright-test/watch.spec.ts b/tests/playwright-test/watch.spec.ts index 9e83f36a64..5239d9fd97 100644 --- a/tests/playwright-test/watch.spec.ts +++ b/tests/playwright-test/watch.spec.ts @@ -600,7 +600,7 @@ test('should run CT on changed deps', async ({ runWatchTest, writeFiles }) => { await testProcess.waitForOutput(`src${path.sep}button.spec.tsx:4:11 › pass`); expect(testProcess.output).not.toContain(`src${path.sep}link.spec.tsx`); - await testProcess.waitForOutput('Error: Timed out 1000ms waiting for expect(received).toHaveText(expected)'); + await testProcess.waitForOutput(`Error: Timed out 1000ms waiting for expect(locator).toHaveText(expected)`); await testProcess.waitForOutput('Waiting for file changes.'); });