diff --git a/docs/src/test-api/class-testinfoerror.md b/docs/src/test-api/class-testinfoerror.md index d5e3cb1cf207a..e5b81d2bb3120 100644 --- a/docs/src/test-api/class-testinfoerror.md +++ b/docs/src/test-api/class-testinfoerror.md @@ -6,16 +6,22 @@ Information about an error thrown during test execution. ## property: TestInfoError.actual * since: v1.49 -- type: ?<[any]> +- type: ?<[string]> Actual value. ## property: TestInfoError.expected * since: v1.49 -- type: ?<[any]> +- type: ?<[string]> Expected value. +## property: TestInfoError.locator +* since: v1.49 +- type: ?<[string]> + +Receiver's locator. + ## property: TestInfoError.log * since: v1.49 - type: ?<[Array]<[string]>> diff --git a/docs/src/test-reporter-api/class-testerror.md b/docs/src/test-reporter-api/class-testerror.md index e35e51c43371b..1bcb3b5debfd5 100644 --- a/docs/src/test-reporter-api/class-testerror.md +++ b/docs/src/test-reporter-api/class-testerror.md @@ -6,16 +6,22 @@ Information about an error thrown during test execution. ## property: TestError.actual * since: v1.49 -- type: ?<[any]> +- type: ?<[string]> Actual value. ## property: TestError.expected * since: v1.49 -- type: ?<[any]> +- type: ?<[string]> Expected value. +## property: TestError.locator +* since: v1.49 +- type: ?<[string]> + +Receiver's locator. + ## property: TestError.log * since: v1.49 - type: ?<[Array]<[string]>> diff --git a/packages/html-reporter/src/testErrorView.tsx b/packages/html-reporter/src/testErrorView.tsx index 8d2bb13bd3065..435d04fd10ff8 100644 --- a/packages/html-reporter/src/testErrorView.tsx +++ b/packages/html-reporter/src/testErrorView.tsx @@ -19,22 +19,50 @@ import * as React from 'react'; import './testErrorView.css'; import type { ImageDiff } from '@web/shared/imageDiffView'; import { ImageDiffView } from '@web/shared/imageDiffView'; +import { ErrorDetails } from './types'; export const TestErrorView: React.FC<{ - error: string; + error: ErrorDetails; testId?: string; }> = ({ error, testId }) => { - const html = React.useMemo(() => ansiErrorToHtml(error), [error]); + const html = React.useMemo(() => { + const formattedError = []; + if (error.shortMessage) + formattedError.push('Error: ' + error.shortMessage); + if (error.locator) + formattedError.push(`Locator: ${error.locator}`); + if (error.expected) + formattedError.push(`Expected: ${error.expected}`); + if (error.actual) + formattedError.push(`Received: ${error.actual}`); + // if (error.diff) + if (error.log) { + formattedError.push('Call log:'); + formattedError.push(...(error.log?.map(line => ' - ' + line) || [])); + } + if (error.snippet) + formattedError.push('', error.snippet); + return ansiErrorToHtml(formattedError.join('\n')); + }, [error]); + return
; }; export const TestScreenshotErrorView: React.FC<{ - errorPrefix?: string, + error: ErrorDetails, diff: ImageDiff, - errorSuffix?: string, -}> = ({ errorPrefix, diff, errorSuffix }) => { - const prefixHtml = React.useMemo(() => ansiErrorToHtml(errorPrefix), [errorPrefix]); - const suffixHtml = React.useMemo(() => ansiErrorToHtml(errorSuffix), [errorSuffix]); +}> = ({ error, diff }) => { + const prefixHtml = React.useMemo(() => ansiErrorToHtml(error.shortMessage), [error]); + const suffixHtml = React.useMemo(() => { + const errorSuffix = ['Call log:', + ...(error.log?.map(line => ' - ' + line) || []), + '', + error.snippet, + '', + error.callStack, + ].join('\n'); + return ansiErrorToHtml(errorSuffix) + }, [error]); return
@@ -73,3 +101,13 @@ const ansiColors = { function escapeHTML(text: string): string { return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!)); } + +export function formatCallLog(log: string[] | undefined): string { + if (!log || !log.some(l => !!l)) + return ''; + return ` +Call log: + ${'- ' + (log || []).join('\n - ')} +`; +} + diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index f6cc86d1c6ad9..686bd4afa3be5 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -98,7 +98,7 @@ export const TestResultView: React.FC<{ {!!errors.length && {errors.map((error, index) => { if (error.type === 'screenshot') - return ; + return ; return ; })} } @@ -155,25 +155,18 @@ function classifyErrors(testErrors: ErrorDetails[], diffs: ImageDiff[]) { if (error.shortMessage?.includes('Screenshot comparison failed:') && error.actual && error.expected) { const matchingDiff = diffs.find(diff => { const attachmentName = diff.actual?.attachment.name; - return attachmentName && error.actual.endsWith(attachmentName); + return attachmentName && error.actual?.endsWith(attachmentName); }); - const errorSuffix = ['Call log:', - ...(error.log?.map(line => ' - ' + line) || []), - '', - error.snippet, - '', - error.callStack, - ].join('\n'); - if (matchingDiff) { - return { - type: 'screenshot', - diff: matchingDiff, - errorPrefix: error.shortMessage, - errorSuffix - }; - } + return { + type: 'screenshot', + diff: matchingDiff, + error, + }; } - return { type: 'regular', error: error.message }; + return { + type: 'regular', + error + }; }); } diff --git a/packages/html-reporter/src/types.ts b/packages/html-reporter/src/types.ts index ad06ba1b6e9dd..f068132f2309a 100644 --- a/packages/html-reporter/src/types.ts +++ b/packages/html-reporter/src/types.ts @@ -87,9 +87,10 @@ export type ErrorDetails = { message: string; location?: Location; shortMessage?: string; + locator?: string; log?: string[]; - expected?: any; - actual?: any; + expected?: string; + actual?: string; snippet?: string; callStack?: string; }; diff --git a/packages/playwright/bundles/expect/third_party/matchers.ts b/packages/playwright/bundles/expect/third_party/matchers.ts index 5102b50c3262c..5cb7ed4ddbdd1 100644 --- a/packages/playwright/bundles/expect/third_party/matchers.ts +++ b/packages/playwright/bundles/expect/third_party/matchers.ts @@ -135,10 +135,18 @@ const matchers: MatchersObject = { ); }; + const header = matcherHint(matcherName, undefined, undefined, options) + '\n'; + const printedExpected = `${pass ? 'not ' : ''}${printExpected(expected)}` + const printedReceived = printReceived(received); + // 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 { actual: received, expected, message, name: matcherName, pass, + header, + printedReceived, + printedExpected, + }; }, toBeCloseTo(received: number, expected: number, precision = 2) { diff --git a/packages/playwright/src/matchers/matcherHint.ts b/packages/playwright/src/matchers/matcherHint.ts index dc29f2dab4cd7..a5df573a3115a 100644 --- a/packages/playwright/src/matchers/matcherHint.ts +++ b/packages/playwright/src/matchers/matcherHint.ts @@ -40,6 +40,12 @@ export type MatcherResult = { actual?: A; log?: string[]; timeout?: number; + + locator?: string; + header?: string; + printedReceived?: string; + printedExpected?: string; + }; export class ExpectError extends Error { diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index ebac8f80283cb..70e7c4dd0b422 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -58,30 +58,35 @@ export async function toMatchText( const timeout = options.timeout ?? this.timeout; const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout); + if (pass === !this.isNot) { + return { + name: matcherName, + message: () => '', + pass, + expected + }; + } + const stringSubstring = options.matchSubstring ? 'substring' : 'string'; - const receivedString = received || ''; - const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); + const headerWithLocator = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); + // Pass timeout and locator as fields on the error? + const header = matcherHint(this, undefined, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); const notFound = received === kNoElementsFoundError; - const message = () => { - if (pass) { - if (typeof expected === 'string') { - if (notFound) - return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); - const printedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length); - return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log); - } else { - if (notFound) - return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); - const printedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null); - return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log); - } - } else { - const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`; - if (notFound) - return messagePrefix + `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); - return messagePrefix + this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false) + callLogText(log); - } - }; + let printedReceived; + // {not} {string/substring/pattern} {formatted} + let printedExpected = `${typeof expected === 'string' ? stringSubstring : 'pattern'} ${this.utils.printExpected(expected)}`; + if (pass) { + printedExpected = `not ${printedExpected}`; + const receivedString = received || ''; + printedReceived = notFound + ? received + : typeof expected === 'string' + ? printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length) + : printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null); + } else { + printedReceived = notFound ? received : `string ${this.utils.printReceived(received)}`; + } + const message = () => `${headerWithLocator}Expected: ${printedExpected}\nReceived: ${printedReceived}` + callLogText(log); return { name: matcherName, @@ -91,5 +96,10 @@ export async function toMatchText( actual: received, log, timeout: timedOut ? timeout : undefined, + + locator: receiver.toString(), + header, + printedReceived, + printedExpected, }; } diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index f27b803389b19..6bbe296ee7fd4 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -38,9 +38,9 @@ type ErrorDetails = { type TestResultErrorDetails = ErrorDetails & { shortMessage?: string; log?: string[]; - expected?: any; - actual?: any; - + locator?: string; + expected?: string; + actual?: string; snippet?: string; stack?: string; }; @@ -396,6 +396,7 @@ export function formatResultFailure(test: TestCase, result: TestResult, initialI location: formattedError.location, shortMessage: error.shortMessage, log: error.log, + locator: error.locator, expected: error.expected, actual: error.actual, snippet: error.snippet, diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index 3cc4024e7106f..25b91104c999b 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -43,15 +43,16 @@ export function filterStackTrace(e: Error): Omit { }; } -function filterExpectDetails(e: Error): Pick { +function filterExpectDetails(e: Error): Pick { const matcherResult = (e as any).matcherResult as MatcherResult; if (!matcherResult) return {}; return { - shortMessage: matcherResult.shortMessage, + shortMessage: matcherResult.header, log: matcherResult.log, - expected: matcherResult.expected, - actual: matcherResult.actual, + locator: matcherResult.locator, + expected: matcherResult.printedExpected, + actual: matcherResult.printedReceived, }; } diff --git a/packages/playwright/src/worker/util.ts b/packages/playwright/src/worker/util.ts index e421c56c0cfe1..670a21341fe07 100644 --- a/packages/playwright/src/worker/util.ts +++ b/packages/playwright/src/worker/util.ts @@ -25,15 +25,16 @@ export function serializeWorkerError(error: Error | any): TestInfoError { }; } -function serializeExpectDetails(e: Error): Pick { +function serializeExpectDetails(e: Error): Pick { const matcherResult = (e as any).matcherResult as MatcherResult; if (!matcherResult) return {}; return { - shortMessage: matcherResult.shortMessage, + shortMessage: matcherResult.header, log: matcherResult.log, - expected: matcherResult.expected, - actual: matcherResult.actual, + expected: matcherResult.printedExpected, + actual: matcherResult.printedReceived, + locator: matcherResult.locator, }; } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 043e11021a015..86f2369db448d 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -9152,12 +9152,17 @@ export interface TestInfoError { /** * Actual value. */ - actual?: any; + actual?: string; /** * Expected value. */ - expected?: any; + expected?: string; + + /** + * Receiver's locator. + */ + locator?: string; /** * Call log. diff --git a/packages/playwright/types/testReporter.d.ts b/packages/playwright/types/testReporter.d.ts index 6ab3f7d77f16f..b78305d8e54d0 100644 --- a/packages/playwright/types/testReporter.d.ts +++ b/packages/playwright/types/testReporter.d.ts @@ -557,18 +557,23 @@ export interface TestError { /** * Actual value. */ - actual?: any; + actual?: string; /** * Expected value. */ - expected?: any; + expected?: string; /** * Error location in the source code. */ location?: Location; + /** + * Receiver's locator. + */ + locator?: string; + /** * Call log. */ diff --git a/tests/page/page-check.spec.ts b/tests/page/page-check.spec.ts index 01b00ddc55c90..1f9235b0252d7 100644 --- a/tests/page/page-check.spec.ts +++ b/tests/page/page-check.spec.ts @@ -18,7 +18,10 @@ import { test as it, expect } from './pageTest'; it('should check the box @smoke', async ({ page }) => { - await page.setContent(``); + await page.setContent(`
foo
`); + // expect(['fobao', 'bar']).toBe(expect.arrayContaining([expect.stringContaining('oba'), expect.stringContaining('bar')])); + expect('fobaro').toBe('bar'); + await expect(page.locator('div')).toHaveText('fobaro', { timeout: 500 }); await page.check('input'); expect(await page.evaluate(() => window['checkbox'].checked)).toBe(true); });