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; 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 logger = this._logger;
const stack = captureRawStack(); const stack = captureRawStack();
const apiZone = zones.zoneData<ApiZone>('apiZone', stack); const apiZone = zones.zoneData<ApiZone>('apiZone', stack);
if (apiZone) if (apiZone)
return func(apiZone); return func(apiZone);
const stackTrace = customStackTrace || captureStackTrace(stack); const stackTrace = captureStackTrace(stack);
if (isInternal) if (isInternal)
delete stackTrace.apiName; delete stackTrace.apiName;
const csi = isInternal ? undefined : this._instrumentation; const csi = isInternal ? undefined : this._instrumentation;

View File

@ -17,7 +17,6 @@
import type * as structs from '../../types/structs'; import type * as structs from '../../types/structs';
import type * as api from '../../types/types'; import type * as api from '../../types/types';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import type { ParsedStackTrace } from '../utils/stackTrace';
import * as util from 'util'; import * as util from 'util';
import { monotonicTime } from '../utils'; import { monotonicTime } from '../utils';
import { ElementHandle } from './elementHandle'; 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 }); 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 }> { async _expect(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 };
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot }; params.expectedValue = serializeArgument(options.expectedValue);
params.expectedValue = serializeArgument(options.expectedValue); const result = (await this._frame._channel.expect(params));
const result = (await this._frame._channel.expect(params)); if (result.received !== undefined)
if (result.received !== undefined) result.received = parseResult(result.received);
result.received = parseResult(result.received); return result;
return result;
}, false /* isInternal */, customStackTrace);
} }
[util.inspect.custom]() { [util.inspect.custom]() {

View File

@ -26,7 +26,6 @@ import type * as channels from '@protocol/channels';
import { parseError, serializeError } from '../protocol/serializers'; import { parseError, serializeError } from '../protocol/serializers';
import { assert, headersObjectToArray, isObject, isRegExp, isString } from '../utils'; import { assert, headersObjectToArray, isObject, isRegExp, isString } from '../utils';
import { mkdirIfNeeded } from '../utils/fileUtils'; import { mkdirIfNeeded } from '../utils/fileUtils';
import type { ParsedStackTrace } from '../utils/stackTrace';
import { Accessibility } from './accessibility'; import { Accessibility } from './accessibility';
import { Artifact } from './artifact'; import { Artifact } from './artifact';
import type { BrowserContext } from './browserContext'; import type { BrowserContext } from './browserContext';
@ -498,26 +497,24 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
return result.binary; return result.binary;
} }
async _expectScreenshot(customStackTrace: ParsedStackTrace, options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[]}> { async _expectScreenshot(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 => ({
const mask = options.screenshotOptions?.mask ? options.screenshotOptions?.mask.map(locator => ({ frame: locator._frame._channel,
frame: locator._frame._channel, selector: locator._selector,
selector: locator._selector, })) : undefined;
})) : undefined; const locator = options.locator ? {
const locator = options.locator ? { frame: options.locator._frame._channel,
frame: options.locator._frame._channel, selector: options.locator._selector,
selector: options.locator._selector, } : undefined;
} : undefined; return await this._channel.expectScreenshot({
return await this._channel.expectScreenshot({ ...options,
...options, isNot: !!options.isNot,
isNot: !!options.isNot, locator,
locator, screenshotOptions: {
screenshotOptions: { ...options.screenshotOptions,
...options.screenshotOptions, mask,
mask, }
} });
});
}, false /* isInternal */, customStackTrace);
} }
async title(): Promise<string> { 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_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 COVERAGE_PATH = path.join(CORE_DIR, '..', '..', 'tests', 'config', 'coverage.js');
const stackIgnoreFilters = [ const internalStackPrefixes = [
(frame: StackFrame) => frame.file.startsWith(CORE_DIR), CORE_DIR,
]; ];
export const addStackIgnoreFilter = (filter: (frame: StackFrame) => boolean) => stackIgnoreFilters.push(filter); export const addInternalStackPrefix = (prefix: string) => internalStackPrefixes.push(prefix);
export type StackFrame = { export type StackFrame = {
file: string, file: string,
@ -60,7 +58,7 @@ export function captureRawStack(): string {
return stack; return stack;
} }
export function isInternalFileName(file: string, functionName?: string): boolean { function isInternalFileName(file: string, functionName?: string): boolean {
// Node 16+ has node:internal. // Node 16+ has node:internal.
if (file.startsWith('internal') || file.startsWith('node:')) if (file.startsWith('internal') || file.startsWith('node:'))
return true; return true;
@ -77,7 +75,7 @@ export function captureStackTrace(rawStack?: string): ParsedStackTrace {
type ParsedFrame = { type ParsedFrame = {
frame: StackFrame; frame: StackFrame;
frameText: string; frameText: string;
inCore: boolean; isPlaywrightLibrary: boolean;
}; };
let parsedFrames = stack.split('\n').map(line => { let parsedFrames = stack.split('\n').map(line => {
const { frame, fileName } = parseStackTraceLine(line); const { frame, fileName } = parseStackTraceLine(line);
@ -87,7 +85,7 @@ export function captureStackTrace(rawStack?: string): ParsedStackTrace {
return null; return null;
if (!process.env.PWDEBUGIMPL && isTesting && fileName.includes(COVERAGE_PATH)) if (!process.env.PWDEBUGIMPL && isTesting && fileName.includes(COVERAGE_PATH))
return null; return null;
const inCore = fileName.startsWith(CORE_LIB) || fileName.startsWith(CORE_SRC); const isPlaywrightLibrary = fileName.startsWith(CORE_DIR);
const parsed: ParsedFrame = { const parsed: ParsedFrame = {
frame: { frame: {
file: fileName, file: fileName,
@ -96,21 +94,29 @@ export function captureStackTrace(rawStack?: string): ParsedStackTrace {
function: frame.function, function: frame.function,
}, },
frameText: line, frameText: line,
inCore isPlaywrightLibrary
}; };
return parsed; return parsed;
}).filter(Boolean) as ParsedFrame[]; }).filter(Boolean) as ParsedFrame[];
let apiName = ''; let apiName = '';
const allFrames = parsedFrames; 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++) { for (let i = 0; i < parsedFrames.length - 1; i++) {
if (parsedFrames[i].inCore && !parsedFrames[i + 1].inCore) { const parsedFrame = parsedFrames[i];
const frame = parsedFrames[i].frame; if (parsedFrame.isPlaywrightLibrary && !parsedFrames[i + 1].isPlaywrightLibrary) {
apiName = normalizeAPIName(frame.function); apiName = apiName || normalizeAPIName(parsedFrame.frame.function);
if (!process.env.PWDEBUGIMPL)
parsedFrames = parsedFrames.slice(i + 1);
break; break;
} }
} }
@ -124,11 +130,11 @@ export function captureStackTrace(rawStack?: string): ParsedStackTrace {
return match[1].toLowerCase() + match[2]; return match[1].toLowerCase() + match[2];
} }
// Hide all test runner and library frames in the user stack (event handlers produce them). // This is for the inspector so that it did not include the test runner stack frames.
parsedFrames = parsedFrames.filter((f, i) => { parsedFrames = parsedFrames.filter(f => {
if (process.env.PWDEBUGIMPL) if (process.env.PWDEBUGIMPL)
return true; return true;
if (stackIgnoreFilters.some(filter => filter(f.frame))) if (internalStackPrefixes.some(prefix => f.frame.file.startsWith(prefix)))
return false; return false;
return true; return true;
}); });

View File

@ -18,7 +18,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core'; import type { APIRequestContext, BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
import * as playwrightLibrary 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 { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, TraceMode, VideoMode } from '../types/test';
import type { TestInfoImpl } from './worker/testInfo'; import type { TestInfoImpl } from './worker/testInfo';
import { rootTestType } from './common/testType'; import { rootTestType } from './common/testType';
@ -27,7 +27,7 @@ export { expect } from './matchers/expect';
export { store } from './store'; export { store } from './store';
export const _baseTest: TestType<{}, {}> = rootTestType.test; 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__']) { if ((process as any)['__pw_initiator__']) {
const originalStackTraceLimit = Error.stackTraceLimit; const originalStackTraceLimit = Error.stackTraceLimit;

View File

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

View File

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

View File

@ -15,8 +15,7 @@
*/ */
import type { Expect } from '../common/types'; import type { Expect } from '../common/types';
import type { ParsedStackTrace } from '../util'; import { expectTypes, callLogText } from '../util';
import { expectTypes, callLogText, captureStackTrace } from '../util';
import { matcherHint } from './matcherHint'; import { matcherHint } from './matcherHint';
import { currentExpectTimeout } from '../common/globals'; import { currentExpectTimeout } from '../common/globals';
@ -25,7 +24,7 @@ export async function toBeTruthy(
matcherName: string, matcherName: string,
receiver: any, receiver: any,
receiverType: string, 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 } = {}, options: { timeout?: number } = {},
) { ) {
expectTypes(receiver, [receiverType], matcherName); expectTypes(receiver, [receiverType], matcherName);
@ -37,7 +36,7 @@ export async function toBeTruthy(
const timeout = currentExpectTimeout(options); 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 = () => { const message = () => {
return matcherHint(this, matcherName, undefined, '', matcherOptions, timedOut ? timeout : undefined) + callLogText(log); 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 { import {
addSuffixToFilePath, serializeError, sanitizeForFilePath, addSuffixToFilePath, serializeError, sanitizeForFilePath,
trimLongString, callLogText, trimLongString, callLogText,
expectTypes, captureStackTrace } from '../util'; expectTypes } from '../util';
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
@ -329,7 +329,6 @@ export async function toHaveScreenshot(
maxDiffPixelRatio: undefined, maxDiffPixelRatio: undefined,
}; };
const customStackTrace = captureStackTrace(`expect.${this.isNot ? 'not.' : ''}toHaveScreenshot`);
const hasSnapshot = fs.existsSync(helper.snapshotPath); const hasSnapshot = fs.existsSync(helper.snapshotPath);
if (this.isNot) { if (this.isNot) {
if (!hasSnapshot) if (!hasSnapshot)
@ -338,7 +337,7 @@ export async function toHaveScreenshot(
// Having `errorMessage` means we timed out while waiting // Having `errorMessage` means we timed out while waiting
// for screenshots not to match, so screenshots // for screenshots not to match, so screenshots
// are actually the same in the end. // 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), expected: await fs.promises.readFile(helper.snapshotPath),
isNot: true, isNot: true,
locator, locator,
@ -359,7 +358,7 @@ export async function toHaveScreenshot(
if (!hasSnapshot) { if (!hasSnapshot) {
// Regenerate a new screenshot by waiting until two screenshots are the same. // Regenerate a new screenshot by waiting until two screenshots are the same.
const timeout = currentExpectTimeout(helper.allOptions); 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, expected: undefined,
isNot: false, isNot: false,
locator, locator,
@ -381,7 +380,7 @@ export async function toHaveScreenshot(
// - regular matcher (i.e. not a `.not`) // - regular matcher (i.e. not a `.not`)
// - perhaps an 'all' flag to update non-matching screenshots // - perhaps an 'all' flag to update non-matching screenshots
const expected = await fs.promises.readFile(helper.snapshotPath); 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, expected,
isNot: false, isNot: false,
locator, locator,

View File

@ -18,8 +18,7 @@
import type { ExpectedTextValue } from '@protocol/channels'; import type { ExpectedTextValue } from '@protocol/channels';
import { isRegExp, isString } from 'playwright-core/lib/utils'; import { isRegExp, isString } from 'playwright-core/lib/utils';
import type { Expect } from '../common/types'; import type { Expect } from '../common/types';
import type { ParsedStackTrace } from '../util'; import { expectTypes, callLogText } from '../util';
import { expectTypes, callLogText, captureStackTrace } from '../util';
import { import {
printReceivedStringContainExpectedResult, printReceivedStringContainExpectedResult,
printReceivedStringContainExpectedSubstring printReceivedStringContainExpectedSubstring
@ -32,7 +31,7 @@ export async function toMatchText(
matcherName: string, matcherName: string,
receiver: any, receiver: any,
receiverType: string, 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, expected: string | RegExp,
options: { timeout?: number, matchSubstring?: boolean } = {}, options: { timeout?: number, matchSubstring?: boolean } = {},
) { ) {
@ -60,7 +59,7 @@ export async function toMatchText(
const timeout = currentExpectTimeout(options); 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 stringSubstring = options.matchSubstring ? 'substring' : 'string';
const receivedString = received || ''; const receivedString = received || '';
const message = pass const message = pass

View File

@ -21,67 +21,26 @@ import path from 'path';
import url from 'url'; import url from 'url';
import { colors, debug, minimatch } from 'playwright-core/lib/utilsBundle'; import { colors, debug, minimatch } from 'playwright-core/lib/utilsBundle';
import type { TestInfoError, Location } from './common/types'; import type { TestInfoError, Location } from './common/types';
import { calculateSha1, isRegExp, isString, captureStackTrace as coreCaptureStackTrace } from 'playwright-core/lib/utils'; import { calculateSha1, captureStackTrace, isRegExp, isString } from 'playwright-core/lib/utils';
import { isInternalFileName } from 'playwright-core/lib/utils';
import type { ParsedStackTrace } from 'playwright-core/lib/utils'; import type { ParsedStackTrace } from 'playwright-core/lib/utils';
export type { ParsedStackTrace }; 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, '..'); const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..');
function filterStackTrace(e: Error) { export function filterStackTrace(e: Error) {
if (process.env.PWDEBUGIMPL) if (process.env.PWDEBUGIMPL)
return; return;
// This method filters internal stack frames using Error.prepareStackTrace const stack = captureStackTrace(e.stack);
// hook. Read more about the hook: https://v8.dev/docs/stack-trace-api const stackLines = stack.frames.filter(f => !f.file.startsWith(PLAYWRIGHT_TEST_PATH)).map(f => {
// if (f.function)
// NOTE: Error.prepareStackTrace will only be called if `e.stack` has not return ` at ${f.function} (${f.file}:${f.line}:${f.column})`;
// been accessed before. This is the case for Jest Expect and simple throw return ` at ${f.file}:${f.line}:${f.column}`;
// statements. });
// const message = e.message;
// If `e.stack` has been accessed, this method will be NOOP. e.stack = `${e.name}: ${e.message}\n${stackLines.join('\n')}`;
const oldPrepare = Error.prepareStackTrace; e.message = message;
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,
};
} }
export function serializeError(error: Error | any): TestInfoError { export function serializeError(error: Error | any): TestInfoError {

View File

@ -73,7 +73,7 @@ test.describe('toHaveCount', () => {
await page.setContent('<div><span></span></div>'); await page.setContent('<div><span></span></div>');
const locator = page.locator('span'); const locator = page.locator('span');
const error = await expect(locator).not.toHaveCount(1, { timeout: 1000 }).catch(e => e); 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>'); await page.setContent('<div></div>');
const locator = page.locator('p'); const locator = page.locator('p');
const error = await expect(locator).not.toHaveText([], { timeout: 1000 }).catch(e => e); 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 }) => { 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'; import { test, expect } from '@playwright/test';
test('should work', ({}) => { test('should work', ({}) => {
// syntax error: cannot have await in non-async function // 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'); 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 }) => { test('should work with cross-imports - 1', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'test1.spec.ts': ` '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. // Otherwise it is computed lazy and will get 'foo bar' instead.
e.stack; e.stack;
e.message = 'foo bar'; e.message = 'foo bar';
e.stack = 'hi!' + e.stack;
throw e; throw e;
}); });
` `
@ -291,7 +290,7 @@ test('should print errors with inconsistent message/stack', async ({ runInlineTe
expect(result.exitCode).toBe(1); expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1); expect(result.failed).toBe(1);
const output = result.output; const output = result.output;
expect(output).toContain('hi!Error: Hello'); expect(output).toContain('foo bar');
expect(output).toContain('function myTest'); expect(output).toContain('function myTest');
}); });