chore(html): expose expect matcher result on TestError
This commit is contained in:
parent
4b1fbde2ad
commit
f4ce289715
|
|
@ -16,6 +16,17 @@ Error message. Set when [Error] (or its subclass) has been thrown.
|
||||||
|
|
||||||
Error stack. Set when [Error] (or its subclass) has been thrown.
|
Error stack. Set when [Error] (or its subclass) has been thrown.
|
||||||
|
|
||||||
|
## property: TestInfoError.matcherResult
|
||||||
|
* since: v1.49
|
||||||
|
- type: ?<[Object]>
|
||||||
|
- `name` <[string]>
|
||||||
|
- `message` ?<[string]> Failure message
|
||||||
|
- `log` ?<[Array]<[string]>> Call log
|
||||||
|
- `expected` ?<[any]>
|
||||||
|
- `actual` ?<[any]>
|
||||||
|
|
||||||
|
Expect matcher result.
|
||||||
|
|
||||||
## property: TestInfoError.value
|
## property: TestInfoError.value
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,17 @@ The value that was thrown. Set when anything except the [Error] (or its subclass
|
||||||
|
|
||||||
Error location in the source code.
|
Error location in the source code.
|
||||||
|
|
||||||
|
## property: TestError.matcherResult
|
||||||
|
* since: v1.49
|
||||||
|
- type: ?<[Object]>
|
||||||
|
- `name` <[string]>
|
||||||
|
- `message` ?<[string]> Failure message
|
||||||
|
- `log` ?<[Array]<[string]>> Call log
|
||||||
|
- `expected` ?<[any]>
|
||||||
|
- `actual` ?<[any]>
|
||||||
|
|
||||||
|
Expect matcher result.
|
||||||
|
|
||||||
## property: TestError.snippet
|
## property: TestError.snippet
|
||||||
* since: v1.33
|
* since: v1.33
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { TestAttachment, TestCase, TestResult, TestStep } from './types';
|
import type { ErrorDetails, TestAttachment, TestCase, TestResult, TestStep } from './types';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { TreeItem } from './treeItem';
|
import { TreeItem } from './treeItem';
|
||||||
import { msToString } from './utils';
|
import { msToString } from './utils';
|
||||||
|
|
@ -150,26 +150,28 @@ export const TestResultView: React.FC<{
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function classifyErrors(testErrors: string[], diffs: ImageDiff[]) {
|
function classifyErrors(testErrors: ErrorDetails[], diffs: ImageDiff[]) {
|
||||||
return testErrors.map(error => {
|
return testErrors.map(error => {
|
||||||
if (error.includes('Screenshot comparison failed:')) {
|
if (error.matcherResult?.name === 'toHaveScreenshot' && error.matcherResult?.actual && error.matcherResult?.expected) {
|
||||||
const matchingDiff = diffs.find(diff => {
|
const matchingDiff = diffs.find(diff => {
|
||||||
const attachmentName = diff.actual?.attachment.name;
|
const attachmentName = diff.actual?.attachment.name;
|
||||||
return attachmentName && error.includes(attachmentName);
|
return attachmentName && error.matcherResult?.actual.endsWith(attachmentName);
|
||||||
});
|
});
|
||||||
|
const errorSuffix = ['Call log:',
|
||||||
|
...(error.matcherResult.log?.map(line => ' - ' + line) || []),
|
||||||
|
'',
|
||||||
|
error.snippet,
|
||||||
|
].join('\n');
|
||||||
if (matchingDiff) {
|
if (matchingDiff) {
|
||||||
const lines = error.split('\n');
|
return {
|
||||||
const index = lines.findIndex(line => /Expected:|Previous:|Received:/.test(line));
|
type: 'screenshot',
|
||||||
const errorPrefix = index !== -1 ? lines.slice(0, index).join('\n') : lines[0];
|
diff: matchingDiff,
|
||||||
|
errorPrefix: error.matcherResult?.message,
|
||||||
const diffIndex = lines.findIndex(line => / +Diff:/.test(line));
|
errorSuffix
|
||||||
const errorSuffix = diffIndex !== -1 ? lines.slice(diffIndex + 2).join('\n') : lines.slice(1).join('\n');
|
};
|
||||||
|
|
||||||
return { type: 'screenshot', diff: matchingDiff, errorPrefix, errorSuffix };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { type: 'regular', error };
|
return { type: 'regular', error: error.message };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,21 @@ export type TestCase = Omit<TestCaseSummary, 'results'> & {
|
||||||
results: TestResult[];
|
results: TestResult[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MatcherResult = {
|
||||||
|
name: string;
|
||||||
|
message?: string;
|
||||||
|
log?: string[];
|
||||||
|
expected?: any;
|
||||||
|
actual?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ErrorDetails = {
|
||||||
|
message: string;
|
||||||
|
location?: Location;
|
||||||
|
matcherResult?: MatcherResult;
|
||||||
|
snippet?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TestAttachment = {
|
export type TestAttachment = {
|
||||||
name: string;
|
name: string;
|
||||||
body?: string;
|
body?: string;
|
||||||
|
|
@ -95,7 +110,7 @@ export type TestResult = {
|
||||||
startTime: string;
|
startTime: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
steps: TestStep[];
|
steps: TestStep[];
|
||||||
errors: string[];
|
errors: ErrorDetails[];
|
||||||
attachments: TestAttachment[];
|
attachments: TestAttachment[];
|
||||||
status: 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted';
|
status: 'passed' | 'failed' | 'timedOut' | 'skipped' | 'interrupted';
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export function matcherHint(state: ExpectMatcherState, locator: Locator | undefi
|
||||||
|
|
||||||
export type MatcherResult<E, A> = {
|
export type MatcherResult<E, A> = {
|
||||||
name: string;
|
name: string;
|
||||||
|
shortMessage?: string;
|
||||||
expected: E;
|
expected: E;
|
||||||
message: () => string;
|
message: () => string;
|
||||||
pass: boolean;
|
pass: boolean;
|
||||||
|
|
|
||||||
|
|
@ -185,8 +185,9 @@ class SnapshotHelper {
|
||||||
this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot';
|
this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot';
|
||||||
}
|
}
|
||||||
|
|
||||||
createMatcherResult(message: string, pass: boolean, log?: string[]): ImageMatcherResult {
|
createMatcherResult(message: string, pass: boolean, log?: string[], shortMessage?: string): ImageMatcherResult {
|
||||||
const unfiltered: ImageMatcherResult = {
|
const unfiltered: ImageMatcherResult = {
|
||||||
|
shortMessage,
|
||||||
name: this.matcherName,
|
name: this.matcherName,
|
||||||
expected: this.expectedPath,
|
expected: this.expectedPath,
|
||||||
actual: this.actualPath,
|
actual: this.actualPath,
|
||||||
|
|
@ -202,7 +203,7 @@ class SnapshotHelper {
|
||||||
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
|
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
|
||||||
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`;
|
const message = `A snapshot doesn't exist at ${this.expectedPath}${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`;
|
||||||
// NOTE: 'isNot' matcher implies inversed value.
|
// NOTE: 'isNot' matcher implies inversed value.
|
||||||
return this.createMatcherResult(message, true);
|
return this.createMatcherResult(message, true, undefined, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDifferentNegated(): ImageMatcherResult {
|
handleDifferentNegated(): ImageMatcherResult {
|
||||||
|
|
@ -217,7 +218,7 @@ class SnapshotHelper {
|
||||||
indent('Expected result should be different from the actual one.', ' '),
|
indent('Expected result should be different from the actual one.', ' '),
|
||||||
].join('\n');
|
].join('\n');
|
||||||
// NOTE: 'isNot' matcher implies inversed value.
|
// NOTE: 'isNot' matcher implies inversed value.
|
||||||
return this.createMatcherResult(message, true);
|
return this.createMatcherResult(message, true, undefined, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMissing(actual: Buffer | string): ImageMatcherResult {
|
handleMissing(actual: Buffer | string): ImageMatcherResult {
|
||||||
|
|
@ -231,14 +232,14 @@ class SnapshotHelper {
|
||||||
if (this.updateSnapshots === 'all') {
|
if (this.updateSnapshots === 'all') {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.log(message);
|
console.log(message);
|
||||||
return this.createMatcherResult(message, true);
|
return this.createMatcherResult(message, true, undefined, message);
|
||||||
}
|
}
|
||||||
if (this.updateSnapshots === 'missing') {
|
if (this.updateSnapshots === 'missing') {
|
||||||
this.testInfo._hasNonRetriableError = true;
|
this.testInfo._hasNonRetriableError = true;
|
||||||
this.testInfo._failWithError(new Error(message));
|
this.testInfo._failWithError(new Error(message));
|
||||||
return this.createMatcherResult('', true);
|
return this.createMatcherResult('', true);
|
||||||
}
|
}
|
||||||
return this.createMatcherResult(message, false);
|
return this.createMatcherResult(message, false, undefined, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDifferent(
|
handleDifferent(
|
||||||
|
|
@ -256,6 +257,8 @@ class SnapshotHelper {
|
||||||
if (diffError)
|
if (diffError)
|
||||||
output.push(indent(diffError, ' '));
|
output.push(indent(diffError, ' '));
|
||||||
|
|
||||||
|
const shortMessage = output.join('\n');
|
||||||
|
|
||||||
if (expected !== undefined) {
|
if (expected !== undefined) {
|
||||||
// Copy the expectation inside the `test-results/` folder for backwards compatibility,
|
// Copy the expectation inside the `test-results/` folder for backwards compatibility,
|
||||||
// so that one can upload `test-results/` directory and have all the data inside.
|
// so that one can upload `test-results/` directory and have all the data inside.
|
||||||
|
|
@ -284,7 +287,7 @@ class SnapshotHelper {
|
||||||
else
|
else
|
||||||
output.push('');
|
output.push('');
|
||||||
|
|
||||||
return this.createMatcherResult(output.join('\n'), false, log);
|
return this.createMatcherResult(output.join('\n'), false, log, shortMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMatching(): ImageMatcherResult {
|
handleMatching(): ImageMatcherResult {
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,19 @@ type Annotation = {
|
||||||
location?: Location;
|
location?: Location;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type MatcherResult = {
|
||||||
|
name: string;
|
||||||
|
message?: string;
|
||||||
|
log?: string[];
|
||||||
|
expected?: any;
|
||||||
|
actual?: any;
|
||||||
|
};
|
||||||
|
|
||||||
type ErrorDetails = {
|
type ErrorDetails = {
|
||||||
message: string;
|
message: string;
|
||||||
location?: Location;
|
location?: Location;
|
||||||
|
matcherResult?: MatcherResult;
|
||||||
|
snippet?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TestSummary = {
|
type TestSummary = {
|
||||||
|
|
@ -383,6 +393,8 @@ 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,
|
||||||
|
matcherResult: error.matcherResult,
|
||||||
|
snippet: error.snippet,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return errorDetails;
|
return errorDetails;
|
||||||
|
|
|
||||||
|
|
@ -485,7 +485,7 @@ class HtmlBuilder {
|
||||||
startTime: result.startTime.toISOString(),
|
startTime: result.startTime.toISOString(),
|
||||||
retry: result.retry,
|
retry: result.retry,
|
||||||
steps: dedupeSteps(result.steps).map(s => this._createTestStep(s)),
|
steps: dedupeSteps(result.steps).map(s => this._createTestStep(s)),
|
||||||
errors: formatResultFailure(test, result, '', true).map(error => error.message),
|
errors: formatResultFailure(test, result, '', true),
|
||||||
status: result.status,
|
status: result.status,
|
||||||
attachments: this._serializeAttachments([
|
attachments: this._serializeAttachments([
|
||||||
...result.attachments,
|
...result.attachments,
|
||||||
|
|
|
||||||
|
|
@ -25,22 +25,37 @@ import type { TestInfoError } from './../types/test';
|
||||||
import type { Location } from './../types/testReporter';
|
import type { Location } from './../types/testReporter';
|
||||||
import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
|
import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
|
||||||
import type { RawStack } from 'playwright-core/lib/utils';
|
import type { RawStack } from 'playwright-core/lib/utils';
|
||||||
|
import type { MatcherResult } from './matchers/matcherHint';
|
||||||
|
|
||||||
const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..');
|
const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..');
|
||||||
const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json'));
|
const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json'));
|
||||||
|
|
||||||
export function filterStackTrace(e: Error): { message: string, stack: string } {
|
export function filterStackTrace(e: Error): { message: string, stack: string, matcherResult: TestInfoError['matcherResult'] } {
|
||||||
const name = e.name ? e.name + ': ' : '';
|
const name = e.name ? e.name + ': ' : '';
|
||||||
if (process.env.PWDEBUGIMPL)
|
if (process.env.PWDEBUGIMPL)
|
||||||
return { message: name + e.message, stack: e.stack || '' };
|
return { message: name + e.message, stack: e.stack || '', matcherResult: filterMatcherResult(e) };
|
||||||
|
|
||||||
const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || []));
|
const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || []));
|
||||||
return {
|
return {
|
||||||
|
matcherResult: filterMatcherResult(e),
|
||||||
message: name + e.message,
|
message: name + e.message,
|
||||||
stack: `${name}${e.message}${stackLines.map(line => '\n' + line).join('')}`
|
stack: `${name}${e.message}${stackLines.map(line => '\n' + line).join('')}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterMatcherResult(e: Error): TestInfoError['matcherResult'] | undefined {
|
||||||
|
const matcherResult = (e as any).matcherResult as MatcherResult<unknown, unknown>;
|
||||||
|
if (!matcherResult)
|
||||||
|
return undefined;
|
||||||
|
return {
|
||||||
|
name: matcherResult.name,
|
||||||
|
message: matcherResult.shortMessage,
|
||||||
|
log: matcherResult.log,
|
||||||
|
expected: matcherResult.expected,
|
||||||
|
actual: matcherResult.actual,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function filterStackFile(file: string) {
|
export function filterStackFile(file: string) {
|
||||||
if (!process.env.PWDEBUGIMPL && file.startsWith(PLAYWRIGHT_TEST_PATH))
|
if (!process.env.PWDEBUGIMPL && file.startsWith(PLAYWRIGHT_TEST_PATH))
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
21
packages/playwright/types/test.d.ts
vendored
21
packages/playwright/types/test.d.ts
vendored
|
|
@ -8379,6 +8379,27 @@ export interface TestInfo {
|
||||||
* Information about an error thrown during test execution.
|
* Information about an error thrown during test execution.
|
||||||
*/
|
*/
|
||||||
export interface TestInfoError {
|
export interface TestInfoError {
|
||||||
|
/**
|
||||||
|
* Expect matcher result.
|
||||||
|
*/
|
||||||
|
matcherResult?: {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Failure message
|
||||||
|
*/
|
||||||
|
message?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call log
|
||||||
|
*/
|
||||||
|
log?: Array<string>;
|
||||||
|
|
||||||
|
expected?: any;
|
||||||
|
|
||||||
|
actual?: any;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error message. Set when [Error] (or its subclass) has been thrown.
|
* Error message. Set when [Error] (or its subclass) has been thrown.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
21
packages/playwright/types/testReporter.d.ts
vendored
21
packages/playwright/types/testReporter.d.ts
vendored
|
|
@ -559,6 +559,27 @@ export interface TestError {
|
||||||
*/
|
*/
|
||||||
location?: Location;
|
location?: Location;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expect matcher result.
|
||||||
|
*/
|
||||||
|
matcherResult?: {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Failure message
|
||||||
|
*/
|
||||||
|
message?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call log
|
||||||
|
*/
|
||||||
|
log?: Array<string>;
|
||||||
|
|
||||||
|
expected?: any;
|
||||||
|
|
||||||
|
actual?: any;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error message. Set when [Error] (or its subclass) has been thrown.
|
* Error message. Set when [Error] (or its subclass) has been thrown.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue