diff --git a/eslint.config.mjs b/eslint.config.mjs index 6fa060cf78..b5486d8760 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -177,7 +177,7 @@ const noBooleanCompareRules = { '@typescript-eslint/no-unnecessary-boolean-literal-compare': 2, }; -const noRestrictedGlobalsRules = { +const noWebGlobalsRules = { 'no-restricted-globals': [ 'error', { 'name': 'window' }, @@ -186,6 +186,13 @@ const noRestrictedGlobalsRules = { ], }; +const noNodeGlobalsRules = { + 'no-restricted-globals': [ + 'error', + { 'name': 'process' }, + ], +}; + const importOrderRules = { 'import/order': [2, { 'groups': ['builtin', 'external', 'internal', ['parent', 'sibling'], 'index', 'type'], @@ -249,7 +256,19 @@ export default [{ files: ['packages/playwright-core/src/server/injected/**/*.ts'], languageOptions: languageOptionsWithTsConfig, rules: { - ...noRestrictedGlobalsRules, + ...noWebGlobalsRules, + ...noFloatingPromisesRules, + ...noBooleanCompareRules, + } +}, { + files: [ + 'packages/playwright-core/src/client/**/*.ts', + 'packages/playwright-core/src/protocol/**/*.ts', + 'packages/playwright-core/src/utils/**/*.ts', + ], + languageOptions: languageOptionsWithTsConfig, + rules: { + ...noNodeGlobalsRules, ...noFloatingPromisesRules, ...noBooleanCompareRules, } diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 6c0ccd230c..4a9b4b9bb9 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -23,9 +23,9 @@ import * as path from 'path'; import * as playwright from '../..'; import { launchBrowserServer, printApiJson, runDriver, runServer } from './driver'; import { registry, writeDockerVersion } from '../server'; -import { gracefullyProcessExitDoNotHang } from '../utils'; +import { gracefullyProcessExitDoNotHang, isLikelyNpxGlobal } from '../utils'; import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer'; -import { assert, getPackageManagerExecCommand, isLikelyNpxGlobal } from '../utils'; +import { assert, getPackageManagerExecCommand } from '../utils'; import { wrapInASCIIBox } from '../server/utils/ascii'; import { dotenv, program } from '../utilsBundle'; diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index 91cf3220ac..4b3ca152ee 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -51,7 +51,9 @@ export class Android extends ChannelOwner implements ap setDefaultTimeout(timeout: number) { this._timeoutSettings.setDefaultTimeout(timeout); - this._channel.setDefaultTimeoutNoReply({ timeout }); + this._wrapApiCall(async () => { + await this._channel.setDefaultTimeoutNoReply({ timeout }); + }, true).catch(() => {}); } async devices(options: { port?: number } = {}): Promise { @@ -133,7 +135,9 @@ export class AndroidDevice extends ChannelOwner i setDefaultTimeout(timeout: number) { this._timeoutSettings.setDefaultTimeout(timeout); - this._channel.setDefaultTimeoutNoReply({ timeout }); + this._wrapApiCall(async () => { + await this._channel.setDefaultTimeoutNoReply({ timeout }); + }, true).catch(() => {}); } serial(): string { diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 5ed9262b02..445379083e 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -246,15 +246,15 @@ export class BrowserContext extends ChannelOwner setDefaultNavigationTimeout(timeout: number | undefined) { this._timeoutSettings.setDefaultNavigationTimeout(timeout); this._wrapApiCall(async () => { - this._channel.setDefaultNavigationTimeoutNoReply({ timeout }).catch(() => {}); - }, true); + await this._channel.setDefaultNavigationTimeoutNoReply({ timeout }); + }, true).catch(() => {}); } setDefaultTimeout(timeout: number | undefined) { this._timeoutSettings.setDefaultTimeout(timeout); this._wrapApiCall(async () => { - this._channel.setDefaultTimeoutNoReply({ timeout }).catch(() => {}); - }, true); + await this._channel.setDefaultTimeoutNoReply({ timeout }); + }, true).catch(() => {}); } browser(): Browser | null { @@ -559,7 +559,7 @@ export async function prepareBrowserContextParams(platform: Platform, options: B }; } if (contextParams.recordVideo && contextParams.recordVideo.dir) - contextParams.recordVideo.dir = platform.path().resolve(process.cwd(), contextParams.recordVideo.dir); + contextParams.recordVideo.dir = platform.path().resolve(contextParams.recordVideo.dir); return contextParams; } diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index e3b541ee9a..aaf8c6beb2 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -199,7 +199,7 @@ export abstract class ChannelOwner\n' + e.stack : ''; + const innerError = ((this._platform.showInternalStackFrames() || this._platform.isUnderTest()) && e.stack) ? '\n\n' + e.stack : ''; if (apiZone.apiName && !apiZone.apiName.includes('')) e.message = apiZone.apiName + ': ' + e.message; const stackFrames = '\n' + stringifyStackFrames(stackTrace.frames).join('\n') + innerError; diff --git a/packages/playwright-core/src/client/clientStackTrace.ts b/packages/playwright-core/src/client/clientStackTrace.ts index 8a18787517..3dc16dbae6 100644 --- a/packages/playwright-core/src/client/clientStackTrace.ts +++ b/packages/playwright-core/src/client/clientStackTrace.ts @@ -28,7 +28,7 @@ export function captureLibraryStackTrace(platform: Platform): { frames: StackFra isPlaywrightLibrary: boolean; }; let parsedFrames = stack.map(line => { - const frame = parseStackFrame(line, platform.pathSeparator); + const frame = parseStackFrame(line, platform.pathSeparator, platform.showInternalStackFrames()); if (!frame || !frame.file) return null; const isPlaywrightLibrary = !!platform.coreDir && frame.file.startsWith(platform.coreDir); @@ -62,10 +62,8 @@ export function captureLibraryStackTrace(platform: Platform): { frames: StackFra } // This is for the inspector so that it did not include the test runner stack frames. - const filterPrefixes = platform.coreDir ? [platform.coreDir, ...platform.boxedStackPrefixes()] : platform.boxedStackPrefixes(); + const filterPrefixes = platform.boxedStackPrefixes(); parsedFrames = parsedFrames.filter(f => { - if (process.env.PWDEBUGIMPL) - return true; if (filterPrefixes.some(prefix => f.frame.file.startsWith(prefix))) return false; return true; diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index f721d89f07..3d9241321d 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -54,7 +54,7 @@ export class Electron extends ChannelOwner implements async launch(options: ElectronOptions = {}): Promise { const params: channels.ElectronLaunchParams = { ...await prepareBrowserContextParams(this._platform, options), - env: envObjectToArray(options.env ? options.env : process.env), + env: envObjectToArray(options.env ? options.env : this._platform.env), tracesDir: options.tracesDir, }; const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication); diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 0aa5aeb667..b91b0b53cd 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -277,15 +277,15 @@ export class Page extends ChannelOwner implements api.Page setDefaultNavigationTimeout(timeout: number) { this._timeoutSettings.setDefaultNavigationTimeout(timeout); this._wrapApiCall(async () => { - this._channel.setDefaultNavigationTimeoutNoReply({ timeout }).catch(() => {}); - }, true); + await this._channel.setDefaultNavigationTimeoutNoReply({ timeout }); + }, true).catch(() => {}); } setDefaultTimeout(timeout: number) { this._timeoutSettings.setDefaultTimeout(timeout); this._wrapApiCall(async () => { - this._channel.setDefaultTimeoutNoReply({ timeout }).catch(() => {}); - }, true); + await this._channel.setDefaultTimeoutNoReply({ timeout }); + }, true).catch(() => {}); } private _forceVideo(): Video { diff --git a/packages/playwright-core/src/client/platform.ts b/packages/playwright-core/src/client/platform.ts index 31d6705378..e9d47c5692 100644 --- a/packages/playwright-core/src/client/platform.ts +++ b/packages/playwright-core/src/client/platform.ts @@ -45,6 +45,7 @@ export type Platform = { coreDir?: string; createGuid: () => string; defaultMaxListeners: () => number; + env: Record; fs: () => typeof fs; inspectCustom: symbol | undefined; isDebugMode: () => boolean; @@ -54,6 +55,7 @@ export type Platform = { log: (name: 'api' | 'channel', message: string | Error | object) => void; path: () => typeof path; pathSeparator: string; + showInternalStackFrames: () => boolean, streamFile: (path: string, writable: Writable) => Promise, streamReadable: (channel: channels.StreamChannel) => Readable, streamWritable: (channel: channels.WritableStreamChannel) => Writable, @@ -77,6 +79,8 @@ export const emptyPlatform: Platform = { defaultMaxListeners: () => 10, + env: {}, + fs: () => { throw new Error('Not implemented'); }, @@ -101,6 +105,8 @@ export const emptyPlatform: Platform = { pathSeparator: '/', + showInternalStackFrames: () => false, + streamFile(path: string, writable: Writable): Promise { throw new Error('Streams are not available'); }, diff --git a/packages/playwright-core/src/client/waiter.ts b/packages/playwright-core/src/client/waiter.ts index 8c01193d7a..e106ebd2bb 100644 --- a/packages/playwright-core/src/client/waiter.ts +++ b/packages/playwright-core/src/client/waiter.ts @@ -96,8 +96,8 @@ export class Waiter { log(s: string) { this._logs.push(s); this._channelOwner._wrapApiCall(async () => { - await this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'log', message: s } }).catch(() => {}); - }, true); + await this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'log', message: s } }); + }, true).catch(() => {}); } private _rejectOn(promise: Promise, dispose?: () => void) { diff --git a/packages/playwright-core/src/client/webSocket.ts b/packages/playwright-core/src/client/webSocket.ts index b4d8003789..2d3541b4f6 100644 --- a/packages/playwright-core/src/client/webSocket.ts +++ b/packages/playwright-core/src/client/webSocket.ts @@ -39,7 +39,7 @@ export async function connectOverWebSocket(parentConnection: Connection, params: connection!.dispatch(message); } catch (e) { closeError = String(e); - transport.close(); + transport.close().catch(() => {}); } }); return connection; @@ -70,7 +70,7 @@ class JsonPipeTransport implements Transport { } async send(message: object) { - this._owner._wrapApiCall(async () => { + await this._owner._wrapApiCall(async () => { await this._pipe!.send({ message }); }, /* isInternal */ true); } diff --git a/packages/playwright-core/src/server/utils/env.ts b/packages/playwright-core/src/server/utils/env.ts index 2a4dd0bfd4..500c180f56 100644 --- a/packages/playwright-core/src/server/utils/env.ts +++ b/packages/playwright-core/src/server/utils/env.ts @@ -47,3 +47,7 @@ export function getPackageManagerExecCommand() { return 'pnpm exec'; return 'npx'; } + +export function isLikelyNpxGlobal() { + return process.argv.length >= 2 && process.argv[1].includes('_npx'); +} diff --git a/packages/playwright-core/src/server/utils/nodePlatform.ts b/packages/playwright-core/src/server/utils/nodePlatform.ts index 4a36322036..395b903c51 100644 --- a/packages/playwright-core/src/server/utils/nodePlatform.ts +++ b/packages/playwright-core/src/server/utils/nodePlatform.ts @@ -61,10 +61,16 @@ export function setBoxedStackPrefixes(prefixes: string[]) { boxedStackPrefixes = prefixes; } +const coreDir = path.dirname(require.resolve('../../../package.json')); + export const nodePlatform: Platform = { name: 'node', - boxedStackPrefixes: () => boxedStackPrefixes, + boxedStackPrefixes: () => { + if (process.env.PWDEBUGIMPL) + return []; + return [coreDir, ...boxedStackPrefixes]; + }, calculateSha1: (text: string) => { const sha1 = crypto.createHash('sha1'); @@ -74,13 +80,15 @@ export const nodePlatform: Platform = { colors, - coreDir: path.dirname(require.resolve('../../../package.json')), + coreDir, createGuid: () => crypto.randomBytes(16).toString('hex'), defaultMaxListeners: () => EventEmitter.defaultMaxListeners, fs: () => fs, + env: process.env, + inspectCustom: util.inspect.custom, isDebugMode: () => !!debugMode(), @@ -101,6 +109,8 @@ export const nodePlatform: Platform = { pathSeparator: path.sep, + showInternalStackFrames: () => !!process.env.PWDEBUGIMPL, + async streamFile(path: string, stream: Writable): Promise { await pipelineAsync(fs.createReadStream(path), stream); }, diff --git a/packages/playwright-core/src/utils/isomorphic/rtti.ts b/packages/playwright-core/src/utils/isomorphic/rtti.ts index 0bcaef0748..ffef6c1cad 100644 --- a/packages/playwright-core/src/utils/isomorphic/rtti.ts +++ b/packages/playwright-core/src/utils/isomorphic/rtti.ts @@ -27,5 +27,3 @@ export function isObject(obj: any): obj is NonNullable { export function isError(obj: any): obj is Error { return obj instanceof Error || (obj && Object.getPrototypeOf(obj)?.name === 'Error'); } - -export const isLikelyNpxGlobal = () => process.argv.length >= 2 && process.argv[1].includes('_npx'); diff --git a/packages/playwright-core/src/utils/isomorphic/stackTrace.ts b/packages/playwright-core/src/utils/isomorphic/stackTrace.ts index 87d9e84eaa..f3d5a54da0 100644 --- a/packages/playwright-core/src/utils/isomorphic/stackTrace.ts +++ b/packages/playwright-core/src/utils/isomorphic/stackTrace.ts @@ -37,7 +37,7 @@ export function captureRawStack(): RawStack { return stack.split('\n'); } -export function parseStackFrame(text: string, pathSeparator: string): StackFrame | null { +export function parseStackFrame(text: string, pathSeparator: string, showInternalStackFrames: boolean): StackFrame | null { const match = text && text.match(re); if (!match) return null; @@ -46,7 +46,7 @@ export function parseStackFrame(text: string, pathSeparator: string): StackFrame let file = match[7]; if (!file) return null; - if (!process.env.PWDEBUGIMPL && (file.startsWith('internal') || file.startsWith('node:'))) + if (!showInternalStackFrames && (file.startsWith('internal') || file.startsWith('node:'))) return null; const line = match[8]; diff --git a/packages/playwright/src/reporters/base.ts b/packages/playwright/src/reporters/base.ts index 79d0d254a1..9708532cbb 100644 --- a/packages/playwright/src/reporters/base.ts +++ b/packages/playwright/src/reporters/base.ts @@ -522,7 +522,7 @@ export function prepareErrorStack(stack: string): { const stackLines = lines.slice(firstStackLine); let location: Location | undefined; for (const line of stackLines) { - const frame = parseStackFrame(line, path.sep); + const frame = parseStackFrame(line, path.sep, !!process.env.PWDEBUGIMPL); if (!frame || !frame.file) continue; if (belongsToNodeModules(frame.file)) diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index d10babff56..d919ae5be7 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -55,7 +55,7 @@ export function filterStackFile(file: string) { export function filteredStackTrace(rawStack: RawStack): StackFrame[] { const frames: StackFrame[] = []; for (const line of rawStack) { - const frame = parseStackFrame(line, path.sep); + const frame = parseStackFrame(line, path.sep, !!process.env.PWDEBUGIMPL); if (!frame || !frame.file) continue; if (!filterStackFile(frame.file))