chore: expose expect error details on TestError (#33183)
This commit is contained in:
parent
36d3a6764e
commit
aebceb345e
|
|
@ -4,18 +4,54 @@
|
||||||
|
|
||||||
Information about an error thrown during test execution.
|
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.matcherName
|
||||||
|
* since: v1.49
|
||||||
|
- type: ?<[string]>
|
||||||
|
|
||||||
|
Expect matcher name.
|
||||||
|
|
||||||
## property: TestError.message
|
## property: TestError.message
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
||||||
Error message. Set when [Error] (or its subclass) has been thrown.
|
Error message. Set when [Error] (or its subclass) has been thrown.
|
||||||
|
|
||||||
|
## property: TestError.received
|
||||||
|
* since: v1.49
|
||||||
|
- type: ?<[string]>
|
||||||
|
|
||||||
|
Received value formatted as a human-readable string.
|
||||||
|
|
||||||
## property: TestError.stack
|
## property: TestError.stack
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
||||||
Error stack. Set when [Error] (or its subclass) has been thrown.
|
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
|
## property: TestError.value
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@ export type MatcherResult<E, A> = {
|
||||||
actual?: A;
|
actual?: A;
|
||||||
log?: string[];
|
log?: string[];
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
locator?: string;
|
||||||
|
printedReceived?: string;
|
||||||
|
printedExpected?: string;
|
||||||
|
printedDiff?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ExpectError extends Error {
|
export class ExpectError extends Error {
|
||||||
|
|
|
||||||
|
|
@ -39,22 +39,41 @@ export async function toBeTruthy(
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeout = options.timeout ?? this.timeout;
|
const timeout = options.timeout ?? this.timeout;
|
||||||
const { matches, log, timedOut, received } = await query(!!this.isNot, timeout);
|
const { matches: pass, log, timedOut, received } = await query(!!this.isNot, timeout);
|
||||||
|
if (pass === !this.isNot) {
|
||||||
|
return {
|
||||||
|
name: matcherName,
|
||||||
|
message: () => '',
|
||||||
|
pass,
|
||||||
|
expected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const notFound = received === kNoElementsFoundError ? received : undefined;
|
const notFound = received === kNoElementsFoundError ? received : undefined;
|
||||||
const actual = matches ? expected : unexpected;
|
const actual = pass ? expected : unexpected;
|
||||||
|
let printedReceived: string | undefined;
|
||||||
|
let printedExpected: string | undefined;
|
||||||
|
if (pass) {
|
||||||
|
printedExpected = `Expected: not ${expected}`;
|
||||||
|
printedReceived = `Received: ${notFound ? kNoElementsFoundError : expected}`;
|
||||||
|
} else {
|
||||||
|
printedExpected = `Expected: ${expected}`;
|
||||||
|
printedReceived = `Received: ${notFound ? kNoElementsFoundError : unexpected}`;
|
||||||
|
}
|
||||||
const message = () => {
|
const message = () => {
|
||||||
const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined);
|
const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined);
|
||||||
const logText = callLogText(log);
|
const logText = callLogText(log);
|
||||||
return matches ? `${header}Expected: not ${expected}\nReceived: ${notFound ? kNoElementsFoundError : expected}${logText}` :
|
return `${header}${printedExpected}\n${printedReceived}${logText}`;
|
||||||
`${header}Expected: ${expected}\nReceived: ${notFound ? kNoElementsFoundError : unexpected}${logText}`;
|
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
message,
|
message,
|
||||||
pass: matches,
|
pass,
|
||||||
actual,
|
actual,
|
||||||
name: matcherName,
|
name: matcherName,
|
||||||
expected,
|
expected,
|
||||||
log,
|
log,
|
||||||
timeout: timedOut ? timeout : undefined,
|
timeout: timedOut ? timeout : undefined,
|
||||||
|
...(printedReceived ? { printedReceived } : {}),
|
||||||
|
...(printedExpected ? { printedExpected } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,22 +44,35 @@ export async function toEqual<T>(
|
||||||
const timeout = options.timeout ?? this.timeout;
|
const timeout = options.timeout ?? this.timeout;
|
||||||
|
|
||||||
const { matches: pass, received, log, timedOut } = await query(!!this.isNot, 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
|
let printedReceived: string | undefined;
|
||||||
? () =>
|
let printedExpected: string | undefined;
|
||||||
matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) +
|
let printedDiff: string | undefined;
|
||||||
`Expected: not ${this.utils.printExpected(expected)}\n` +
|
if (pass) {
|
||||||
`Received: ${this.utils.printReceived(received)}` + callLogText(log)
|
printedExpected = `Expected: not ${this.utils.printExpected(expected)}`;
|
||||||
: () =>
|
printedReceived = `Received: ${this.utils.printReceived(received)}`;
|
||||||
matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) +
|
} else {
|
||||||
this.utils.printDiffOrStringify(
|
printedDiff = this.utils.printDiffOrStringify(
|
||||||
expected,
|
expected,
|
||||||
received,
|
received,
|
||||||
EXPECTED_LABEL,
|
EXPECTED_LABEL,
|
||||||
RECEIVED_LABEL,
|
RECEIVED_LABEL,
|
||||||
false,
|
false,
|
||||||
) + callLogText(log);
|
);
|
||||||
|
}
|
||||||
|
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)}`;
|
||||||
|
};
|
||||||
// Passing the actual and expected objects so that a custom reporter
|
// Passing the actual and expected objects so that a custom reporter
|
||||||
// could access them, for example in order to display a custom visual diff,
|
// could access them, for example in order to display a custom visual diff,
|
||||||
// or create a different error message
|
// or create a different error message
|
||||||
|
|
@ -70,5 +83,8 @@ export async function toEqual<T>(
|
||||||
pass,
|
pass,
|
||||||
log,
|
log,
|
||||||
timeout: timedOut ? timeout : undefined,
|
timeout: timedOut ? timeout : undefined,
|
||||||
|
...(printedReceived ? { printedReceived } : {}),
|
||||||
|
...(printedExpected ? { printedExpected } : {}),
|
||||||
|
...(printedDiff ? { printedDiff } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,10 @@ class SnapshotHelper {
|
||||||
pass,
|
pass,
|
||||||
message: () => message,
|
message: () => message,
|
||||||
log,
|
log,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
|
...(this.locator ? { locator: this.locator.toString() } : {}),
|
||||||
|
printedExpected: this.expectedPath,
|
||||||
|
printedReceived: this.actualPath,
|
||||||
};
|
};
|
||||||
return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as ImageMatcherResult;
|
return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as ImageMatcherResult;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,29 +58,56 @@ export async function toMatchText(
|
||||||
const timeout = options.timeout ?? this.timeout;
|
const timeout = options.timeout ?? this.timeout;
|
||||||
|
|
||||||
const { matches: pass, received, log, timedOut } = await query(!!this.isNot, 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 stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
||||||
const receivedString = received || '';
|
const receivedString = received || '';
|
||||||
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
|
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
|
||||||
const notFound = received === kNoElementsFoundError;
|
const notFound = received === kNoElementsFoundError;
|
||||||
const message = () => {
|
|
||||||
if (pass) {
|
let printedReceived: string | undefined;
|
||||||
if (typeof expected === 'string') {
|
let printedExpected: string | undefined;
|
||||||
if (notFound)
|
let printedDiff: string | undefined;
|
||||||
return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log);
|
if (pass) {
|
||||||
const printedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length);
|
if (typeof expected === 'string') {
|
||||||
return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log);
|
if (notFound) {
|
||||||
|
printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`;
|
||||||
|
printedReceived = `Received: ${received}`;
|
||||||
} else {
|
} else {
|
||||||
if (notFound)
|
printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`;
|
||||||
return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log);
|
const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length);
|
||||||
const printedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null);
|
printedReceived = `Received string: ${formattedReceived}`;
|
||||||
return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`;
|
if (notFound) {
|
||||||
if (notFound)
|
printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`;
|
||||||
return messagePrefix + `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log);
|
printedReceived = `Received: ${received}`;
|
||||||
return messagePrefix + this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false) + callLogText(log);
|
} else {
|
||||||
|
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 {
|
||||||
|
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 {
|
return {
|
||||||
|
|
@ -91,5 +118,10 @@ export async function toMatchText(
|
||||||
actual: received,
|
actual: received,
|
||||||
log,
|
log,
|
||||||
timeout: timedOut ? timeout : undefined,
|
timeout: timedOut ? timeout : undefined,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
|
locator: receiver.toString(),
|
||||||
|
...(printedReceived ? { printedReceived } : {}),
|
||||||
|
...(printedExpected ? { printedExpected } : {}),
|
||||||
|
...(printedDiff ? { printedDiff } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,13 @@ type Annotation = {
|
||||||
type ErrorDetails = {
|
type ErrorDetails = {
|
||||||
message: string;
|
message: string;
|
||||||
location?: Location;
|
location?: Location;
|
||||||
|
timeout?: number;
|
||||||
|
matcherName?: string;
|
||||||
|
locator?: string;
|
||||||
|
expected?: string;
|
||||||
|
received?: string;
|
||||||
|
log?: string[];
|
||||||
|
snippet?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TestSummary = {
|
type TestSummary = {
|
||||||
|
|
@ -383,6 +390,13 @@ export function formatResultFailure(test: TestCase, result: TestResult, initialI
|
||||||
errorDetails.push({
|
errorDetails.push({
|
||||||
message: indent(formattedError.message, initialIndent),
|
message: indent(formattedError.message, initialIndent),
|
||||||
location: formattedError.location,
|
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;
|
return errorDetails;
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,11 @@ import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutMana
|
||||||
import type { RunnableDescription } from './timeoutManager';
|
import type { RunnableDescription } from './timeoutManager';
|
||||||
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
|
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
|
||||||
import type { FullConfig, Location } from '../../types/testReporter';
|
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 { TestTracing } from './testTracing';
|
||||||
import type { Attachment } from './testTracing';
|
import type { Attachment } from './testTracing';
|
||||||
import type { StackFrame } from '@protocol/channels';
|
import type { StackFrame } from '@protocol/channels';
|
||||||
|
import { serializeWorkerError } from './util';
|
||||||
|
|
||||||
export interface TestStepInternal {
|
export interface TestStepInternal {
|
||||||
complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void;
|
complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void;
|
||||||
|
|
@ -272,7 +273,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
|
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
|
||||||
(result.error as any)[stepSymbol] = step;
|
(result.error as any)[stepSymbol] = step;
|
||||||
const error = serializeError(result.error);
|
const error = serializeWorkerError(result.error);
|
||||||
if (data.boxedStack)
|
if (data.boxedStack)
|
||||||
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
|
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
|
||||||
step.error = error;
|
step.error = error;
|
||||||
|
|
@ -330,7 +331,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
_failWithError(error: Error | unknown) {
|
_failWithError(error: Error | unknown) {
|
||||||
if (this.status === 'passed' || this.status === 'skipped')
|
if (this.status === 'passed' || this.status === 'skipped')
|
||||||
this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed';
|
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;
|
const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
|
||||||
if (step && step.boxedStack)
|
if (step && step.boxedStack)
|
||||||
serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
|
serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
|
||||||
|
|
|
||||||
45
packages/playwright/src/worker/util.ts
Normal file
45
packages/playwright/src/worker/util.ts
Normal file
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||||
import { debugTest, relativeFilePath, serializeError } from '../util';
|
import { debugTest, relativeFilePath } from '../util';
|
||||||
import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc';
|
import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc';
|
||||||
import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
|
import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
|
||||||
import { deserializeConfig } from '../common/configLoader';
|
import { deserializeConfig } from '../common/configLoader';
|
||||||
|
|
@ -32,6 +32,7 @@ import type { TestInfoError } from '../../types/test';
|
||||||
import type { Location } from '../../types/testReporter';
|
import type { Location } from '../../types/testReporter';
|
||||||
import { inheritFixtureNames } from '../common/fixtures';
|
import { inheritFixtureNames } from '../common/fixtures';
|
||||||
import { type TimeSlot } from './timeoutManager';
|
import { type TimeSlot } from './timeoutManager';
|
||||||
|
import { serializeWorkerError } from './util';
|
||||||
|
|
||||||
export class WorkerMain extends ProcessRunner {
|
export class WorkerMain extends ProcessRunner {
|
||||||
private _params: WorkerInitParams;
|
private _params: WorkerInitParams;
|
||||||
|
|
@ -112,7 +113,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
|
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
|
||||||
this._fatalErrors.push(...fakeTestInfo.errors);
|
this._fatalErrors.push(...fakeTestInfo.errors);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._fatalErrors.push(serializeError(e));
|
this._fatalErrors.push(serializeWorkerError(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._fatalErrors.length) {
|
if (this._fatalErrors.length) {
|
||||||
|
|
@ -153,7 +154,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
// No current test - fatal error.
|
// No current test - fatal error.
|
||||||
if (!this._currentTest) {
|
if (!this._currentTest) {
|
||||||
if (!this._fatalErrors.length)
|
if (!this._fatalErrors.length)
|
||||||
this._fatalErrors.push(serializeError(error));
|
this._fatalErrors.push(serializeWorkerError(error));
|
||||||
void this._stop();
|
void this._stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +225,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
// In theory, we should run above code without any errors.
|
// In theory, we should run above code without any errors.
|
||||||
// However, in the case we screwed up, or loadTestFile failed in the worker
|
// However, in the case we screwed up, or loadTestFile failed in the worker
|
||||||
// but not in the runner, let's do a fatal error.
|
// but not in the runner, let's do a fatal error.
|
||||||
this._fatalErrors.push(serializeError(e));
|
this._fatalErrors.push(serializeWorkerError(e));
|
||||||
void this._stop();
|
void this._stop();
|
||||||
} finally {
|
} finally {
|
||||||
const donePayload: DonePayload = {
|
const donePayload: DonePayload = {
|
||||||
|
|
|
||||||
30
packages/playwright/types/testReporter.d.ts
vendored
30
packages/playwright/types/testReporter.d.ts
vendored
|
|
@ -554,16 +554,41 @@ export interface TestCase {
|
||||||
* Information about an error thrown during test execution.
|
* Information about an error thrown during test execution.
|
||||||
*/
|
*/
|
||||||
export interface TestError {
|
export interface TestError {
|
||||||
|
/**
|
||||||
|
* Expected value formatted as a human-readable string.
|
||||||
|
*/
|
||||||
|
expected?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error location in the source code.
|
* Error location in the source code.
|
||||||
*/
|
*/
|
||||||
location?: Location;
|
location?: Location;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receiver's locator.
|
||||||
|
*/
|
||||||
|
locator?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call log.
|
||||||
|
*/
|
||||||
|
log?: Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expect matcher name.
|
||||||
|
*/
|
||||||
|
matcherName?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error message. Set when [Error] (or its subclass) has been thrown.
|
* Error message. Set when [Error] (or its subclass) has been thrown.
|
||||||
*/
|
*/
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Received value formatted as a human-readable string.
|
||||||
|
*/
|
||||||
|
received?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source code snippet with highlighted error.
|
* Source code snippet with highlighted error.
|
||||||
*/
|
*/
|
||||||
|
|
@ -574,6 +599,11 @@ export interface TestError {
|
||||||
*/
|
*/
|
||||||
stack?: string;
|
stack?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout in milliseconds, if the error was caused by a timeout.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value that was thrown. Set when anything except the [Error] (or its subclass) has been thrown.
|
* The value that was thrown. Set when anything except the [Error] (or its subclass) has been thrown.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,16 @@ test('toMatchText-based assertions should have matcher result', async ({ page })
|
||||||
{
|
{
|
||||||
const e = await expect(locator).toHaveText(/Text2/, { timeout: 1 }).catch(e => e);
|
const e = await expect(locator).toHaveText(/Text2/, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
|
e.matcherResult.printedDiff = stripAnsi(e.matcherResult.printedDiff);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
actual: 'Text content',
|
actual: 'Text content',
|
||||||
expected: /Text2/,
|
expected: /Text2/,
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`),
|
||||||
name: 'toHaveText',
|
name: 'toHaveText',
|
||||||
pass: false,
|
pass: false,
|
||||||
|
locator: `locator('#node')`,
|
||||||
|
printedDiff: `Expected pattern: /Text2/
|
||||||
|
Received string: \"Text content\"`,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
@ -46,12 +50,17 @@ Call log`);
|
||||||
{
|
{
|
||||||
const e = await expect(locator).not.toHaveText(/Text/, { timeout: 1 }).catch(e => e);
|
const e = await expect(locator).not.toHaveText(/Text/, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
|
e.matcherResult.printedExpected = stripAnsi(e.matcherResult.printedExpected);
|
||||||
|
e.matcherResult.printedReceived = stripAnsi(e.matcherResult.printedReceived);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
actual: 'Text content',
|
actual: 'Text content',
|
||||||
expected: /Text/,
|
expected: /Text/,
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveText(expected)`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveText(expected)`),
|
||||||
name: 'toHaveText',
|
name: 'toHaveText',
|
||||||
pass: true,
|
pass: true,
|
||||||
|
locator: `locator('#node')`,
|
||||||
|
printedExpected: 'Expected pattern: not /Text/',
|
||||||
|
printedReceived: `Received string: \"Text content\"`,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
@ -79,6 +88,8 @@ test('toBeTruthy-based assertions should have matcher result', async ({ page })
|
||||||
name: 'toBeVisible',
|
name: 'toBeVisible',
|
||||||
pass: false,
|
pass: false,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: visible',
|
||||||
|
printedReceived: 'Received: <element(s) not found>',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -101,6 +112,8 @@ Call log`);
|
||||||
name: 'toBeVisible',
|
name: 'toBeVisible',
|
||||||
pass: true,
|
pass: true,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: not visible',
|
||||||
|
printedReceived: 'Received: visible',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -120,6 +133,7 @@ test('toEqual-based assertions should have matcher result', async ({ page }) =>
|
||||||
{
|
{
|
||||||
const e = await expect(page.locator('#node2')).toHaveCount(1, { timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#node2')).toHaveCount(1, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
|
e.matcherResult.printedDiff = stripAnsi(e.matcherResult.printedDiff);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
actual: 0,
|
actual: 0,
|
||||||
expected: 1,
|
expected: 1,
|
||||||
|
|
@ -127,6 +141,8 @@ test('toEqual-based assertions should have matcher result', async ({ page }) =>
|
||||||
name: 'toHaveCount',
|
name: 'toHaveCount',
|
||||||
pass: false,
|
pass: false,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedDiff: `Expected: 1
|
||||||
|
Received: 0`,
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -141,6 +157,8 @@ Call log`);
|
||||||
{
|
{
|
||||||
const e = await expect(page.locator('#node')).not.toHaveCount(1, { timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#node')).not.toHaveCount(1, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
|
e.matcherResult.printedExpected = stripAnsi(e.matcherResult.printedExpected);
|
||||||
|
e.matcherResult.printedReceived = stripAnsi(e.matcherResult.printedReceived);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
actual: 1,
|
actual: 1,
|
||||||
expected: 1,
|
expected: 1,
|
||||||
|
|
@ -148,6 +166,8 @@ Call log`);
|
||||||
name: 'toHaveCount',
|
name: 'toHaveCount',
|
||||||
pass: true,
|
pass: true,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: `Expected: not 1`,
|
||||||
|
printedReceived: `Received: 1`,
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -177,6 +197,8 @@ test('toBeChecked({ checked: false }) should have expected: false', async ({ pag
|
||||||
name: 'toBeChecked',
|
name: 'toBeChecked',
|
||||||
pass: false,
|
pass: false,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: checked',
|
||||||
|
printedReceived: 'Received: unchecked',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -199,6 +221,8 @@ Call log`);
|
||||||
name: 'toBeChecked',
|
name: 'toBeChecked',
|
||||||
pass: true,
|
pass: true,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: not checked',
|
||||||
|
printedReceived: 'Received: checked',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -221,6 +245,8 @@ Call log`);
|
||||||
name: 'toBeChecked',
|
name: 'toBeChecked',
|
||||||
pass: false,
|
pass: false,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: unchecked',
|
||||||
|
printedReceived: 'Received: checked',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -243,6 +269,8 @@ Call log`);
|
||||||
name: 'toBeChecked',
|
name: 'toBeChecked',
|
||||||
pass: true,
|
pass: true,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: not unchecked',
|
||||||
|
printedReceived: 'Received: unchecked',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -271,6 +299,8 @@ test('toHaveScreenshot should populate matcherResult', async ({ page, server, is
|
||||||
name: 'toHaveScreenshot',
|
name: 'toHaveScreenshot',
|
||||||
pass: false,
|
pass: false,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: expect.stringContaining('screenshot-sanity-'),
|
||||||
|
printedReceived: expect.stringContaining('screenshot-sanity-actual'),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Screenshot comparison failed:
|
expect.soft(stripAnsi(e.toString())).toContain(`Error: Screenshot comparison failed:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue