diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 0e66ab3d0f6a7..e8079b4293dc2 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -18,11 +18,12 @@ import fs from 'fs'; import path from 'path'; import * as playwrightLibrary from 'playwright-core'; -import { setBoxedStackPrefixes, createGuid, currentZone, debugMode, jsonStringifyForceASCII, asLocatorDescription, renderTitleForCall, getActionGroup } from 'playwright-core/lib/utils'; +import { createGuid, currentZone, debugMode, jsonStringifyForceASCII, asLocatorDescription, renderTitleForCall, getActionGroup } from 'playwright-core/lib/utils'; import { currentTestInfo } from './common/globals'; import { rootTestType } from './common/testType'; import { runBrowserBackendAtEnd } from './mcp/test/browserBackend'; +import { initPlaywrightTest } from './util'; import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test'; import type { ContextReuseMode } from './common/config'; @@ -39,19 +40,7 @@ import type { BrowserContext, BrowserContextOptions, LaunchOptions, Page, Tracin export { expect } from './matchers/expect'; export const _baseTest: TestType<{}, {}> = rootTestType.test; -setBoxedStackPrefixes([path.dirname(require.resolve('../package.json'))]); - -if ((process as any)['__pw_initiator__']) { - const originalStackTraceLimit = Error.stackTraceLimit; - Error.stackTraceLimit = 200; - try { - throw new Error('Requiring @playwright/test second time, \nFirst:\n' + (process as any)['__pw_initiator__'] + '\n\nSecond: '); - } finally { - Error.stackTraceLimit = originalStackTraceLimit; - } -} else { - (process as any)['__pw_initiator__'] = new Error().stack; -} +initPlaywrightTest('Importing @playwright/test'); type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & { _combinedContextOptions: BrowserContextOptions, diff --git a/packages/playwright/src/mcp/test/testContext.ts b/packages/playwright/src/mcp/test/testContext.ts index a2204e24db08b..e6626ff1ce3d9 100644 --- a/packages/playwright/src/mcp/test/testContext.ts +++ b/packages/playwright/src/mcp/test/testContext.ts @@ -93,7 +93,7 @@ export class TestContext { async createTestRunner(): Promise { if (this._testRunner) await this._testRunner.stopTests(); - const testRunner = new TestRunner(this.configLocation!, {}); + const testRunner = new TestRunner(this.configLocation!, {}, 'Playwright Agent'); await testRunner.initialize({}); this._testRunner = testRunner; testRunner.on(TestRunnerEvent.TestFilesChanged, testFiles => { diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 7ea4813d20e34..f2b11cea8d391 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -39,6 +39,7 @@ import { ensureSeedTest, seedProject } from './mcp/test/seed'; import { decorateCommand } from './mcp/program'; import { setupExitWatchdog } from './mcp/browser/watchdog'; import { initClaudeCodeRepo, initOpencodeRepo, initVSCodeRepo } from './agents/generateAgents'; +import { initPlaywrightTest } from './util'; import type { ConfigCLIOverrides } from './common/ipc'; import type { TraceMode } from '../types/test'; @@ -86,7 +87,7 @@ function addClearCacheCommand(program: Command) { command.description('clears build and test caches'); command.option('-c, --config ', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`); command.action(async opts => { - const runner = new TestRunner(resolveConfigLocation(opts.config), {}); + const runner = new TestRunner(resolveConfigLocation(opts.config), {}, 'Playwright CLI'); const { status } = await runner.clearCache(createErrorCollectingReporter(terminalScreen)); const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1); gracefullyProcessExitDoNotHang(exitCode); @@ -98,7 +99,7 @@ function addDevServerCommand(program: Command) { command.description('start dev server'); command.option('-c, --config ', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`); command.action(async options => { - const runner = new TestRunner(resolveConfigLocation(options.config), {}); + const runner = new TestRunner(resolveConfigLocation(options.config), {}, 'Playwright CLI'); await runner.startDevServer(createErrorCollectingReporter(terminalScreen), 'in-process'); }); } @@ -163,6 +164,7 @@ function addTestMCPServerCommand(program: Command) { command.option('--port ', 'port to listen on for SSE transport.'); command.action(async options => { setupExitWatchdog(); + initPlaywrightTest('Playwright Agent'); const backendFactory: ServerBackendFactory = { name: 'Playwright Test Runner', nameInConfig: 'playwright-test-runner', @@ -206,6 +208,7 @@ function addInitAgentsCommand(program: Command) { } async function runTests(args: string[], opts: { [key: string]: any }) { + initPlaywrightTest('Playwright CLI'); await startProfiling(); const cliOverrides = overridesFromOptions(opts); @@ -268,6 +271,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { } async function runTestServer(opts: { [key: string]: any }) { + initPlaywrightTest('Playwright CLI'); const host = opts.host || 'localhost'; const port = opts.port ? +opts.port : 0; const status = await testServer.runTestServer(opts.config, { }, { host, port }); @@ -276,6 +280,7 @@ async function runTestServer(opts: { [key: string]: any }) { } async function mergeReports(reportDir: string | undefined, opts: { [key: string]: any }) { + initPlaywrightTest('Playwright CLI'); const configFile = opts.config; const config = configFile ? await loadConfigFromFile(configFile) : await loadEmptyConfigForMergeReports(); diff --git a/packages/playwright/src/runner/testRunner.ts b/packages/playwright/src/runner/testRunner.ts index 59b4ce7400de1..84490d7b08025 100644 --- a/packages/playwright/src/runner/testRunner.ts +++ b/packages/playwright/src/runner/testRunner.ts @@ -29,7 +29,7 @@ import { webServerPluginsForConfig } from '../plugins/webServerPlugin'; import { internalScreen } from '../reporters/base'; import { InternalReporter } from '../reporters/internalReporter'; import { affectedTestFiles, collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache'; -import { serializeError } from '../util'; +import { initPlaywrightTest, serializeError } from '../util'; import { createErrorCollectingReporter, createReporters } from './reporters'; import { TestRun, createApplyRebaselinesTask, createClearCacheTask, createGlobalSetupTasks, createListFilesTask, createLoadTask, createPluginSetupTasks, createReportBeginTask, createRunTestsTasks, createStartDevServerTask, runTasks, runTasksDeferCleanup } from './tasks'; import { LastRunReporter } from './lastRun'; @@ -97,7 +97,8 @@ export class TestRunner extends EventEmitter { private _watchTestDirs = false; private _populateDependenciesOnList = false; - constructor(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides) { + constructor(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides, initiator: string) { + initPlaywrightTest(initiator); super(); this.configLocation = configLocation; this._configCLIOverrides = configCLIOverrides; diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 08fc4415e7700..82c61f326b4d3 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -94,7 +94,7 @@ export class TestServerDispatcher implements TestServerInterface { readonly _dispatchEvent: TestServerInterfaceEventEmitters['dispatchEvent']; constructor(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides) { - this._testRunner = new TestRunner(configLocation, configCLIOverrides); + this._testRunner = new TestRunner(configLocation, configCLIOverrides, 'VSCode extension'); this.transport = { onconnect: () => {}, dispatch: (method, params) => (this as any)[method](params), diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index 6ab72eb1dbf89..e95158f8b6342 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -19,7 +19,7 @@ import path from 'path'; import url from 'url'; import util from 'util'; -import { parseStackFrame, sanitizeForFilePath, calculateSha1, isRegExp, isString, stringifyStackFrames } from 'playwright-core/lib/utils'; +import { setBoxedStackPrefixes, parseStackFrame, sanitizeForFilePath, calculateSha1, isRegExp, isString, stringifyStackFrames } from 'playwright-core/lib/utils'; import { debug, mime, minimatch } from 'playwright-core/lib/utilsBundle'; import type { Location } from './../types/testReporter'; @@ -31,6 +31,35 @@ 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 initPlaywrightTest(initiator: string) { + setBoxedStackPrefixes([path.dirname(require.resolve('../package.json'))]); + + initiator = ` ${initiator}\n v${require('../package.json').version} at ${PLAYWRIGHT_TEST_PATH}`; + + if ((process as any)['__pw_initiator_path__'] === PLAYWRIGHT_TEST_PATH) { + // Same version of Playwright, update the initiator. + (process as any)['__pw_initiator__'] = initiator; + return; + } + + if ((process as any)['__pw_initiator__']) { + // Different version of Playwright, throw. + const error = new Error([ + 'Mixing two versions of Playwright:', + '', + (process as any)['__pw_initiator__'], + '', + initiator, + ].join('\n')); + error.stack = 'Error: ' + error.message; + throw error; + } + + // First-time import, set up the initiator. + (process as any)['__pw_initiator_path__'] = PLAYWRIGHT_TEST_PATH; + (process as any)['__pw_initiator__'] = initiator; +} + export function filterStackTrace(e: Error): { message: string, stack: string, cause?: ReturnType } { const name = e.name ? e.name + ': ' : ''; const cause = e.cause instanceof Error ? filterStackTrace(e.cause) : undefined;