diff --git a/packages/isomorphic/stackTrace.ts b/packages/isomorphic/stackTrace.ts index f6c906e0613bf..a973a0a465573 100644 --- a/packages/isomorphic/stackTrace.ts +++ b/packages/isomorphic/stackTrace.ts @@ -37,7 +37,7 @@ export function captureRawStack(): RawStack { return stack.split('\n'); } -export function parseStackFrame(text: string, pathSeparator: string, showInternalStackFrames: boolean): StackFrame | null { +export function parseStackFrame(text: string, pathSeparator: string): StackFrame | null { const match = text && text.match(re); if (!match) return null; @@ -46,7 +46,7 @@ export function parseStackFrame(text: string, pathSeparator: string, showInterna let file = match[7]; if (!file) return null; - if (!showInternalStackFrames && (file.startsWith('internal') || file.startsWith('node:'))) + if (!_showInternalStackFrames && (file.startsWith('internal') || file.startsWith('node:'))) return null; const line = match[8]; @@ -134,7 +134,7 @@ export function splitErrorMessage(message: string): { name: string, message: str }; } -export function parseErrorStack(stack: string, pathSeparator: string, showInternalStackFrames: boolean = false): { +export function parseErrorStack(stack: string, pathSeparator: string): { message: string; stackLines: string[]; location?: StackFrame; @@ -147,7 +147,7 @@ export function parseErrorStack(stack: string, pathSeparator: string, showIntern const stackLines = lines.slice(firstStackLine); let location: StackFrame | undefined; for (const line of stackLines) { - const frame = parseStackFrame(line, pathSeparator, showInternalStackFrames); + const frame = parseStackFrame(line, pathSeparator); if (!frame || !frame.file) continue; if (belongsToNodeModules(frame.file, pathSeparator)) @@ -162,6 +162,29 @@ function belongsToNodeModules(file: string, pathSeparator: string) { return file.includes(`${pathSeparator}node_modules${pathSeparator}`); } +export function filterStackFile(file: string) { + if (_showInternalStackFrames) + return true; + if (!!_coreDir && file.startsWith(_coreDir)) + return false; + if (_boxedStackPrefixes.some(prefix => file.startsWith(prefix))) + return false; + return true; +} + +export function filteredStackTrace(rawStack: RawStack, pathSeparator: string): StackFrame[] { + const frames: StackFrame[] = []; + for (const line of rawStack) { + const frame = parseStackFrame(line, pathSeparator); + if (!frame || !frame.file) + continue; + if (!filterStackFile(frame.file)) + continue; + frames.push(frame); + } + return frames; +} + const re = new RegExp('^' + // Sometimes we strip out the ' at' because it's noisy '(?:\\s*at )?' + @@ -216,12 +239,6 @@ export function setBoxedStackPrefixes(prefixes: string[]) { _boxedStackPrefixes = prefixes; } -export function boxedStackPrefixes(): string[] { - if (_showInternalStackFrames) - return []; - return _coreDir ? [_coreDir, ..._boxedStackPrefixes] : _boxedStackPrefixes.slice(); -} - export function setShowInternalStackFrames(value: boolean) { _showInternalStackFrames = value; } diff --git a/packages/playwright-core/src/client/clientStackTrace.ts b/packages/playwright-core/src/client/clientStackTrace.ts index 3f2e8fcd73219..7e55614ec1c95 100644 --- a/packages/playwright-core/src/client/clientStackTrace.ts +++ b/packages/playwright-core/src/client/clientStackTrace.ts @@ -16,7 +16,7 @@ import path from 'path'; -import { boxedStackPrefixes, captureRawStack, coreDir, parseStackFrame, showInternalStackFrames } from '@isomorphic/stackTrace'; +import { captureRawStack, coreDir, filterStackFile, parseStackFrame } from '@isomorphic/stackTrace'; import type { StackFrame } from '@isomorphic/stackTrace'; @@ -30,7 +30,7 @@ export function captureLibraryStackTrace(): { frames: StackFrame[], apiName: str isPlaywrightLibrary: boolean; }; let parsedFrames = stack.map(line => { - const frame = parseStackFrame(line, path.sep, showInternalStackFrames()); + const frame = parseStackFrame(line, path.sep); if (!frame || !frame.file) return null; const isPlaywrightLibrary = !!playwrightCoreDir && frame.file.startsWith(playwrightCoreDir); @@ -65,12 +65,7 @@ export function captureLibraryStackTrace(): { frames: StackFrame[], apiName: str } // This is for the inspector so that it did not include the test runner stack frames. - const filterPrefixes = boxedStackPrefixes(); - parsedFrames = parsedFrames.filter(f => { - if (filterPrefixes.some(prefix => f.frame.file.startsWith(prefix))) - return false; - return true; - }); + parsedFrames = parsedFrames.filter(f => filterStackFile(f.frame.file)); return { frames: parsedFrames.map(p => p.frame), diff --git a/packages/playwright/src/common/fixtures.ts b/packages/playwright/src/common/fixtures.ts index b47c3f7d6a01c..64c95426fbaba 100644 --- a/packages/playwright/src/common/fixtures.ts +++ b/packages/playwright/src/common/fixtures.ts @@ -15,8 +15,9 @@ */ import crypto from 'crypto'; +import { filterStackFile } from '@isomorphic/stackTrace'; -import { filterStackFile, formatLocation } from '../util'; +import { formatLocation } from '../util'; import type { FixturesWithLocation } from './config'; import type { Fixtures } from '../../types/test'; diff --git a/packages/playwright/src/common/process.ts b/packages/playwright/src/common/process.ts index 49ab3805d5cb3..ce722bdb234be 100644 --- a/packages/playwright/src/common/process.ts +++ b/packages/playwright/src/common/process.ts @@ -19,7 +19,9 @@ import 'playwright-core/lib/bootstrap'; import { ManualPromise } from '@isomorphic/manualPromise'; import { setTimeOrigin } from '@isomorphic/time'; import { startProfiling, stopProfiling } from '@utils/profiler'; +import { setBoxedStackPrefixes } from '@isomorphic/stackTrace'; +import { packageRoot } from '../package'; import { serializeError } from '../util'; import type { EnvProducedPayload, ProcessInitParams, TestInfoErrorPayload } from './ipc'; @@ -61,6 +63,7 @@ let forceExitInitiated = false; let processRunner: ProcessRunner | undefined; let processName: string | undefined; const startingEnv = { ...process.env }; +setBoxedStackPrefixes([packageRoot]); export function startProcessRunner(create: (params: any) => ProcessRunner) { sendMessageToParent({ method: 'ready' }); diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index c53f7a29e284f..d9be54325effd 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -20,7 +20,6 @@ import path from 'path'; import * as playwrightLibrary from 'playwright-core'; import { asLocatorDescription } from '@isomorphic/locatorGenerators'; import { getActionGroup, renderTitleForCall } from '@isomorphic/protocolFormatter'; -import { setBoxedStackPrefixes } from '@isomorphic/stackTrace'; import { escapeHTML } from '@isomorphic/stringUtils'; import { jsonStringifyForceASCII } from '@utils/ascii'; import { createGuid } from '@utils/crypto'; @@ -29,7 +28,6 @@ import { currentZone } from '@utils/zones'; import { buildErrorContext } from './errorContext'; import { config, testType } from './common'; import * as globals from './globals'; -import { packageRoot } from './package'; import { createCustomMessageHandler, runDaemonForContext } from './mcp/test/browserBackend'; import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; @@ -46,8 +44,6 @@ import type { BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracin export { expect } from './matchers/expect'; export const _baseTest: TestType<{}, {}> = testType.rootTestType.test; -setBoxedStackPrefixes([packageRoot]); - if ((process as any)['__pw_initiator__']) { const originalStackTraceLimit = Error.stackTraceLimit; Error.stackTraceLimit = 200; diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 412196154e0bf..79255f2227b4c 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -115,7 +115,7 @@ export interface ExpectTestInfo { export type ExpectConfig = { testInfo: ExpectTestInfo | null; - filteredStackTrace: (rawStack: string[]) => StackFrame[]; + filteredStackTrace: (rawStack: string[], pathSeparator: string) => StackFrame[]; ignoreSnapshots: boolean; updateSnapshots: 'all' | 'changed' | 'missing' | 'none'; timeout?: number; @@ -143,8 +143,8 @@ export type ExpectConfig = { toPass?: { timeout?: number; intervals?: number[] }; }; -function unfilteredStackTrace(rawStack: string[]): StackFrame[] { - return rawStack.map(frame => parseStackFrame(frame, path.sep, !!process.env.PWDEBUGIMPL)).filter(f => !!f); +function unfilteredStackTrace(rawStack: string[], pathSeparator: string): StackFrame[] { + return rawStack.map(frame => parseStackFrame(frame, pathSeparator)).filter(f => !!f); } let _expectConfig: ExpectConfig = { testInfo: null, filteredStackTrace: unfilteredStackTrace, ignoreSnapshots: false, updateSnapshots: 'missing' }; @@ -339,7 +339,7 @@ function callMatcherAsStep(matcherName: string, info: ExpectMetaInfo, actual: un // This looks like it is unnecessary, but it isn't - we need to filter // out all the frames that belong to the test runner from caught runtime errors. - const stackFrames = expectConfig().filteredStackTrace(captureRawStack()); + const stackFrames = expectConfig().filteredStackTrace(captureRawStack(), path.sep); const stepData = { category: 'expect' as const, apiName, diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 242f7eee8b7e5..109aa1e54ae30 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -20,19 +20,21 @@ import 'playwright-core/lib/bootstrap'; import { libCli, tools } from 'playwright-core/lib/coreBundle'; import { program } from 'commander'; +import { setBoxedStackPrefixes } from '@isomorphic/stackTrace'; import { gracefullyProcessExitDoNotHang } from '@utils/processLauncher'; import { builtInReporters, config, configLoader } from './common'; import { runTests, clearCache, runTestServerAction } from './cli/testActions'; import { showReport, mergeReports } from './cli/reportActions'; import { TestServerBackend, testServerBackendTools } from './mcp/test/testBackend'; import { ClaudeGenerator, CodexGenerator, OpencodeGenerator, VSCodeGenerator, CopilotGenerator } from './agents/generateAgents'; -import { packageJSON } from './package'; +import { packageRoot, packageJSON } from './package'; export { program }; import type { TraceMode } from '../types/test'; import type { Command } from 'commander'; +setBoxedStackPrefixes([packageRoot]); libCli.decorateProgram(program); function addTestCommand(program: Command) { diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index e1d0737af599b..cc1886d7f98fc 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -631,7 +631,7 @@ export function prepareErrorStack(stack: string): { stackLines: string[]; location?: Location; } { - return parseErrorStack(stack, path.sep, !!process.env.PWDEBUGIMPL); + return parseErrorStack(stack, path.sep); } function resolveFromEnv(name: string): string | undefined { diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index c0c36ec2524b4..2fe4eb8b47622 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -24,24 +24,20 @@ import minimatch from 'minimatch'; import { calculateSha1 } from '@utils/crypto'; import { sanitizeForFilePath } from '@utils/fileUtils'; import { isRegExp } from '@isomorphic/rtti'; -import { parseStackFrame, stringifyStackFrames } from '@isomorphic/stackTrace'; +import { stringifyStackFrames, filteredStackTrace } from '@isomorphic/stackTrace'; import { ansiRegex, isString, stripAnsiEscapes } from '@isomorphic/stringUtils'; -import type { RawStack, StackFrame } from '@isomorphic/stackTrace'; import type { Location } from './../types/testReporter'; import type { TestInfoError } from './../types/test'; import type { TestCase } from './common/test'; -const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..'); -const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json')); - -export function filterStackTrace(e: Error): { message: string, stack: string, cause?: ReturnType } { +export function filterStackTrace(e: Error): TestInfoError { const name = e.name ? e.name + ': ' : ''; const cause = e.cause instanceof Error ? filterStackTrace(e.cause) : undefined; if (process.env.PWDEBUGIMPL) return { message: name + e.message, stack: e.stack || '', cause }; - const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || [])); + const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || [], path.sep)); return { message: name + e.message, stack: `${name}${e.message}${stackLines.map(line => '\n' + line).join('')}`, @@ -49,29 +45,6 @@ export function filterStackTrace(e: Error): { message: string, stack: string, ca }; } -export function filterStackFile(file: string) { - if (process.env.PWDEBUGIMPL) - return true; - if (file.startsWith(PLAYWRIGHT_TEST_PATH)) - return false; - if (file.startsWith(PLAYWRIGHT_CORE_PATH)) - return false; - return true; -} - -export function filteredStackTrace(rawStack: RawStack): StackFrame[] { - const frames: StackFrame[] = []; - for (const line of rawStack) { - const frame = parseStackFrame(line, path.sep, !!process.env.PWDEBUGIMPL); - if (!frame || !frame.file) - continue; - if (!filterStackFile(frame.file)) - continue; - frames.push(frame); - } - return frames; -} - export function serializeError(error: Error | any): TestInfoError { if (error instanceof Error) return filterStackTrace(error); diff --git a/packages/playwright/src/worker/fixtureRunner.ts b/packages/playwright/src/worker/fixtureRunner.ts index acba16fa278d2..b13fd9feaf499 100644 --- a/packages/playwright/src/worker/fixtureRunner.ts +++ b/packages/playwright/src/worker/fixtureRunner.ts @@ -16,9 +16,10 @@ import { ManualPromise } from '@isomorphic/manualPromise'; import { escapeWithQuotes } from '@isomorphic/stringUtils'; +import { filterStackFile } from '@isomorphic/stackTrace'; import { fixtures } from '../common'; -import { filterStackFile, formatLocation } from '../util'; +import { formatLocation } from '../util'; import type { TestInfoImpl } from './testInfo'; import type { FixtureDescription, RunnableDescription } from './timeoutManager'; diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 58ee006bee26a..546c63cfd9a2a 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import path from 'path'; import { ManualPromise } from '@isomorphic/manualPromise'; -import { captureRawStack, stringifyStackFrames } from '@isomorphic/stackTrace'; +import { captureRawStack, stringifyStackFrames, filteredStackTrace } from '@isomorphic/stackTrace'; import { escapeWithQuotes } from '@isomorphic/stringUtils'; import { monotonicTime } from '@isomorphic/time'; import { createGuid } from '@utils/crypto'; @@ -26,7 +26,7 @@ import { sanitizeForFilePath, trimLongString } from '@utils/fileUtils'; import { currentZone } from '@utils/zones'; import { TimeoutManager, TimeoutManagerError } from './timeoutManager'; -import { addSuffixToFilePath, filteredStackTrace, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, windowsFilesystemFriendlyLength } from '../util'; +import { addSuffixToFilePath, getContainedPath, normalizeAndSaveAttachment, sanitizeFilePathBeforeExtension, windowsFilesystemFriendlyLength } from '../util'; import { TestTracing } from './testTracing'; import { testInfoError } from './util'; import { ipc, transform } from '../common'; @@ -295,7 +295,7 @@ export class TestInfoImpl implements TestInfo { parentStep = this._parentStep(); } - const filteredStack = filteredStackTrace(captureRawStack()); + const filteredStack = filteredStackTrace(captureRawStack(), path.sep); let boxedStack = parentStep?.boxedStack; let location = data.location; if (!boxedStack && data.box) { diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index d6373496f55ba..049eae8bfbfd9 100644 --- a/packages/playwright/src/worker/testTracing.ts +++ b/packages/playwright/src/worker/testTracing.ts @@ -24,8 +24,7 @@ import { monotonicTime } from '@isomorphic/time'; import { calculateSha1, createGuid } from '@utils/crypto'; import { SerializedFS } from '@utils/serializedFS'; import { getPlaywrightVersion } from 'playwright-core/lib/coreBundle'; - -import { filteredStackTrace } from '../util'; +import { filteredStackTrace } from '@isomorphic/stackTrace'; import type { TestStepCategory, TestInfoImpl } from './testInfo'; import type { PlaywrightWorkerOptions, TestInfo, TestInfoError, TraceMode } from '../../types/test'; @@ -251,7 +250,7 @@ export class TestTracing { appendForError(error: TestInfoError) { const rawStack = error.stack?.split('\n') || []; - const stack = rawStack ? filteredStackTrace(rawStack) : []; + const stack = rawStack ? filteredStackTrace(rawStack, path.sep) : []; this._appendTraceEvent({ type: 'error', message: this._formatError(error), diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index 22e8f46451639..a566e37f217b9 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -18,11 +18,12 @@ import colors from 'colors/safe'; import { ManualPromise } from '@isomorphic/manualPromise'; import { removeFolders } from '@utils/fileUtils'; import { gracefullyCloseAll } from '@utils/processLauncher'; +import { filteredStackTrace } from '@isomorphic/stackTrace'; import { configLoader, fixtures, ipc, poolBuilder, ProcessRunner, suiteUtils, testLoader } from '../common'; import * as globals from '../globals'; import { setExpectConfig } from '../matchers/expect'; -import { debugTest, filteredStackTrace, relativeFilePath } from '../util'; +import { debugTest, relativeFilePath } from '../util'; import { FixtureRunner } from './fixtureRunner'; import { TestSkipError, TestInfoImpl, emtpyTestInfoCallbacks } from './testInfo'; import { testInfoError } from './util';