Skip to content

Commit

Permalink
chore: expose expect error details on TestError
Browse files Browse the repository at this point in the history
  • Loading branch information
yury-s committed Oct 18, 2024
1 parent 29c84a3 commit 2d88d7a
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 40 deletions.
36 changes: 36 additions & 0 deletions docs/src/test-reporter-api/class-testerror.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,54 @@

Information about an error thrown during test execution.

## property: TestError.expected
* since: v1.49
- type: ?<[string]>

Expected value formatted as a human-readable string.

## property: TestError.locator
* since: v1.49
- type: ?<[string]>

Receiver's locator.

## property: TestError.log
* since: v1.49
- type: ?<[Array]<[string]>>

Call log.

## property: TestError.message
* since: v1.10
- type: ?<[string]>

Error message. Set when [Error] (or its subclass) has been thrown.

## property: TestError.matcherName
* since: v1.49
- type: ?<[string]>

Expect matcher name.

## property: TestError.received
* since: v1.49
- type: ?<[string]>

Received value formatted as a human-readable string.

## property: TestError.stack
* since: v1.10
- type: ?<[string]>

Error stack. Set when [Error] (or its subclass) has been thrown.

## property: TestError.timeout
* since: v1.49
- type: ?<[int]>

Timeout in milliseconds, if the error was caused by a timeout..

## property: TestError.value
* since: v1.10
- type: ?<[string]>
Expand Down
4 changes: 4 additions & 0 deletions packages/playwright/src/matchers/matcherHint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export type MatcherResult<E, A> = {
actual?: A;
log?: string[];
timeout?: number;
locator?: string;
printedReceived?: string;
printedExpected?: string;
printedDiff?: string;
};

export class ExpectError extends Error {
Expand Down
22 changes: 20 additions & 2 deletions packages/playwright/src/matchers/toBeTruthy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,29 @@ export async function toBeTruthy(

const timeout = options.timeout ?? this.timeout;
const { matches, log, timedOut, received } = await query(!!this.isNot, timeout);
if (matches === !this.isNot) {
return {
name: matcherName,
message: () => '',
pass: matches,
expected
};
}
const notFound = received === kNoElementsFoundError ? received : undefined;
const actual = matches ? expected : unexpected;
let printedReceived: string | undefined;
let printedExpected: string | undefined;
if (matches) {
printedExpected = `Expected: not ${expected}`;
printedReceived = `Received: ${notFound ? kNoElementsFoundError : expected}`;
} else {
printedExpected = `Expected: ${expected}`;
printedReceived = `Received: ${notFound ? kNoElementsFoundError : 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: ${notFound ? kNoElementsFoundError : expected}${logText}` :
`${header}Expected: ${expected}\nReceived: ${notFound ? kNoElementsFoundError : unexpected}${logText}`;
return `${header}${printedExpected}\n${printedReceived}${logText}`;
};
return {
message,
Expand All @@ -56,5 +72,7 @@ export async function toBeTruthy(
expected,
log,
timeout: timedOut ? timeout : undefined,
...(printedReceived ? { printedReceived } : {}),
...(printedExpected ? { printedExpected } : {}),
};
}
48 changes: 34 additions & 14 deletions packages/playwright/src/matchers/toEqual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,21 +44,38 @@ export async function toEqual<T>(
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 message = pass
? () =>
matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) +
`Expected: not ${this.utils.printExpected(expected)}\n` +
`Received: ${this.utils.printReceived(received)}` + callLogText(log)
: () =>
matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) +
this.utils.printDiffOrStringify(
expected,
received,
EXPECTED_LABEL,
RECEIVED_LABEL,
false,
) + callLogText(log);
let printedReceived: string | undefined;
let printedExpected: string | undefined;
let printedDiff: string | undefined;

if (pass) {
printedExpected = `Expected: not ${this.utils.printExpected(expected)}`;
printedReceived = `Received: ${this.utils.printReceived(received)}`;

} else {
printedDiff = this.utils.printDiffOrStringify(
expected,
received,
EXPECTED_LABEL,
RECEIVED_LABEL,
false,
);
}

Check failure on line 72 in packages/playwright/src/matchers/toEqual.ts

View workflow job for this annotation

GitHub Actions / docs & lint

Expected indentation of 2 spaces but found 0

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

Check failure on line 78 in packages/playwright/src/matchers/toEqual.ts

View workflow job for this annotation

GitHub Actions / docs & lint

Missing semicolon

// 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 All @@ -70,5 +87,8 @@ export async function toEqual<T>(
pass,
log,
timeout: timedOut ? timeout : undefined,
...(printedReceived ? { printedReceived } : {}),
...(printedExpected ? { printedExpected } : {}),
...(printedDiff ? { printedDiff } : {}),
};
}
4 changes: 4 additions & 0 deletions packages/playwright/src/matchers/toMatchSnapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ class SnapshotHelper {
}

createMatcherResult(message: string, pass: boolean, log?: string[]): ImageMatcherResult {

const unfiltered: ImageMatcherResult = {
name: this.matcherName,
expected: this.expectedPath,
Expand All @@ -194,6 +195,9 @@ class SnapshotHelper {
pass,
message: () => message,
log,
...(this.locator ? { locator: this.locator.toString() } : {}),

Check failure on line 198 in packages/playwright/src/matchers/toMatchSnapshot.ts

View workflow job for this annotation

GitHub Actions / docs & lint

'this.locator' will use Object's default stringification format ('[object Object]') when stringified
printedExpected: this.expectedPath,
printedReceived: this.actualPath,
};
return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as ImageMatcherResult;
}
Expand Down
60 changes: 45 additions & 15 deletions packages/playwright/src/matchers/toMatchText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,29 +58,55 @@ 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 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);

