From 9d16a61c1dea0e04739f2e442ac91c623ccdbb6f Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 22 Mar 2024 15:56:48 +0100 Subject: [PATCH] fix(expect): throw better received error when no element was found --- packages/playwright-core/src/server/frames.ts | 2 ++ .../src/server/injected/injectedScript.ts | 2 +- .../playwright/src/matchers/matcherHint.ts | 1 + .../playwright/src/matchers/toBeTruthy.ts | 6 ++-- .../playwright/src/matchers/toMatchText.ts | 4 +-- tests/page/expect-matcher-result.spec.ts | 2 +- tests/page/expect-to-have-text.spec.ts | 2 +- tests/page/matchers.misc.spec.ts | 34 +++++++++++++++++++ tests/playwright-test/expect.spec.ts | 2 +- 9 files changed, 46 insertions(+), 9 deletions(-) diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index fb4dcee705..bf28adb99f 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -1382,6 +1382,8 @@ export class Frame extends SdkObject { // Library mode special case for the expect errors which are return values, not exceptions. if (result.matches === options.isNot) metadata.error = { error: { name: 'Expect', message: 'Expect failed' } }; + if (result.log?.[result.log.length - 1].startsWith('waiting for ')) + result.received = ''; return result; } diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 8a73b9e423..de3c13c1b2 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -1166,7 +1166,7 @@ export class InjectedScript { throw this.createStacklessError('Element is not a checkbox'); if (elementState === 'error:notconnected') throw this.createStacklessError('Element is not connected'); - return { received: elementState, matches: elementState }; + return { matches: elementState }; } } diff --git a/packages/playwright/src/matchers/matcherHint.ts b/packages/playwright/src/matchers/matcherHint.ts index 17d893431c..09232db4ea 100644 --- a/packages/playwright/src/matchers/matcherHint.ts +++ b/packages/playwright/src/matchers/matcherHint.ts @@ -20,6 +20,7 @@ import type { Locator } from 'playwright-core'; import type { StackFrame } from '@protocol/channels'; import { stringifyStackFrames } from 'playwright-core/lib/utils'; + 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) diff --git a/packages/playwright/src/matchers/toBeTruthy.ts b/packages/playwright/src/matchers/toBeTruthy.ts index 26f01ad950..d22995ad97 100644 --- a/packages/playwright/src/matchers/toBeTruthy.ts +++ b/packages/playwright/src/matchers/toBeTruthy.ts @@ -40,13 +40,13 @@ export async function toBeTruthy( }; const timeout = currentExpectTimeout(options); - const { matches, log, timedOut } = await query(!!this.isNot, timeout); + const { matches, log, timedOut, received } = await query(!!this.isNot, timeout); const actual = matches ? expected : unexpected; const message = () => { 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 matches ? `${header}Expected: not ${expected}\nReceived: ${received ?? expected}${logText}` : + `${header}Expected: ${expected}\nReceived: ${received ?? unexpected}${logText}`; }; return { message, diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index 5e8145db9c..df95727250 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -69,11 +69,11 @@ export async function toMatchText( typeof expected === 'string' ? matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\n` + - `Received string: ${printReceivedStringContainExpectedSubstring( + `Received string: ${receivedString.indexOf(expected) !== -1 ? printReceivedStringContainExpectedSubstring( receivedString, receivedString.indexOf(expected), expected.length, - )}` + callLogText(log) + ) : '"' + receivedString + '"'}` + callLogText(log) : matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + `Expected pattern: not ${this.utils.printExpected(expected)}\n` + `Received string: ${printReceivedStringContainExpectedResult( diff --git a/tests/page/expect-matcher-result.spec.ts b/tests/page/expect-matcher-result.spec.ts index fc57b51292..8f8a83bc83 100644 --- a/tests/page/expect-matcher-result.spec.ts +++ b/tests/page/expect-matcher-result.spec.ts @@ -86,7 +86,7 @@ test('toBeTruthy-based assertions should have matcher result', async ({ page }) Locator: locator('#node2') Expected: visible -Received: hidden +Received: Call log`); } diff --git a/tests/page/expect-to-have-text.spec.ts b/tests/page/expect-to-have-text.spec.ts index 12a11d1541..0018cfca0d 100644 --- a/tests/page/expect-to-have-text.spec.ts +++ b/tests/page/expect-to-have-text.spec.ts @@ -158,7 +158,7 @@ test.describe('not.toHaveText', () => { await page.setContent('
hello
'); const error = await expect(page.locator('span')).not.toHaveText('hello', { timeout: 1000 }).catch(e => e); expect(stripAnsi(error.message)).toContain('Expected string: not "hello"'); - expect(stripAnsi(error.message)).toContain('Received string: ""'); + expect(stripAnsi(error.message)).toContain('Received string: ""'); expect(stripAnsi(error.message)).toContain('waiting for locator(\'span\')'); }); }); diff --git a/tests/page/matchers.misc.spec.ts b/tests/page/matchers.misc.spec.ts index 360549177c..f2693ecc5c 100644 --- a/tests/page/matchers.misc.spec.ts +++ b/tests/page/matchers.misc.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { stripAnsi } from '../config/utils'; import { test as it, expect } from './pageTest'; it('should outlive frame navigation', async ({ page, server }) => { @@ -23,3 +24,36 @@ it('should outlive frame navigation', async ({ page, server }) => { }, 1000); await expect(page.locator('.box').first()).toBeEmpty(); }); + +it('should print no-locator-resolved error when locator matcher did not resolve to any element', async ({ page, server }) => { + const myLocator = page.locator('.nonexisting'); + const expectWithShortLivingTimeout = expect.configure({ timeout: 10 }); + const locatorMatchers = [ + () => expectWithShortLivingTimeout(myLocator).toBeAttached(), + () => expectWithShortLivingTimeout(myLocator).toBeChecked(), + () => expectWithShortLivingTimeout(myLocator).toBeDisabled(), + () => expectWithShortLivingTimeout(myLocator).toBeEditable(), + () => expectWithShortLivingTimeout(myLocator).toBeEmpty(), + () => expectWithShortLivingTimeout(myLocator).toBeEnabled(), + () => expectWithShortLivingTimeout(myLocator).toBeFocused(), + () => expectWithShortLivingTimeout(myLocator).toBeInViewport(), + () => expectWithShortLivingTimeout(myLocator).toBeVisible(), + () => expectWithShortLivingTimeout(myLocator).toContainText('abc'), + () => expectWithShortLivingTimeout(myLocator).toHaveAttribute('abc'), + () => expectWithShortLivingTimeout(myLocator).toHaveClass('abc'), + () => expectWithShortLivingTimeout(myLocator).toHaveCSS('abc', 'abc'), + () => expectWithShortLivingTimeout(myLocator).toHaveId('abc'), + () => expectWithShortLivingTimeout(myLocator).toHaveJSProperty('abc', 'abc'), + () => expectWithShortLivingTimeout(myLocator).toHaveText('abc'), + () => expectWithShortLivingTimeout(myLocator).toHaveValue('abc'), + () => expectWithShortLivingTimeout(myLocator).toHaveValues(['abc']), + ]; + for (const matcher of locatorMatchers) { + await it.step(matcher.toString(), async () => { + const error = await matcher().catch(e => e); + expect(error).toBeInstanceOf(Error); + expect(error.message).toContain(`waiting for locator('.nonexisting')`); + expect(stripAnsi(error.message)).toMatch(/Received( string)?: "?/); + }); + } +}); diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index 9a564532ef..d0ca255eed 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -647,7 +647,7 @@ test('should print pending operations for toHaveText', async ({ runInlineTest }) const output = result.output; expect(output).toContain(`expect(locator).toHaveText(expected)`); expect(output).toContain('Expected string: "Text"'); - expect(output).toContain('Received string: ""'); + expect(output).toContain('Received string: ""'); expect(output).toContain('waiting for locator(\'no-such-thing\')'); });