revert(#12706): also fix related bugs it introduced (#21070)

This commit is contained in:
Pavel Feldman 2023-02-21 14:15:11 -08:00 committed by GitHub
parent c69a7424b4
commit 06fc72b6ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 144 additions and 199 deletions

View File

@ -154,14 +154,14 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
return channel;
}
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal = false, customStackTrace?: ParsedStackTrace): Promise<R> {
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal = false): Promise<R> {
const logger = this._logger;
const stack = captureRawStack();
const apiZone = zones.zoneData<ApiZone>('apiZone', stack);
if (apiZone)
return func(apiZone);
const stackTrace = customStackTrace || captureStackTrace(stack);
const stackTrace = captureStackTrace(stack);
if (isInternal)
delete stackTrace.apiName;
const csi = isInternal ? undefined : this._instrumentation;

View File

@ -17,7 +17,6 @@
import type * as structs from '../../types/structs';
import type * as api from '../../types/types';
import type * as channels from '@protocol/channels';
import type { ParsedStackTrace } from '../utils/stackTrace';
import * as util from 'util';
import { monotonicTime } from '../utils';
import { ElementHandle } from './elementHandle';
@ -310,15 +309,13 @@ export class Locator implements api.Locator {
await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options });
}
async _expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> {
return this._frame._wrapApiCall(async () => {
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
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);
async _expect(expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> {
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
params.expectedValue = serializeArgument(options.expectedValue);
const result = (await this._frame._channel.expect(params));
if (result.received !== undefined)
result.received = parseResult(result.received);
return result;
}
[util.inspect.custom]() {

View File

@ -26,7 +26,6 @@ import type * as channels from '@protocol/channels';
import { parseError, serializeError } from '../protocol/serializers';
import { assert, headersObjectToArray, isObject, isRegExp, isString } from '../utils';
import { mkdirIfNeeded } from '../utils/fileUtils';
import type { ParsedStackTrace } from '../utils/stackTrace';
import { Accessibility } from './accessibility';
import { Artifact } from './artifact';
import type { BrowserContext } from './browserContext';
@ -498,26 +497,24 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return result.binary;
}
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;
return await this._channel.expectScreenshot({
...options,
isNot: !!options.isNot,
locator,
screenshotOptions: {
...options.screenshotOptions,
mask,
}
});
}, false /* isInternal */, customStackTrace);
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;
return await this._channel.expectScreenshot({
...options,
isNot: !!options.isNot,
locator,
screenshotOptions: {
...options.screenshotOptions,
mask,
}
});
}
async title(): Promise<string> {

View File

@ -28,14 +28,12 @@ export function rewriteErrorMessage<E extends Error>(e: E, newMessage: string):
}
const CORE_DIR = path.resolve(__dirname, '..', '..');
const CORE_LIB = path.join(CORE_DIR, 'lib');
const CORE_SRC = path.join(CORE_DIR, 'src');
const COVERAGE_PATH = path.join(CORE_DIR, '..', '..', 'tests', 'config', 'coverage.js');
const stackIgnoreFilters = [
(frame: StackFrame) => frame.file.startsWith(CORE_DIR),
const internalStackPrefixes = [
CORE_DIR,
];
export const addStackIgnoreFilter = (filter: (frame: StackFrame) => boolean) => stackIgnoreFilters.push(filter);
export const addInternalStackPrefix = (prefix: string) => internalStackPrefixes.push(prefix);
export type StackFrame = {
file: string,
@ -60,7 +58,7 @@ export function captureRawStack(): string {
return stack;
}
export function isInternalFileName(file: string, functionName?: string): boolean {
function isInternalFileName(file: string, functionName?: string): boolean {
// Node 16+ has node:internal.
if (file.startsWith('internal') || file.startsWith('node:'))
return true;
@ -77,7 +75,7 @@ export function captureStackTrace(rawStack?: string): ParsedStackTrace {
type ParsedFrame = {
frame: StackFrame;
frameText: string;
inCore: boolean;
isPlaywrightLibrary: boolean;
};
let parsedFrames = stack.split('\n').map(line => {
const { frame, fileName } = parseStackTraceLine(line);
@ -87,7 +85,7 @@ export function captureStackTrace(rawStack?: string): ParsedStackTrace {
return null;
if (!process.env.PWDEBUGIMPL && isTesting && fileName.includes(COVERAGE_PATH))
return null;
const inCore = fileName.startsWith(CORE_LIB) || fileName.startsWith(CORE_SRC);
const isPlaywrightLibrary = fileName.startsWith(CORE_DIR);
const parsed: ParsedFrame = {
frame: {
file: fileName,
@ -96,21 +94,29 @@ export function captureStackTrace(rawStack?: string): ParsedStackTrace {
function: frame.function,
},
frameText: line,
inCore
isPlaywrightLibrary
};
return parsed;
}).filter(Boolean) as ParsedFrame[];
let apiName = '';
const allFrames = parsedFrames;
// Deepest transition between non-client code calling into client code
// is the api entry.
// Use stack trap for the API annotation, if available.
for (let i = parsedFrames.length - 1; i >= 0; i--) {
const parsedFrame = parsedFrames[i];
if (parsedFrame.frame.function?.startsWith('__PWTRAP__[')) {
apiName = parsedFrame.frame.function!.substring('__PWTRAP__['.length, parsedFrame.frame.function!.length - 1);
break;
}
}
// Otherwise, 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);
if (!process.env.PWDEBUGIMPL)
parsedFrames = parsedFrames.slice(i + 1);
const parsedFrame = parsedFrames[i];
if (parsedFrame.isPlaywrightLibrary && !parsedFrames[i + 1].isPlaywrightLibrary) {
apiName = apiName || normalizeAPIName(parsedFrame.frame.function);
break;
}
}
@ -124,11 +130,11 @@ export function captureStackTrace(rawStack?: string): ParsedStackTrace {
return match[1].toLowerCase() + match[2];
}
// Hide all test runner and library frames in the user stack (event handlers produce them).
parsedFrames = parsedFrames.filter((f, i) => {
// This is for the inspector so that it did not include the test runner stack frames.
parsedFrames = parsedFrames.filter(f => {
if (process.env.PWDEBUGIMPL)
return true;
if (stackIgnoreFilters.some(filter => filter(f.frame)))
if (internalStackPrefixes.some(prefix => f.frame.file.startsWith(prefix)))
return false;
return true;
});

View File

@ -18,7 +18,7 @@ import * as fs from 'fs';
import * as path from 'path';
import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
import * as playwrightLibrary from 'playwright-core';
import { createGuid, debugMode, removeFolders, addStackIgnoreFilter } from 'playwright-core/lib/utils';
import { createGuid, debugMode, removeFolders, addInternalStackPrefix } from 'playwright-core/lib/utils';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test';
import type { TestInfoImpl } from './worker/testInfo';
import { rootTestType } from './common/testType';
@ -27,7 +27,7 @@ export { expect } from './matchers/expect';
export { store } from './store';
export const _baseTest: TestType<{}, {}> = rootTestType.test;
addStackIgnoreFilter((frame: StackFrame) => frame.file.startsWith(path.dirname(require.resolve('../package.json'))));
addInternalStackPrefix(path.dirname(require.resolve('../package.json')));
if ((process as any)['__pw_initiator__']) {
const originalStackTraceLimit = Error.stackTraceLimit;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { pollAgainstTimeout } from 'playwright-core/lib/utils';
import { captureStackTrace, pollAgainstTimeout } from 'playwright-core/lib/utils';
import path from 'path';
import {
toBeChecked,
@ -44,7 +44,7 @@ import {
import { toMatchSnapshot, toHaveScreenshot } from './toMatchSnapshot';
import type { Expect } from '../common/types';
import { currentTestInfo, currentExpectTimeout } from '../common/globals';
import { serializeError, captureStackTrace, trimLongString } from '../util';
import { serializeError, trimLongString } from '../util';
import {
expect as expectLibrary,
INVERTED_COLOR,
@ -157,6 +157,7 @@ type ExpectMetaInfo = {
isNot: boolean;
isSoft: boolean;
isPoll: boolean;
nameTokens: string[];
pollTimeout?: number;
pollIntervals?: number[];
generator?: Generator;
@ -166,7 +167,7 @@ class ExpectMetaInfoProxyHandler {
private _info: ExpectMetaInfo;
constructor(messageOrOptions: ExpectMessageOrOptions, isSoft: boolean, isPoll: boolean, generator?: Generator) {
this._info = { isSoft, isPoll, generator, isNot: false };
this._info = { isSoft, isPoll, generator, isNot: false, nameTokens: [] };
if (typeof messageOrOptions === 'string') {
this._info.message = messageOrOptions;
} else {
@ -224,12 +225,12 @@ class ExpectMetaInfoProxyHandler {
messageLines.splice(uselessMatcherLineIndex, 1);
}
const newMessage = [
'Error: ' + customMessage,
customMessage,
'',
...messageLines,
].join('\n');
jestError.message = newMessage;
jestError.stack = newMessage + '\n' + stackLines.join('\n');
jestError.stack = jestError.name + ': ' + newMessage + '\n' + stackLines.join('\n');
}
const serializerError = serializeError(jestError);
@ -241,8 +242,10 @@ class ExpectMetaInfoProxyHandler {
};
try {
const result = matcher.call(target, ...args);
if ((result instanceof Promise))
const result = namedFunction(defaultTitle)(() => {
return matcher.call(target, ...args);
});
if (result instanceof Promise)
return result.then(() => step.complete({})).catch(reportStepError);
else
step.complete({});
@ -253,6 +256,14 @@ class ExpectMetaInfoProxyHandler {
}
}
function namedFunction(name: string) {
const result = function(callback: any) {
return callback();
};
Object.defineProperty(result, 'name', { value: '__PWTRAP__[' + name + ']' });
return result;
}
async function pollMatcher(matcherName: any, isNot: boolean, pollIntervals: number[] | undefined, timeout: number, generator: () => any, ...args: any[]) {
const result = await pollAgainstTimeout<Error|undefined>(async () => {
const value = await generator();

View File

@ -24,11 +24,10 @@ import type { TestInfoErrorState } from '../worker/testInfo';
import { toBeTruthy } from './toBeTruthy';
import { toEqual } from './toEqual';
import { toExpectedTextValues, toMatchText } from './toMatchText';
import type { ParsedStackTrace } from 'playwright-core/lib/utils';
import { constructURLBasedOnBaseURL, isTextualMimeType, pollAgainstTimeout } from 'playwright-core/lib/utils';
interface LocatorEx extends Locator {
_expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>;
_expect(expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>;
}
interface APIResponseEx extends APIResponse {
@ -40,9 +39,9 @@ export function toBeChecked(
locator: LocatorEx,
options?: { checked?: boolean, timeout?: number },
) {
return toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', async (isNot, timeout) => {
const checked = !options || options.checked === undefined || options.checked === true;
return await locator._expect(customStackTrace, checked ? 'to.be.checked' : 'to.be.unchecked', { isNot, timeout });
return await locator._expect(checked ? 'to.be.checked' : 'to.be.unchecked', { isNot, timeout });
}, options);
}
@ -51,8 +50,8 @@ export function toBeDisabled(
locator: LocatorEx,
options?: { timeout?: number },
) {
return toBeTruthy.call(this, 'toBeDisabled', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return await locator._expect(customStackTrace, 'to.be.disabled', { isNot, timeout });
return toBeTruthy.call(this, 'toBeDisabled', locator, 'Locator', async (isNot, timeout) => {
return await locator._expect('to.be.disabled', { isNot, timeout });
}, options);
}
@ -61,9 +60,9 @@ export function toBeEditable(
locator: LocatorEx,
options?: { editable?: boolean, timeout?: number },
) {
return toBeTruthy.call(this, 'toBeEditable', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toBeTruthy.call(this, 'toBeEditable', locator, 'Locator', async (isNot, timeout) => {
const editable = !options || options.editable === undefined || options.editable === true;
return await locator._expect(customStackTrace, editable ? 'to.be.editable' : 'to.be.readonly', { isNot, timeout });
return await locator._expect(editable ? 'to.be.editable' : 'to.be.readonly', { isNot, timeout });
}, options);
}
@ -72,8 +71,8 @@ export function toBeEmpty(
locator: LocatorEx,
options?: { timeout?: number },
) {
return toBeTruthy.call(this, 'toBeEmpty', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return await locator._expect(customStackTrace, 'to.be.empty', { isNot, timeout });
return toBeTruthy.call(this, 'toBeEmpty', locator, 'Locator', async (isNot, timeout) => {
return await locator._expect('to.be.empty', { isNot, timeout });
}, options);
}
@ -82,9 +81,9 @@ export function toBeEnabled(
locator: LocatorEx,
options?: { enabled?: boolean, timeout?: number },
) {
return toBeTruthy.call(this, 'toBeEnabled', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toBeTruthy.call(this, 'toBeEnabled', locator, 'Locator', async (isNot, timeout) => {
const enabled = !options || options.enabled === undefined || options.enabled === true;
return await locator._expect(customStackTrace, enabled ? 'to.be.enabled' : 'to.be.disabled', { isNot, timeout });
return await locator._expect(enabled ? 'to.be.enabled' : 'to.be.disabled', { isNot, timeout });
}, options);
}
@ -93,8 +92,8 @@ export function toBeFocused(
locator: LocatorEx,
options?: { timeout?: number },
) {
return toBeTruthy.call(this, 'toBeFocused', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return await locator._expect(customStackTrace, 'to.be.focused', { isNot, timeout });
return toBeTruthy.call(this, 'toBeFocused', locator, 'Locator', async (isNot, timeout) => {
return await locator._expect('to.be.focused', { isNot, timeout });
}, options);
}
@ -103,8 +102,8 @@ export function toBeHidden(
locator: LocatorEx,
options?: { timeout?: number },
) {
return toBeTruthy.call(this, 'toBeHidden', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return await locator._expect(customStackTrace, 'to.be.hidden', { isNot, timeout });
return toBeTruthy.call(this, 'toBeHidden', locator, 'Locator', async (isNot, timeout) => {
return await locator._expect('to.be.hidden', { isNot, timeout });
}, options);
}
@ -113,9 +112,9 @@ export function toBeVisible(
locator: LocatorEx,
options?: { visible?: boolean, timeout?: number },
) {
return toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', async (isNot, timeout) => {
const visible = !options || options.visible === undefined || options.visible === true;
return await locator._expect(customStackTrace, visible ? 'to.be.visible' : 'to.be.hidden', { isNot, timeout });
return await locator._expect(visible ? 'to.be.visible' : 'to.be.hidden', { isNot, timeout });
}, options);
}
@ -124,8 +123,8 @@ export function toBeInViewport(
locator: LocatorEx,
options?: { timeout?: number, ratio?: number },
) {
return toBeTruthy.call(this, 'toBeInViewport', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return await locator._expect(customStackTrace, 'to.be.in.viewport', { isNot, expectedNumber: options?.ratio, timeout });
return toBeTruthy.call(this, 'toBeInViewport', locator, 'Locator', async (isNot, timeout) => {
return await locator._expect('to.be.in.viewport', { isNot, expectedNumber: options?.ratio, timeout });
}, options);
}
@ -138,12 +137,12 @@ export function toContainText(
if (Array.isArray(expected)) {
return toEqual.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
const expectedText = toExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase });
return await locator._expect(customStackTrace, 'to.contain.text.array', { expectedText, isNot, useInnerText: options.useInnerText, timeout });
return await locator._expect('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, customStackTrace) => {
return toMatchText.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase });
return await locator._expect(customStackTrace, 'to.have.text', { expectedText, isNot, useInnerText: options.useInnerText, timeout });
return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options.useInnerText, timeout });
}, expected, options);
}
}
@ -155,9 +154,9 @@ export function toHaveAttribute(
expected: string | RegExp,
options?: { timeout?: number },
) {
return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]);
return await locator._expect(customStackTrace, 'to.have.attribute', { expressionArg: name, expectedText, isNot, timeout });
return await locator._expect('to.have.attribute', { expressionArg: name, expectedText, isNot, timeout });
}, expected, options);
}
@ -168,14 +167,14 @@ export function toHaveClass(
options?: { timeout?: number },
) {
if (Array.isArray(expected)) {
return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues(expected);
return await locator._expect(customStackTrace, 'to.have.class.array', { expectedText, isNot, timeout });
return await locator._expect('to.have.class.array', { expectedText, isNot, timeout });
}, expected, options);
} else {
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]);
return await locator._expect(customStackTrace, 'to.have.class', { expectedText, isNot, timeout });
return await locator._expect('to.have.class', { expectedText, isNot, timeout });
}, expected, options);
}
}
@ -186,8 +185,8 @@ export function toHaveCount(
expected: number,
options?: { timeout?: number },
) {
return toEqual.call(this, 'toHaveCount', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return await locator._expect(customStackTrace, 'to.have.count', { expectedNumber: expected, isNot, timeout });
return toEqual.call(this, 'toHaveCount', locator, 'Locator', async (isNot, timeout) => {
return await locator._expect('to.have.count', { expectedNumber: expected, isNot, timeout });
}, expected, options);
}
@ -198,9 +197,9 @@ export function toHaveCSS(
expected: string | RegExp,
options?: { timeout?: number },
) {
return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]);
return await locator._expect(customStackTrace, 'to.have.css', { expressionArg: name, expectedText, isNot, timeout });
return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, timeout });
}, expected, options);
}
@ -210,9 +209,9 @@ export function toHaveId(
expected: string | RegExp,
options?: { timeout?: number },
) {
return toMatchText.call(this, 'toHaveId', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toMatchText.call(this, 'toHaveId', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]);
return await locator._expect(customStackTrace, 'to.have.id', { expectedText, isNot, timeout });
return await locator._expect('to.have.id', { expectedText, isNot, timeout });
}, expected, options);
}
@ -223,8 +222,8 @@ export function toHaveJSProperty(
expected: any,
options?: { timeout?: number },
) {
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 });
return toEqual.call(this, 'toHaveJSProperty', locator, 'Locator', async (isNot, timeout) => {
return await locator._expect('to.have.property', { expressionArg: name, expectedValue: expected, isNot, timeout });
}, expected, options);
}
@ -235,14 +234,14 @@ export function toHaveText(
options: { timeout?: number, useInnerText?: boolean, ignoreCase?: boolean } = {},
) {
if (Array.isArray(expected)) {
return toEqual.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toEqual.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues(expected, { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase });
return await locator._expect(customStackTrace, 'to.have.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
return await locator._expect('to.have.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
}, expected, options);
} else {
return toMatchText.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toMatchText.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase });
return await locator._expect(customStackTrace, 'to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
}, expected, options);
}
}
@ -253,9 +252,9 @@ export function toHaveValue(
expected: string | RegExp,
options?: { timeout?: number },
) {
return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]);
return await locator._expect(customStackTrace, 'to.have.value', { expectedText, isNot, timeout });
return await locator._expect('to.have.value', { expectedText, isNot, timeout });
}, expected, options);
}
@ -267,7 +266,7 @@ export function toHaveValues(
) {
return toEqual.call(this, 'toHaveValues', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
const expectedText = toExpectedTextValues(expected);
return await locator._expect(customStackTrace, 'to.have.values', { expectedText, isNot, timeout });
return await locator._expect('to.have.values', { expectedText, isNot, timeout });
}, expected, options);
}
@ -278,9 +277,9 @@ export function toHaveTitle(
options: { timeout?: number } = {},
) {
const locator = page.locator(':root') as LocatorEx;
return toMatchText.call(this, 'toHaveTitle', locator, 'Locator', async (isNot, timeout, customStackTrace) => {
return toMatchText.call(this, 'toHaveTitle', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true });
return await locator._expect(customStackTrace, 'to.have.title', { expectedText, isNot, timeout });
return await locator._expect('to.have.title', { expectedText, isNot, timeout });
}, expected, options);
}
@ -293,9 +292,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, customStackTrace) => {
return toMatchText.call(this, 'toHaveURL', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]);
return await locator._expect(customStackTrace, 'to.have.url', { expectedText, isNot, timeout });
return await locator._expect('to.have.url', { expectedText, isNot, timeout });
}, expected, options);
}

View File

@ -15,8 +15,7 @@
*/
import type { Expect } from '../common/types';
import type { ParsedStackTrace } from '../util';
import { expectTypes, callLogText, captureStackTrace } from '../util';
import { expectTypes, callLogText } from '../util';
import { matcherHint } from './matcherHint';
import { currentExpectTimeout } from '../common/globals';
@ -25,7 +24,7 @@ export async function toBeTruthy(
matcherName: string,
receiver: any,
receiverType: string,
query: (isNot: boolean, timeout: number, customStackTrace: ParsedStackTrace) => Promise<{ matches: boolean, log?: string[], received?: any, timedOut?: boolean }>,
query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, log?: string[], received?: any, timedOut?: boolean }>,
options: { timeout?: number } = {},
) {
expectTypes(receiver, [receiverType], matcherName);
@ -37,7 +36,7 @@ export async function toBeTruthy(
const timeout = currentExpectTimeout(options);
const { matches, log, timedOut } = await query(this.isNot, timeout, captureStackTrace(`expect.${this.isNot ? 'not.' : ''}${matcherName}`));
const { matches, log, timedOut } = await query(this.isNot, timeout);
const message = () => {
return matcherHint(this, matcherName, undefined, '', matcherOptions, timedOut ? timeout : undefined) + callLogText(log);

View File

@ -25,7 +25,7 @@ import type { PageScreenshotOptions } from 'playwright-core/types/types';
import {
addSuffixToFilePath, serializeError, sanitizeForFilePath,
trimLongString, callLogText,
expectTypes, captureStackTrace } from '../util';
expectTypes } from '../util';
import { colors } from 'playwright-core/lib/utilsBundle';
import fs from 'fs';
import path from 'path';
@ -329,7 +329,6 @@ export async function toHaveScreenshot(
maxDiffPixelRatio: undefined,
};
const customStackTrace = captureStackTrace(`expect.${this.isNot ? 'not.' : ''}toHaveScreenshot`);
const hasSnapshot = fs.existsSync(helper.snapshotPath);
if (this.isNot) {
if (!hasSnapshot)
@ -338,7 +337,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(customStackTrace, {
const isDifferent = !(await page._expectScreenshot({
expected: await fs.promises.readFile(helper.snapshotPath),
isNot: true,
locator,
@ -359,7 +358,7 @@ export async function toHaveScreenshot(
if (!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(customStackTrace, {
const { actual, previous, diff, errorMessage, log } = await page._expectScreenshot({
expected: undefined,
isNot: false,
locator,
@ -381,7 +380,7 @@ export async function toHaveScreenshot(
// - regular matcher (i.e. not a `.not`)
// - perhaps an 'all' flag to update non-matching screenshots
const expected = await fs.promises.readFile(helper.snapshotPath);
const { actual, diff, errorMessage, log } = await page._expectScreenshot(customStackTrace, {
const { actual, diff, errorMessage, log } = await page._expectScreenshot({
expected,
isNot: false,
locator,

View File

@ -18,8 +18,7 @@
import type { ExpectedTextValue } from '@protocol/channels';
import { isRegExp, isString } from 'playwright-core/lib/utils';
import type { Expect } from '../common/types';
import type { ParsedStackTrace } from '../util';
import { expectTypes, callLogText, captureStackTrace } from '../util';
import { expectTypes, callLogText } from '../util';
import {
printReceivedStringContainExpectedResult,
printReceivedStringContainExpectedSubstring
@ -32,7 +31,7 @@ export async function toMatchText(
matcherName: string,
receiver: any,
receiverType: string,
query: (isNot: boolean, timeout: number, customStackTrace: ParsedStackTrace) => Promise<{ matches: boolean, received?: string, log?: string[], timedOut?: boolean }>,
query: (isNot: boolean, timeout: number) => Promise<{ matches: boolean, received?: string, log?: string[], timedOut?: boolean }>,
expected: string | RegExp,
options: { timeout?: number, matchSubstring?: boolean } = {},
) {
@ -60,7 +59,7 @@ export async function toMatchText(
const timeout = currentExpectTimeout(options);
const { matches: pass, received, log, timedOut } = await query(this.isNot, timeout, captureStackTrace(`expect.${this.isNot ? 'not.' : ''}${matcherName}`));
const { matches: pass, received, log, timedOut } = await query(this.isNot, timeout);
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
const receivedString = received || '';
const message = pass

View File

@ -21,67 +21,26 @@ import path from 'path';
import url from 'url';
import { colors, debug, minimatch } from 'playwright-core/lib/utilsBundle';
import type { TestInfoError, Location } from './common/types';
import { calculateSha1, isRegExp, isString, captureStackTrace as coreCaptureStackTrace } from 'playwright-core/lib/utils';
import { isInternalFileName } from 'playwright-core/lib/utils';
import { calculateSha1, captureStackTrace, isRegExp, isString } from 'playwright-core/lib/utils';
import type { ParsedStackTrace } from 'playwright-core/lib/utils';
export type { ParsedStackTrace };
const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core'));
const EXPECT_PATH = require.resolve('./common/expectBundle');
const EXPECT_PATH_IMPL = require.resolve('./common/expectBundleImpl');
const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..');
function filterStackTrace(e: Error) {
export function filterStackTrace(e: Error) {
if (process.env.PWDEBUGIMPL)
return;
// This method filters internal stack frames using Error.prepareStackTrace
// hook. Read more about the hook: https://v8.dev/docs/stack-trace-api
//
// NOTE: Error.prepareStackTrace will only be called if `e.stack` has not
// been accessed before. This is the case for Jest Expect and simple throw
// statements.
//
// If `e.stack` has been accessed, this method will be NOOP.
const oldPrepare = Error.prepareStackTrace;
const stackFormatter = oldPrepare || ((error, structuredStackTrace) => [
`${error.name}: ${error.message}`,
...structuredStackTrace.map(callSite => ' at ' + callSite.toString()),
].join('\n'));
Error.prepareStackTrace = (error, structuredStackTrace) => {
return stackFormatter(error, structuredStackTrace.filter(callSite => {
const fileName = callSite.getFileName();
const functionName = callSite.getFunctionName() || undefined;
if (!fileName)
return true;
return !fileName.startsWith(PLAYWRIGHT_TEST_PATH) &&
!fileName.startsWith(PLAYWRIGHT_CORE_PATH) &&
!isInternalFileName(fileName, functionName);
}));
};
// eslint-disable-next-line
e.stack; // trigger Error.prepareStackTrace
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 === EXPECT_PATH || frame.file === EXPECT_PATH_IMPL)
continue;
frames.push(frame);
frameTexts.push(stackTrace.frameTexts[i]);
}
return {
allFrames: stackTrace.allFrames,
frames,
frameTexts,
apiName: customApiName ?? stackTrace.apiName,
};
const stack = captureStackTrace(e.stack);
const stackLines = stack.frames.filter(f => !f.file.startsWith(PLAYWRIGHT_TEST_PATH)).map(f => {
if (f.function)
return ` at ${f.function} (${f.file}:${f.line}:${f.column})`;
return ` at ${f.file}:${f.line}:${f.column}`;
});
const message = e.message;
e.stack = `${e.name}: ${e.message}\n${stackLines.join('\n')}`;
e.message = message;
}
export function serializeError(error: Error | any): TestInfoError {

View File

@ -73,7 +73,7 @@ test.describe('toHaveCount', () => {
await page.setContent('<div><span></span></div>');
const locator = page.locator('span');
const error = await expect(locator).not.toHaveCount(1, { timeout: 1000 }).catch(e => e);
expect(error.message).toContain('expect.toHaveCount with timeout 1000ms');
expect(error.message).toContain('expect.not.toHaveCount with timeout 1000ms');
});
});

View File

@ -206,7 +206,7 @@ test.describe('toHaveText with array', () => {
await page.setContent('<div></div>');
const locator = page.locator('p');
const error = await expect(locator).not.toHaveText([], { timeout: 1000 }).catch(e => e);
expect(error.message).toContain('expect.toHaveText with timeout 1000ms');
expect(error.message).toContain('expect.not.toHaveText with timeout 1000ms');
});
test('pass eventually empty', async ({ page }) => {

View File

@ -333,7 +333,7 @@ test('should filter out syntax error stack traces', async ({ runInlineTest }, te
import { test, expect } from '@playwright/test';
test('should work', ({}) => {
// syntax error: cannot have await in non-async function
await Proimse.resolve();
await Promise.resolve();
});
`
});
@ -382,26 +382,6 @@ test('should not filter out POM', async ({ runInlineTest }) => {
expect(result.output).not.toContain('internal');
});
test('should filter stack even without default Error.prepareStackTrace', async ({ runInlineTest }) => {
const result = await runInlineTest({
'expect-test.spec.ts': `
import { test, expect } from '@playwright/test';
test('should work', ({}) => {
Error.prepareStackTrace = undefined;
throw new Error('foobar');
});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain('foobar');
expect(result.output).toContain('expect-test.spec.ts');
expect(result.output).not.toContain(path.sep + `playwright-test`);
expect(result.output).not.toContain(path.sep + `playwright-core`);
expect(result.output).not.toContain('internal');
const stackLines = result.output.split('\n').filter(line => line.includes(' at '));
expect(stackLines.length).toBe(1);
});
test('should work with cross-imports - 1', async ({ runInlineTest }) => {
const result = await runInlineTest({
'test1.spec.ts': `

View File

@ -283,7 +283,6 @@ test('should print errors with inconsistent message/stack', async ({ runInlineTe
// Otherwise it is computed lazy and will get 'foo bar' instead.
e.stack;
e.message = 'foo bar';
e.stack = 'hi!' + e.stack;
throw e;
});
`
@ -291,7 +290,7 @@ test('should print errors with inconsistent message/stack', async ({ runInlineTe
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
const output = result.output;
expect(output).toContain('hi!Error: Hello');
expect(output).toContain('foo bar');
expect(output).toContain('function myTest');
});