mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix: properly define apiName for web-first assertions (#12706)
Turns out relying on PWTRAP in stack is not reliable: depending on the call structure, the stack might be cut unpredictably by Node.js. This patch removes PWTRAP and instead plumbs explicit stack and pre-set `apiName` all the way down to `wrapApiCall`.
This commit is contained in:
parent
e3bd7ce119
commit
88610c8b4c
@ -101,14 +101,14 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||
return channel;
|
||||
}
|
||||
|
||||
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal = false): Promise<R> {
|
||||
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal = false, customStackTrace?: ParsedStackTrace): Promise<R> {
|
||||
const logger = this._logger;
|
||||
const stack = captureRawStack();
|
||||
const apiZone = zones.zoneData<ApiZone>('apiZone', stack);
|
||||
if (apiZone)
|
||||
return func(apiZone);
|
||||
|
||||
const stackTrace = captureStackTrace(stack);
|
||||
const stackTrace = customStackTrace || captureStackTrace(stack);
|
||||
if (isInternal)
|
||||
delete stackTrace.apiName;
|
||||
const csi = isInternal ? undefined : this._instrumentation;
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
import * as structs from '../../types/structs';
|
||||
import * as api from '../../types/types';
|
||||
import * as channels from '../protocol/channels';
|
||||
import type { ParsedStackTrace } from '../utils/stackTrace';
|
||||
import * as util from 'util';
|
||||
import { isRegExp, monotonicTime } from '../utils/utils';
|
||||
import { ElementHandle } from './elementHandle';
|
||||
@ -262,14 +263,16 @@ export class Locator implements api.Locator {
|
||||
await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options });
|
||||
}
|
||||
|
||||
async _expect(expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }> {
|
||||
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
|
||||
if (options.expectedValue)
|
||||
params.expectedValue = serializeArgument(options.expectedValue);
|
||||
const result = (await this._frame._channel.expect(params));
|
||||
if (result.received !== undefined)
|
||||
result.received = parseResult(result.received);
|
||||
return result;
|
||||
async _expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }> {
|
||||
return this._frame._wrapApiCall(async () => {
|
||||
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
|
||||
if (options.expectedValue)
|
||||
params.expectedValue = serializeArgument(options.expectedValue);
|
||||
const result = (await this._frame._channel.expect(params));
|
||||
if (result.received !== undefined)
|
||||
result.received = parseResult(result.received);
|
||||
return result;
|
||||
}, false /* isInternal */, customStackTrace);
|
||||
}
|
||||
|
||||
[util.inspect.custom]() {
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
import { Events } from './events';
|
||||
import { assert } from '../utils/utils';
|
||||
import { TimeoutSettings } from '../utils/timeoutSettings';
|
||||
import type { ParsedStackTrace } from '../utils/stackTrace';
|
||||
import * as channels from '../protocol/channels';
|
||||
import { parseError, serializeError } from '../protocol/serializers';
|
||||
import { Accessibility } from './accessibility';
|
||||
@ -483,34 +484,36 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[]}> {
|
||||
const mask = options.screenshotOptions?.mask ? options.screenshotOptions?.mask.map(locator => ({
|
||||
frame: locator._frame._channel,
|
||||
selector: locator._selector,
|
||||
})) : undefined;
|
||||
const locator = options.locator ? {
|
||||
frame: options.locator._frame._channel,
|
||||
selector: options.locator._selector,
|
||||
} : undefined;
|
||||
const expected = options.expected ? options.expected.toString('base64') : undefined;
|
||||
async _expectScreenshot(customStackTrace: ParsedStackTrace, options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[]}> {
|
||||
return this._wrapApiCall(async () => {
|
||||
const mask = options.screenshotOptions?.mask ? options.screenshotOptions?.mask.map(locator => ({
|
||||
frame: locator._frame._channel,
|
||||
selector: locator._selector,
|
||||
})) : undefined;
|
||||
const locator = options.locator ? {
|
||||
frame: options.locator._frame._channel,
|
||||
selector: options.locator._selector,
|
||||
} : undefined;
|
||||
const expected = options.expected ? options.expected.toString('base64') : undefined;
|
||||
|
||||
const result = await this._channel.expectScreenshot({
|
||||
...options,
|
||||
isNot: !!options.isNot,
|
||||
expected,
|
||||
locator,
|
||||
screenshotOptions: {
|
||||
...options.screenshotOptions,
|
||||
mask,
|
||||
}
|
||||
});
|
||||
return {
|
||||
log: result.log,
|
||||
actual: result.actual ? Buffer.from(result.actual, 'base64') : undefined,
|
||||
previous: result.previous ? Buffer.from(result.previous, 'base64') : undefined,
|
||||
diff: result.diff ? Buffer.from(result.diff, 'base64') : undefined,
|
||||
errorMessage: result.errorMessage,
|
||||
};
|
||||
const result = await this._channel.expectScreenshot({
|
||||
...options,
|
||||
isNot: !!options.isNot,
|
||||
expected,
|
||||
locator,
|
||||
screenshotOptions: {
|
||||
...options.screenshotOptions,
|
||||
mask,
|
||||
}
|
||||
});
|
||||
return {
|
||||
log: result.log,
|
||||
actual: result.actual ? Buffer.from(result.actual, 'base64') : undefined,
|
||||
previous: result.previous ? Buffer.from(result.previous, 'base64') : undefined,
|
||||
diff: result.diff ? Buffer.from(result.diff, 'base64') : undefined,
|
||||
errorMessage: result.errorMessage,
|
||||
};
|
||||
}, false /* isInternal */, customStackTrace);
|
||||
}
|
||||
|
||||
async title(): Promise<string> {
|
||||
|
||||
@ -108,28 +108,14 @@ export function captureStackTrace(rawStack?: string): ParsedStackTrace {
|
||||
|
||||
let apiName = '';
|
||||
const allFrames = parsedFrames;
|
||||
|
||||
// expect matchers have the following stack structure:
|
||||
// at Object.__PWTRAP__[expect.toHaveText] (...)
|
||||
// at __EXTERNAL_MATCHER_TRAP__ (...)
|
||||
// at Object.throwingMatcher [as toHaveText] (...)
|
||||
const TRAP = '__PWTRAP__[';
|
||||
const expectIndex = parsedFrames.findIndex(f => f.frameText.includes(TRAP));
|
||||
if (expectIndex !== -1) {
|
||||
const text = parsedFrames[expectIndex].frameText;
|
||||
const aliasIndex = text.indexOf(TRAP);
|
||||
apiName = text.substring(aliasIndex + TRAP.length, text.indexOf(']'));
|
||||
parsedFrames = parsedFrames.slice(expectIndex + 3);
|
||||
} else {
|
||||
// Deepest transition between non-client code calling into client code
|
||||
// is the api entry.
|
||||
for (let i = 0; i < parsedFrames.length - 1; i++) {
|
||||
if (parsedFrames[i].inCore && !parsedFrames[i + 1].inCore) {
|
||||
const frame = parsedFrames[i].frame;
|
||||
apiName = normalizeAPIName(frame.function);
|
||||
parsedFrames = parsedFrames.slice(i + 1);
|
||||
break;
|
||||
}
|
||||
// Deepest transition between non-client code calling into client code
|
||||
// is the api entry.
|
||||
for (let i = 0; i < parsedFrames.length - 1; i++) {
|
||||
if (parsedFrames[i].inCore && !parsedFrames[i + 1].inCore) {
|
||||
const frame = parsedFrames[i].frame;
|
||||
apiName = normalizeAPIName(frame.function);
|
||||
parsedFrames = parsedFrames.slice(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -47,10 +47,7 @@ import { toMatchSnapshot, toHaveScreenshot, getSnapshotName } from './matchers/t
|
||||
import type { Expect, TestError } from './types';
|
||||
import matchers from 'expect/build/matchers';
|
||||
import { currentTestInfo } from './globals';
|
||||
import { serializeError } from './util';
|
||||
import StackUtils from 'stack-utils';
|
||||
|
||||
const stackUtils = new StackUtils();
|
||||
import { serializeError, captureStackTrace } from './util';
|
||||
|
||||
// #region
|
||||
// Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts
|
||||
@ -184,7 +181,7 @@ class ExpectMetaInfoProxyHandler {
|
||||
}
|
||||
|
||||
function wrap(matcherName: string, matcher: any) {
|
||||
const result = function(this: any, ...args: any[]) {
|
||||
return function(this: any, ...args: any[]) {
|
||||
const testInfo = currentTestInfo();
|
||||
if (!testInfo)
|
||||
return matcher.call(this, ...args);
|
||||
@ -194,15 +191,9 @@ function wrap(matcherName: string, matcher: any) {
|
||||
const [received, nameOrOptions, optOptions] = args;
|
||||
titleSuffix = `(${getSnapshotName(testInfo, received, nameOrOptions, optOptions)})`;
|
||||
}
|
||||
|
||||
const INTERNAL_STACK_LENGTH = 4;
|
||||
// at Object.__PWTRAP__[expect.toHaveText] (...)
|
||||
// at __EXTERNAL_MATCHER_TRAP__ (...)
|
||||
// at Object.throwingMatcher [as toHaveText] (...)
|
||||
// at Proxy.<anonymous>
|
||||
// at <test function> (...)
|
||||
const stackLines = new Error().stack!.split('\n').slice(INTERNAL_STACK_LENGTH + 1);
|
||||
const frame = stackLines[0] ? stackUtils.parseLine(stackLines[0]) : undefined;
|
||||
const stackTrace = captureStackTrace();
|
||||
const stackLines = stackTrace.frameTexts;
|
||||
const frame = stackTrace.frames[0];
|
||||
const customMessage = expectCallMetaInfo?.message ?? '';
|
||||
const isSoft = expectCallMetaInfo?.isSoft ?? false;
|
||||
const step = testInfo._addStep({
|
||||
@ -257,8 +248,6 @@ function wrap(matcherName: string, matcher: any) {
|
||||
reportStepError(e);
|
||||
}
|
||||
};
|
||||
Object.defineProperty(result, 'name', { value: '__PWTRAP__[expect.' + matcherName + ']' });
|
||||
return result;
|
||||
}
|
||||
|
||||
const wrappedMatchers: any = {};
|
||||
|
||||
@ -22,9 +22,10 @@ import { expectTypes, callLogText } from '../util';
|
||||
import { toBeTruthy } from './toBeTruthy';
|
||||
import { toEqual } from './toEqual';
|
||||
import { toExpectedTextValues, toMatchText } from './toMatchText';
|
||||
import { ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace';
|
||||
|
||||
interface LocatorEx extends Locator {
|
||||
_expect(expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }>;
|
||||
_expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }>;
|
||||
}
|
||||
|
||||
interface APIResponseEx extends APIResponse {
|
||||
@ -36,9 +37,9 @@ export function toBeChecked(
|
||||
locator: LocatorEx,
|
||||
options?: { checked?: boolean, timeout?: number },
|
||||
) {
|
||||
return toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const checked = !options || options.checked === undefined || options.checked === true;
|
||||
return await locator._expect(checked ? 'to.be.checked' : 'to.be.unchecked', { isNot, timeout });
|
||||
return await locator._expect(customStackTrace, checked ? 'to.be.checked' : 'to.be.unchecked', { isNot, timeout });
|
||||
}, options);
|
||||
}
|
||||
|
||||
@ -47,8 +48,8 @@ export function toBeDisabled(
|
||||
locator: LocatorEx,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthy.call(this, 'toBeDisabled', locator, 'Locator', async (isNot, timeout) => {
|
||||
return await locator._expect('to.be.disabled', { isNot, timeout });
|
||||
return toBeTruthy.call(this, 'toBeDisabled', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
return await locator._expect(customStackTrace, 'to.be.disabled', { isNot, timeout });
|
||||
}, options);
|
||||
}
|
||||
|
||||
@ -57,8 +58,8 @@ export function toBeEditable(
|
||||
locator: LocatorEx,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthy.call(this, 'toBeEditable', locator, 'Locator', async (isNot, timeout) => {
|
||||
return await locator._expect('to.be.editable', { isNot, timeout });
|
||||
return toBeTruthy.call(this, 'toBeEditable', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
return await locator._expect(customStackTrace, 'to.be.editable', { isNot, timeout });
|
||||
}, options);
|
||||
}
|
||||
|
||||
@ -67,8 +68,8 @@ export function toBeEmpty(
|
||||
locator: LocatorEx,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthy.call(this, 'toBeEmpty', locator, 'Locator', async (isNot, timeout) => {
|
||||
return await locator._expect('to.be.empty', { isNot, timeout });
|
||||
return toBeTruthy.call(this, 'toBeEmpty', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
return await locator._expect(customStackTrace, 'to.be.empty', { isNot, timeout });
|
||||
}, options);
|
||||
}
|
||||
|
||||
@ -77,8 +78,8 @@ export function toBeEnabled(
|
||||
locator: LocatorEx,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthy.call(this, 'toBeEnabled', locator, 'Locator', async (isNot, timeout) => {
|
||||
return await locator._expect('to.be.enabled', { isNot, timeout });
|
||||
return toBeTruthy.call(this, 'toBeEnabled', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
return await locator._expect(customStackTrace, 'to.be.enabled', { isNot, timeout });
|
||||
}, options);
|
||||
}
|
||||
|
||||
@ -87,8 +88,8 @@ export function toBeFocused(
|
||||
locator: LocatorEx,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthy.call(this, 'toBeFocused', locator, 'Locator', async (isNot, timeout) => {
|
||||
return await locator._expect('to.be.focused', { isNot, timeout });
|
||||
return toBeTruthy.call(this, 'toBeFocused', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
return await locator._expect(customStackTrace, 'to.be.focused', { isNot, timeout });
|
||||
}, options);
|
||||
}
|
||||
|
||||
@ -97,8 +98,8 @@ export function toBeHidden(
|
||||
locator: LocatorEx,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthy.call(this, 'toBeHidden', locator, 'Locator', async (isNot, timeout) => {
|
||||
return await locator._expect('to.be.hidden', { isNot, timeout });
|
||||
return toBeTruthy.call(this, 'toBeHidden', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
return await locator._expect(customStackTrace, 'to.be.hidden', { isNot, timeout });
|
||||
}, options);
|
||||
}
|
||||
|
||||
@ -107,8 +108,8 @@ export function toBeVisible(
|
||||
locator: LocatorEx,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', async (isNot, timeout) => {
|
||||
return await locator._expect('to.be.visible', { isNot, timeout });
|
||||
return toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
return await locator._expect(customStackTrace, 'to.be.visible', { isNot, timeout });
|
||||
}, options);
|
||||
}
|
||||
|
||||
@ -119,14 +120,14 @@ export function toContainText(
|
||||
options?: { timeout?: number, useInnerText?: boolean },
|
||||
) {
|
||||
if (Array.isArray(expected)) {
|
||||
return toEqual.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toEqual.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true });
|
||||
return await locator._expect('to.contain.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.contain.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
|
||||
}, expected, { ...options, contains: true });
|
||||
} else {
|
||||
return toMatchText.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toMatchText.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true });
|
||||
return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
}
|
||||
@ -138,9 +139,9 @@ export function toHaveAttribute(
|
||||
expected: string | RegExp,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues([expected]);
|
||||
return await locator._expect('to.have.attribute', { expressionArg: name, expectedText, isNot, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.have.attribute', { expressionArg: name, expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
|
||||
@ -151,14 +152,14 @@ export function toHaveClass(
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
if (Array.isArray(expected)) {
|
||||
return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues(expected);
|
||||
return await locator._expect('to.have.class.array', { expectedText, isNot, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.have.class.array', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
} else {
|
||||
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues([expected]);
|
||||
return await locator._expect('to.have.class', { expectedText, isNot, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.have.class', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
}
|
||||
@ -169,8 +170,8 @@ export function toHaveCount(
|
||||
expected: number,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toEqual.call(this, 'toHaveCount', locator, 'Locator', async (isNot, timeout) => {
|
||||
return await locator._expect('to.have.count', { expectedNumber: expected, isNot, timeout });
|
||||
return toEqual.call(this, 'toHaveCount', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
return await locator._expect(customStackTrace, 'to.have.count', { expectedNumber: expected, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
|
||||
@ -181,9 +182,9 @@ export function toHaveCSS(
|
||||
expected: string | RegExp,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues([expected]);
|
||||
return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.have.css', { expressionArg: name, expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
|
||||
@ -193,9 +194,9 @@ export function toHaveId(
|
||||
expected: string | RegExp,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toMatchText.call(this, 'toHaveId', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toMatchText.call(this, 'toHaveId', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues([expected]);
|
||||
return await locator._expect('to.have.id', { expectedText, isNot, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.have.id', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
|
||||
@ -206,8 +207,8 @@ export function toHaveJSProperty(
|
||||
expected: any,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toEqual.call(this, 'toHaveJSProperty', locator, 'Locator', async (isNot, timeout) => {
|
||||
return await locator._expect('to.have.property', { expressionArg: name, expectedValue: expected, isNot, timeout });
|
||||
return toEqual.call(this, 'toHaveJSProperty', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
return await locator._expect(customStackTrace, 'to.have.property', { expressionArg: name, expectedValue: expected, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
|
||||
@ -218,14 +219,14 @@ export function toHaveText(
|
||||
options: { timeout?: number, useInnerText?: boolean } = {},
|
||||
) {
|
||||
if (Array.isArray(expected)) {
|
||||
return toEqual.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toEqual.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues(expected, { normalizeWhiteSpace: true });
|
||||
return await locator._expect('to.have.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.have.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
|
||||
}, expected, options);
|
||||
} else {
|
||||
return toMatchText.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toMatchText.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true });
|
||||
return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
}
|
||||
@ -236,9 +237,9 @@ export function toHaveValue(
|
||||
expected: string | RegExp,
|
||||
options?: { timeout?: number },
|
||||
) {
|
||||
return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues([expected]);
|
||||
return await locator._expect('to.have.value', { expectedText, isNot, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.have.value', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
|
||||
@ -249,9 +250,9 @@ export function toHaveTitle(
|
||||
options: { timeout?: number } = {},
|
||||
) {
|
||||
const locator = page.locator(':root') as LocatorEx;
|
||||
return toMatchText.call(this, 'toHaveTitle', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toMatchText.call(this, 'toHaveTitle', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true });
|
||||
return await locator._expect('to.have.title', { expectedText, isNot, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.have.title', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
|
||||
@ -264,9 +265,9 @@ export function toHaveURL(
|
||||
const baseURL = (page.context() as any)._options.baseURL;
|
||||
expected = typeof expected === 'string' ? constructURLBasedOnBaseURL(baseURL, expected) : expected;
|
||||
const locator = page.locator(':root') as LocatorEx;
|
||||
return toMatchText.call(this, 'toHaveURL', locator, 'Locator', async (isNot, timeout) => {
|
||||
return toMatchText.call(this, 'toHaveURL', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
|
||||
const expectedText = toExpectedTextValues([expected]);
|
||||
return await locator._expect('to.have.url', { expectedText, isNot, timeout });
|
||||
return await locator._expect(customStackTrace, 'to.have.url', { expectedText, isNot, timeout });
|
||||
}, expected, options);
|
||||
}
|
||||
|
||||
|
||||
@ -15,14 +15,14 @@
|
||||
*/
|
||||
|
||||
import type { Expect } from '../types';
|
||||
import { expectTypes, callLogText, currentExpectTimeout } from '../util';
|
||||
import { expectTypes, callLogText, currentExpectTimeout, ParsedStackTrace, captureStackTrace } from '../util';
|
||||
|
||||
export async function toBeTruthy(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
matcherName: string,
|
||||
receiver: any,
|
||||
receiverType: string,
|
||||
query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, log?: string[] }>,
|
||||
query: (isNot: boolean, timeout: number, customStackTrace: ParsedStackTrace) => Promise<{ matches: boolean, log?: string[] }>,
|
||||
options: { timeout?: number } = {},
|
||||
) {
|
||||
expectTypes(receiver, [receiverType], matcherName);
|
||||
@ -34,7 +34,7 @@ export async function toBeTruthy(
|
||||
|
||||
const timeout = currentExpectTimeout(options);
|
||||
|
||||
const { matches, log } = await query(this.isNot, timeout);
|
||||
const { matches, log } = await query(this.isNot, timeout, captureStackTrace('expect.' + matcherName));
|
||||
|
||||
const message = () => {
|
||||
return this.utils.matcherHint(matcherName, undefined, '', matcherOptions) + callLogText(log);
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
import type { Expect } from '../types';
|
||||
import { expectTypes } from '../util';
|
||||
import { callLogText, currentExpectTimeout } from '../util';
|
||||
import { ParsedStackTrace, captureStackTrace } from 'playwright-core/lib/utils/stackTrace';
|
||||
|
||||
// Omit colon and one or more spaces, so can call getLabelPrinter.
|
||||
const EXPECTED_LABEL = 'Expected';
|
||||
@ -30,7 +31,7 @@ export async function toEqual<T>(
|
||||
matcherName: string,
|
||||
receiver: any,
|
||||
receiverType: string,
|
||||
query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: any, log?: string[] }>,
|
||||
query: (isNot: boolean, timeout: number, customStackTrace: ParsedStackTrace) => Promise<{ matches: boolean, received?: any, log?: string[] }>,
|
||||
expected: T,
|
||||
options: { timeout?: number, contains?: boolean } = {},
|
||||
) {
|
||||
@ -44,7 +45,9 @@ export async function toEqual<T>(
|
||||
|
||||
const timeout = currentExpectTimeout(options);
|
||||
|
||||
const { matches: pass, received, log } = await query(this.isNot, timeout);
|
||||
const customStackTrace = captureStackTrace();
|
||||
customStackTrace.apiName = 'expect.' + matcherName;
|
||||
const { matches: pass, received, log } = await query(this.isNot, timeout, customStackTrace);
|
||||
|
||||
const message = pass
|
||||
? () =>
|
||||
|
||||
@ -21,7 +21,10 @@ import type { Expect } from '../types';
|
||||
import { currentTestInfo } from '../globals';
|
||||
import { mimeTypeToComparator, ImageComparatorOptions, Comparator } from 'playwright-core/lib/utils/comparators';
|
||||
import type { PageScreenshotOptions } from 'playwright-core/types/types';
|
||||
import { addSuffixToFilePath, serializeError, sanitizeForFilePath, trimLongString, callLogText, currentExpectTimeout, expectTypes } from '../util';
|
||||
import {
|
||||
addSuffixToFilePath, serializeError, sanitizeForFilePath,
|
||||
trimLongString, callLogText, currentExpectTimeout,
|
||||
expectTypes, captureStackTrace } from '../util';
|
||||
import { UpdateSnapshots } from '../types';
|
||||
import colors from 'colors/safe';
|
||||
import fs from 'fs';
|
||||
@ -310,6 +313,7 @@ export async function toHaveScreenshot(
|
||||
maxDiffPixelRatio: undefined,
|
||||
};
|
||||
|
||||
const customStackTrace = captureStackTrace(`expect.toHaveScreenshot`);
|
||||
const hasSnapshot = fs.existsSync(helper.snapshotPath);
|
||||
if (this.isNot) {
|
||||
if (!hasSnapshot)
|
||||
@ -318,7 +322,7 @@ export async function toHaveScreenshot(
|
||||
// Having `errorMessage` means we timed out while waiting
|
||||
// for screenshots not to match, so screenshots
|
||||
// are actually the same in the end.
|
||||
const isDifferent = !(await page._expectScreenshot({
|
||||
const isDifferent = !(await page._expectScreenshot(customStackTrace, {
|
||||
expected: await fs.promises.readFile(helper.snapshotPath),
|
||||
isNot: true,
|
||||
locator,
|
||||
@ -336,7 +340,7 @@ export async function toHaveScreenshot(
|
||||
if (helper.updateSnapshots === 'all' || !hasSnapshot) {
|
||||
// Regenerate a new screenshot by waiting until two screenshots are the same.
|
||||
const timeout = currentExpectTimeout(helper.allOptions);
|
||||
const { actual, previous, diff, errorMessage, log } = await page._expectScreenshot({
|
||||
const { actual, previous, diff, errorMessage, log } = await page._expectScreenshot(customStackTrace, {
|
||||
expected: undefined,
|
||||
isNot: false,
|
||||
locator,
|
||||
@ -371,7 +375,7 @@ export async function toHaveScreenshot(
|
||||
// - regular matcher (i.e. not a `.not`)
|
||||
// - no flags to update screenshots
|
||||
const expected = await fs.promises.readFile(helper.snapshotPath);
|
||||
const { actual, diff, errorMessage, log } = await page._expectScreenshot({
|
||||
const { actual, diff, errorMessage, log } = await page._expectScreenshot(customStackTrace, {
|
||||
expected,
|
||||
isNot: false,
|
||||
locator,
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
import type { ExpectedTextValue } from 'playwright-core/lib/protocol/channels';
|
||||
import { isRegExp, isString } from 'playwright-core/lib/utils/utils';
|
||||
import type { Expect } from '../types';
|
||||
import { expectTypes, callLogText, currentExpectTimeout } from '../util';
|
||||
import { expectTypes, callLogText, currentExpectTimeout, captureStackTrace, ParsedStackTrace } from '../util';
|
||||
import {
|
||||
printReceivedStringContainExpectedResult,
|
||||
printReceivedStringContainExpectedSubstring
|
||||
@ -29,7 +29,7 @@ export async function toMatchText(
|
||||
matcherName: string,
|
||||
receiver: any,
|
||||
receiverType: string,
|
||||
query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: string, log?: string[] }>,
|
||||
query: (isNot: boolean, timeout: number, customStackTrace: ParsedStackTrace) => Promise<{ matches: boolean, received?: string, log?: string[] }>,
|
||||
expected: string | RegExp,
|
||||
options: { timeout?: number, matchSubstring?: boolean } = {},
|
||||
) {
|
||||
@ -57,7 +57,7 @@ export async function toMatchText(
|
||||
|
||||
const timeout = currentExpectTimeout(options);
|
||||
|
||||
const { matches: pass, received, log } = await query(this.isNot, timeout);
|
||||
const { matches: pass, received, log } = await query(this.isNot, timeout, captureStackTrace('expect.' + matcherName));
|
||||
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
||||
const receivedString = received || '';
|
||||
const message = pass
|
||||
|
||||
@ -24,6 +24,9 @@ import debug from 'debug';
|
||||
import { calculateSha1, isRegExp } from 'playwright-core/lib/utils/utils';
|
||||
import { isInternalFileName } from 'playwright-core/lib/utils/stackTrace';
|
||||
import { currentTestInfo } from './globals';
|
||||
import { captureStackTrace as coreCaptureStackTrace, ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace';
|
||||
|
||||
export { ParsedStackTrace };
|
||||
|
||||
const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core'));
|
||||
const EXPECT_PATH = path.dirname(require.resolve('expect'));
|
||||
@ -60,6 +63,25 @@ function filterStackTrace(e: Error) {
|
||||
Error.prepareStackTrace = oldPrepare;
|
||||
}
|
||||
|
||||
export function captureStackTrace(customApiName?: string): ParsedStackTrace {
|
||||
const stackTrace: ParsedStackTrace = coreCaptureStackTrace();
|
||||
const frames = [];
|
||||
const frameTexts = [];
|
||||
for (let i = 0; i < stackTrace.frames.length; ++i) {
|
||||
const frame = stackTrace.frames[i];
|
||||
if (frame.file.startsWith(EXPECT_PATH))
|
||||
continue;
|
||||
frames.push(frame);
|
||||
frameTexts.push(stackTrace.frameTexts[i]);
|
||||
}
|
||||
return {
|
||||
allFrames: stackTrace.allFrames,
|
||||
frames,
|
||||
frameTexts,
|
||||
apiName: customApiName ?? stackTrace.apiName,
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeError(error: Error | any): TestError {
|
||||
if (error instanceof Error) {
|
||||
filterStackTrace(error);
|
||||
|
||||
@ -288,9 +288,10 @@ test('should fail to screenshot an element with infinite animation', async ({ ru
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(stripAnsi(result.output)).toContain(`Timeout 2000ms exceeded while generating screenshot because element kept changing`);
|
||||
expect(stripAnsi(result.output)).toContain(`expect.toHaveScreenshot with timeout 2000ms`);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-previous.png'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-expected.png'))).toBe(false);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-previous.png'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-diff.png'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('__screenshots__', 'a.spec.js', 'is-a-test-1.png'))).toBe(false);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user