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
@@ -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);
});