fix
This commit is contained in:
parent
78c5ff0817
commit
0669a71309
|
|
@ -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]>>
|
||||
|
|
|
|||
|
|
@ -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]>>
|
||||
|
|
|
|||
|
|
@ -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 <div className='test-error-view test-error-text' data-testId={testId} dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||
};
|
||||
|
||||
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 <div data-testid='test-screenshot-error-view' className='test-error-view'>
|
||||
<div dangerouslySetInnerHTML={{ __html: prefixHtml || '' }} className='test-error-text' style={{ marginBottom: 20 }}></div>
|
||||
<ImageDiffView key='image-diff' diff={diff} hideDetails={true}></ImageDiffView>
|
||||
|
|
@ -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 - ')}
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export const TestResultView: React.FC<{
|
|||
{!!errors.length && <AutoChip header='Errors'>
|
||||
{errors.map((error, index) => {
|
||||
if (error.type === 'screenshot')
|
||||
return <TestScreenshotErrorView key={'test-result-error-message-' + index} errorPrefix={error.errorPrefix} diff={error.diff!} errorSuffix={error.errorSuffix}></TestScreenshotErrorView>;
|
||||
return <TestScreenshotErrorView key={'test-result-error-message-' + index} error={error.error} diff={error.diff!}></TestScreenshotErrorView>;
|
||||
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!}></TestErrorView>;
|
||||
})}
|
||||
</AutoChip>}
|
||||
|
|
@ -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
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { type: 'regular', error: error.message };
|
||||
return {
|
||||
type: 'regular',
|
||||
error
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,12 @@ export type MatcherResult<E, A> = {
|
|||
actual?: A;
|
||||
log?: string[];
|
||||
timeout?: number;
|
||||
|
||||
locator?: string;
|
||||
header?: string;
|
||||
printedReceived?: string;
|
||||
printedExpected?: string;
|
||||
|
||||
};
|
||||
|
||||
export class ExpectError extends Error {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
if (pass === !this.isNot) {
|
||||
return {
|
||||
name: matcherName,
|
||||
message: () => '',
|
||||
pass,
|
||||
expected
|
||||
};
|
||||
}
|
||||
|
||||
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
||||
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;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -25,36 +25,22 @@ import type { TestInfoError } from './../types/test';
|
|||
import type { Location } from './../types/testReporter';
|
||||
import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } 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_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json'));
|
||||
|
||||
export function filterStackTrace(e: Error): Omit<TestInfoError, 'value'> {
|
||||
export function filterStackTrace(e: Error): { message: string, stack: string } {
|
||||
const name = e.name ? e.name + ': ' : '';
|
||||
if (process.env.PWDEBUGIMPL)
|
||||
return { message: name + e.message, stack: e.stack || '', ...filterExpectDetails(e) };
|
||||
return { message: name + e.message, stack: e.stack || '' };
|
||||
|
||||
const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || []));
|
||||
return {
|
||||
...filterExpectDetails(e),
|
||||
message: name + e.message,
|
||||
stack: `${name}${e.message}${stackLines.map(line => '\n' + line).join('')}`
|
||||
};
|
||||
}
|
||||
|
||||
function filterExpectDetails(e: Error): Pick<TestInfoError, 'shortMessage'|'log'|'expected'|'actual'> {
|
||||
const matcherResult = (e as any).matcherResult as MatcherResult<unknown, unknown>;
|
||||
if (!matcherResult)
|
||||
return {};
|
||||
return {
|
||||
shortMessage: matcherResult.shortMessage,
|
||||
log: matcherResult.log,
|
||||
expected: matcherResult.expected,
|
||||
actual: matcherResult.actual,
|
||||
};
|
||||
}
|
||||
|
||||
export function filterStackFile(file: string) {
|
||||
if (!process.env.PWDEBUGIMPL && file.startsWith(PLAYWRIGHT_TEST_PATH))
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -25,15 +25,16 @@ export function serializeWorkerError(error: Error | any): TestInfoError {
|
|||
};
|
||||
}
|
||||
|
||||
function serializeExpectDetails(e: Error): Pick<TestInfoError, 'shortMessage'|'log'|'expected'|'actual'> {
|
||||
function serializeExpectDetails(e: Error): Pick<TestInfoError, 'shortMessage'|'log'|'expected'|'actual'|'locator'> {
|
||||
const matcherResult = (e as any).matcherResult as MatcherResult<unknown, unknown>;
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
9
packages/playwright/types/test.d.ts
vendored
9
packages/playwright/types/test.d.ts
vendored
|
|
@ -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.
|
||||
|
|
|
|||
9
packages/playwright/types/testReporter.d.ts
vendored
9
packages/playwright/types/testReporter.d.ts
vendored
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@
|
|||
import { test as it, expect } from './pageTest';
|
||||
|
||||
it('should check the box @smoke', async ({ page }) => {
|
||||
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
|
||||
await page.setContent(`<div>foo</div>`);
|
||||
// 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);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue