mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-02 10:34:27 +03:00
parent
c69a7424b4
commit
06fc72b6ed
@ -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;
|
||||
|
@ -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]() {
|
||||
|
@ -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> {
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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 }) => {
|
||||
|
@ -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': `
|
||||
|
@ -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');
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user