let printedReceived: string | undefined;
let printedExpected: string | undefined;
let printedDiff: string | undefined;
if (pass) {
if (typeof expected === 'string') {
if (notFound) {
printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`;
printedReceived = `Received: ${received}`;
} else {
printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`;
const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length);
printedReceived = `Received string: ${formattedReceived}`;
}
} else {
if (notFound) {
printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`;
printedReceived = `Received: ${received}`;
} 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);
printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`;
const formattedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null);
printedReceived = `Received string: ${formattedReceived}`;
}
}
} else {
const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`;
if (notFound) {
printedExpected = `${labelExpected}: ${this.utils.printExpected(expected)}`;
printedReceived = `Received: ${received}`;
} 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);
printedDiff = this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false);
}
}

const message = () => {
const resultDetails = printedDiff ? printedDiff : printedExpected + '\n' + printedReceived;
return messagePrefix + resultDetails + callLogText(log);
};

return {
Expand All @@ -91,5 +117,9 @@ export async function toMatchText(
actual: received,
log,
timeout: timedOut ? timeout : undefined,
locator: receiver.toString(),

Check failure on line 120 in packages/playwright/src/matchers/toMatchText.ts

View workflow job for this annotation

GitHub Actions / docs & lint

'receiver' will use Object's default stringification format ('[object Object]') when stringified
...(printedReceived ? { printedReceived } : {}),
...(printedExpected ? { printedExpected } : {}),
...(printedDiff ? { printedDiff } : {}),
};
}
21 changes: 19 additions & 2 deletions packages/playwright/src/reporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ type ErrorDetails = {
location?: Location;
};

type TestResultErrorDetails = ErrorDetails & {
timeout?: number;
matcherName?: string;
locator?: string;
expected?: string;
received?: string;
log?: string[];
snippet?: string;
};

type TestSummary = {
didNotRun: number;
skipped: number;
Expand Down Expand Up @@ -364,8 +374,8 @@ function quotePathIfNeeded(path: string): string {
return path;
}

export function formatResultFailure(test: TestCase, result: TestResult, initialIndent: string, highlightCode: boolean): ErrorDetails[] {
const errorDetails: ErrorDetails[] = [];
export function formatResultFailure(test: TestCase, result: TestResult, initialIndent: string, highlightCode: boolean): TestResultErrorDetails[] {
const errorDetails: TestResultErrorDetails[] = [];

if (result.status === 'passed' && test.expectedStatus === 'failed') {
errorDetails.push({
Expand All @@ -383,6 +393,13 @@ export function formatResultFailure(test: TestCase, result: TestResult, initialI
errorDetails.push({
message: indent(formattedError.message, initialIndent),
location: formattedError.location,
timeout: error.timeout,
matcherName: error.matcherName,
locator: error.locator,
expected: error.expected,
received: error.received,
log: error.log,
snippet: error.snippet,
});
}
return errorDetails;
Expand Down
7 changes: 4 additions & 3 deletions packages/playwright/src/worker/testInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutMana
import type { RunnableDescription } from './timeoutManager';
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
import type { FullConfig, Location } from '../../types/testReporter';
import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, serializeError, trimLongString, windowsFilesystemFriendlyLength } from '../util';
import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
import { TestTracing } from './testTracing';
import type { Attachment } from './testTracing';
import type { StackFrame } from '@protocol/channels';
import { serializeWorkerError } from './util';

export interface TestStepInternal {
complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void;
Expand Down Expand Up @@ -272,7 +273,7 @@ export class TestInfoImpl implements TestInfo {
if (result.error) {
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
(result.error as any)[stepSymbol] = step;
const error = serializeError(result.error);
const error = serializeWorkerError(result.error);
if (data.boxedStack)
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
step.error = error;
Expand Down Expand Up @@ -330,7 +331,7 @@ export class TestInfoImpl implements TestInfo {
_failWithError(error: Error | unknown) {
if (this.status === 'passed' || this.status === 'skipped')
this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed';
const serialized = serializeError(error);
const serialized = serializeWorkerError(error);
const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
if (step && step.boxedStack)
serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
Expand Down
45 changes: 45 additions & 0 deletions packages/playwright/src/worker/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* 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 type { TestError } from '../../types/testReporter';
import type { TestInfoError } from '../../types/test';
import type { MatcherResult } from '../matchers/matcherHint';
import { serializeError } from '../util';


type MatcherResultDetails = Pick<TestError, 'timeout'|'matcherName'|'locator'|'expected'|'received'|'log'>;

export function serializeWorkerError(error: Error | any): TestInfoError & MatcherResultDetails {
return {
...serializeError(error),
...serializeExpectDetails(error),
};
}

function serializeExpectDetails(e: Error): MatcherResultDetails {
const matcherResult = (e as any).matcherResult as MatcherResult<unknown, unknown>;
if (!matcherResult)
return {};
return {
timeout: matcherResult.timeout,
matcherName: matcherResult.name,
locator: matcherResult.locator,
expected: matcherResult.printedExpected,
received: matcherResult.printedReceived,
log: matcherResult.log,
};
}

Loading

0 comments on commit 2d88d7a

Please sign in to comment.