From e38f9aa572e4c189d26482390de4344da46e7956 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 6 Mar 2026 18:06:52 +0000 Subject: [PATCH 1/5] chore: better error message in devtools (#39540) --- packages/devtools/src/grid.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/devtools/src/grid.tsx b/packages/devtools/src/grid.tsx index 59faf1247ccb0..4266690313de0 100644 --- a/packages/devtools/src/grid.tsx +++ b/packages/devtools/src/grid.tsx @@ -180,7 +180,11 @@ const SessionChip: React.FC<{ sessionFile: SessionFile; canConnect: boolean; vis
{channel && } {!canConnect &&
Session closed
} - {canConnect && !channel && wsUrl === null &&
Not supported — v{sessionFile.config.version}
} + {canConnect && !channel && wsUrl === null &&
+ Session v{sessionFile.config.version} is not compatible with this viewer{model.clientInfo ? ` v${model.clientInfo.version}` : ''}. +
+ Please update playwright-cli and restart this with "playwright-cli show". +
} {canConnect && !channel && wsUrl === undefined &&
Connecting
}
From 528d1c0cf75792bfc9003fd85927ffdfd38316c1 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 6 Mar 2026 18:16:35 +0000 Subject: [PATCH 2/5] fix(test runner): project.workers is sometimes exceeded (#39539) --- packages/playwright/src/runner/dispatcher.ts | 15 ++++--- tests/playwright-test/worker-index.spec.ts | 41 ++++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/packages/playwright/src/runner/dispatcher.ts b/packages/playwright/src/runner/dispatcher.ts index a4008a07aac92..8824d38d9f0de 100644 --- a/packages/playwright/src/runner/dispatcher.ts +++ b/packages/playwright/src/runner/dispatcher.ts @@ -40,7 +40,8 @@ import type { RegisteredListener } from 'playwright-core/lib/utils'; export type EnvByProjectId = Map>; export class Dispatcher { - private _workerSlots: { busy: boolean, worker?: WorkerHost, jobDispatcher?: JobDispatcher }[] = []; + // Worker slot is claimed when it has jobDispatcher assigned. + private _workerSlots: { worker?: WorkerHost, jobDispatcher?: JobDispatcher }[] = []; private _queue: TestGroup[] = []; private _workerLimitPerProjectId = new Map(); private _queuedOrRunningHashCount = new Map(); @@ -71,7 +72,7 @@ export class Dispatcher { const projectIdWorkerLimit = this._workerLimitPerProjectId.get(job.projectId); if (!projectIdWorkerLimit) return index; - const runningWorkersWithSameProjectId = this._workerSlots.filter(w => w.busy && w.worker && w.worker.projectId() === job.projectId).length; + const runningWorkersWithSameProjectId = this._workerSlots.filter(w => w.jobDispatcher?.job.projectId === job.projectId).length; if (runningWorkersWithSameProjectId < projectIdWorkerLimit) return index; } @@ -92,9 +93,9 @@ export class Dispatcher { const job = this._queue[jobIndex]; // 2. Find a worker with the same hash, or just some free worker. - let workerIndex = this._workerSlots.findIndex(w => !w.busy && w.worker && w.worker.hash() === job.workerHash && !w.worker.didSendStop()); + let workerIndex = this._workerSlots.findIndex(w => !w.jobDispatcher && w.worker && w.worker.hash() === job.workerHash && !w.worker.didSendStop()); if (workerIndex === -1) - workerIndex = this._workerSlots.findIndex(w => !w.busy); + workerIndex = this._workerSlots.findIndex(w => !w.jobDispatcher); if (workerIndex === -1) { // No workers available, bail out. return; @@ -103,7 +104,6 @@ export class Dispatcher { // 3. Claim both the job and the worker slot. this._queue.splice(jobIndex, 1); const jobDispatcher = new JobDispatcher(job, this._config, this._reporter, this._failureTracker, () => this.stop().catch(() => {})); - this._workerSlots[workerIndex].busy = true; this._workerSlots[workerIndex].jobDispatcher = jobDispatcher; // 4. Run the job. This is the only async operation. @@ -111,7 +111,6 @@ export class Dispatcher { // 5. Release the worker slot. this._workerSlots[workerIndex].jobDispatcher = undefined; - this._workerSlots[workerIndex].busy = false; // 6. Check whether we are done or should schedule another job. this._checkFinished(); @@ -178,7 +177,7 @@ export class Dispatcher { return; // Make sure all workers have finished the current job. - if (this._workerSlots.some(w => w.busy)) + if (this._workerSlots.some(w => !!w.jobDispatcher)) return; this._finished.resolve(); @@ -209,7 +208,7 @@ export class Dispatcher { void this.stop(); // 1. Allocate workers. for (let i = 0; i < this._config.config.workers; i++) - this._workerSlots.push({ busy: false }); + this._workerSlots.push({}); // 2. Schedule enough jobs. for (let i = 0; i < this._workerSlots.length; i++) this._scheduleJob(); diff --git a/tests/playwright-test/worker-index.spec.ts b/tests/playwright-test/worker-index.spec.ts index e84a205d565c0..419549b8ce0b6 100644 --- a/tests/playwright-test/worker-index.spec.ts +++ b/tests/playwright-test/worker-index.spec.ts @@ -280,6 +280,47 @@ test('should respect project.workers=1', async ({ runInlineTest }) => { ]); }); +test('should respect project.workers=1 on startup', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + export default { + workers: 2, + projects: [ + { name: 'project1' }, + { name: 'project2', workers: 1 }, + ], + }; + `, + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('test1', async ({}, testInfo) => { + console.log('%%test1-begin:' + testInfo.project.name); + await new Promise(f => setTimeout(f, 2000)); + console.log('%%test1-end:' + testInfo.project.name); + }); + `, + 'b.test.js': ` + import { test, expect } from '@playwright/test'; + test('test2', async ({}, testInfo) => { + console.log('%%test2-begin:' + testInfo.project.name); + await new Promise(f => setTimeout(f, 2000)); + console.log('%%test2-end:' + testInfo.project.name); + }); + `, + }, { workers: 2 }); + expect(result.passed).toBe(4); + expect(result.exitCode).toBe(0); + + // Once both tests from the first project finish apporximately at the same time, + // tests from the second project should run sequentially. + expect(result.outputLines.slice(4, 8)).toEqual([ + 'test1-begin:project2', + 'test1-end:project2', + 'test2-begin:project2', + 'test2-end:project2', + ]); +}); + test('should respect project.workers>1', async ({ runInlineTest }) => { const result = await runInlineTest({ 'playwright.config.ts': ` From fecb58c2bcf3908753bc8da27bbb3e5adc84bd9b Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Fri, 6 Mar 2026 18:17:38 +0000 Subject: [PATCH 3/5] Revert "chore: turn modifiers into hooks early (#39508)" (#39542) --- packages/playwright/src/common/fixtures.ts | 6 +- packages/playwright/src/common/poolBuilder.ts | 57 ++++++------------- packages/playwright/src/common/test.ts | 32 +++-------- packages/playwright/src/common/testType.ts | 2 +- .../playwright/src/worker/fixtureRunner.ts | 10 ++++ .../playwright/src/worker/timeoutManager.ts | 6 ++ packages/playwright/src/worker/workerMain.ts | 30 +++++++++- tests/playwright-test/test-modifiers.spec.ts | 6 +- 8 files changed, 74 insertions(+), 75 deletions(-) diff --git a/packages/playwright/src/common/fixtures.ts b/packages/playwright/src/common/fixtures.ts index dad97674a300b..30b144e663465 100644 --- a/packages/playwright/src/common/fixtures.ts +++ b/packages/playwright/src/common/fixtures.ts @@ -228,16 +228,12 @@ export class FixturePool { return hash.digest('hex'); } - validateFunction(fn: Function, prefix: string, location: Location): 'worker' | 'test' { - let scope: 'worker' | 'test' = 'worker'; + validateFunction(fn: Function, prefix: string, location: Location) { for (const name of fixtureParameterNames(fn, location, e => this._onLoadError(e))) { const registration = this._registrations.get(name); if (!registration) this._addLoadError(`${prefix} has unknown parameter "${name}".`, location); - if (registration?.scope === 'test') - scope = 'test'; } - return scope; } resolve(name: string, forFixture?: FixtureRegistration): FixtureRegistration | undefined { diff --git a/packages/playwright/src/common/poolBuilder.ts b/packages/playwright/src/common/poolBuilder.ts index 2571747db91cb..f975efe92f80d 100644 --- a/packages/playwright/src/common/poolBuilder.ts +++ b/packages/playwright/src/common/poolBuilder.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { FixturePool, inheritFixtureNames } from './fixtures'; +import { FixturePool } from './fixtures'; import { formatLocation } from '../util'; -import { currentTestInfo } from './globals'; import type { FullProjectInternal } from './config'; import type { LoadError } from './fixtures'; @@ -42,32 +41,8 @@ export class PoolBuilder { this._project = project; } - buildPools(topSuite: Suite, testErrors?: TestError[]) { - topSuite.forEachSuite(suite => { - const modifiers = suite._modifiers.slice(); - suite._modifiers = []; - - for (const modifier of modifiers.reverse()) { - let pool = this._buildTestTypePool(modifier.testType, testErrors); - pool = this._buildPoolForSuite(pool, suite, testErrors); - const scope = pool.validateFunction(modifier.fn, modifier.type + ' modifier', modifier.location); - - const fn = async (fixtures: any) => { - const result = await modifier.fn(fixtures); - currentTestInfo()?._modifier(modifier.type, modifier.location, [!!result, modifier.description]); - }; - inheritFixtureNames(modifier.fn, fn); - - suite._hooks.unshift({ - type: scope === 'worker' ? 'beforeAll' : 'beforeEach', - fn, - title: `${modifier.type} modifier`, - location: modifier.location, - }); - } - }); - - topSuite.forEachTest(test => { + buildPools(suite: Suite, testErrors?: TestError[]) { + suite.forEachTest(test => { const pool = this._buildPoolForTest(test, testErrors); if (this._type === 'loader') test._poolDigest = pool.digest; @@ -78,18 +53,22 @@ export class PoolBuilder { private _buildPoolForTest(test: TestCase, testErrors?: TestError[]): FixturePool { let pool = this._buildTestTypePool(test._testType, testErrors); - pool = this._buildPoolForSuite(pool, test.parent, testErrors); - pool.validateFunction(test.fn, 'Test', test.location); - return pool; - } - private _buildPoolForSuite(pool: FixturePool, suite: Suite, testErrors?: TestError[]): FixturePool { - if (suite.parent) - pool = this._buildPoolForSuite(pool, suite.parent, testErrors); - if (suite._use.length) - pool = new FixturePool(suite._use, e => this._handleLoadError(e, testErrors), pool, suite._type === 'describe'); - for (const hook of suite._hooks) - pool.validateFunction(hook.fn, hook.type + ' hook', hook.location); + const parents: Suite[] = []; + for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent) + parents.push(parent); + parents.reverse(); + + for (const parent of parents) { + if (parent._use.length) + pool = new FixturePool(parent._use, e => this._handleLoadError(e, testErrors), pool, parent._type === 'describe'); + for (const hook of parent._hooks) + pool.validateFunction(hook.fn, hook.type + ' hook', hook.location); + for (const modifier of parent._modifiers) + pool.validateFunction(modifier.fn, modifier.type + ' modifier', modifier.location); + } + + pool.validateFunction(test.fn, 'Test', test.location); return pool; } diff --git a/packages/playwright/src/common/test.ts b/packages/playwright/src/common/test.ts index e4b904e90879c..fa8d8c20032d0 100644 --- a/packages/playwright/src/common/test.ts +++ b/packages/playwright/src/common/test.ts @@ -35,19 +35,11 @@ class Base { } } -type Modifier = { +export type Modifier = { type: 'slow' | 'fixme' | 'skip' | 'fail', fn: Function, location: Location, - description: string | undefined, - testType: TestTypeImpl, -}; - -export type SuiteHook = { - type: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll'; - fn: Function; - title: string; - location: Location; + description: string | undefined }; export class Suite extends Base { @@ -55,14 +47,13 @@ export class Suite extends Base { parent?: Suite; _use: FixturesWithLocation[] = []; _entries: (Suite | TestCase)[] = []; - _hooks: SuiteHook[] = []; + _hooks: { type: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', fn: Function, title: string, location: Location }[] = []; _timeout: number | undefined; _retries: number | undefined; // Annotations known statically before running the test, e.g. `test.describe.skip()` or `test.describe({ annotation }, body)`. _staticAnnotations: TestAnnotation[] = []; // Explicitly declared tags that are not a part of the title. _tags: string[] = []; - // Modifiers are converted into hooks after the fixture pool is built. _modifiers: Modifier[] = []; _parallelMode: 'none' | 'default' | 'serial' | 'parallel' = 'none'; _fullProject: FullProjectInternal | undefined; @@ -214,14 +205,6 @@ export class Suite extends Base { } } - forEachSuite(visitor: (suite: Suite) => void) { - visitor(this); - for (const entry of this._entries) { - if (entry instanceof Suite) - entry.forEachSuite(visitor); - } - } - _serialize(): any { return { kind: 'suite', @@ -234,9 +217,9 @@ export class Suite extends Base { retries: this._retries, staticAnnotations: this._staticAnnotations.slice(), tags: this._tags.slice(), - modifiers: this._modifiers.map(m => ({ ...m, fn: undefined, testType: undefined })), + modifiers: this._modifiers.slice(), parallelMode: this._parallelMode, - hooks: this._hooks.map(h => ({ ...h, fn: undefined })), + hooks: this._hooks.map(h => ({ type: h.type, location: h.location, title: h.title })), fileId: this._fileId, }; } @@ -250,9 +233,9 @@ export class Suite extends Base { suite._retries = data.retries; suite._staticAnnotations = data.staticAnnotations; suite._tags = data.tags; - suite._modifiers = data.modifiers.map((m: any) => ({ ...m, fn: () => { }, testType: rootTestType })); + suite._modifiers = data.modifiers; suite._parallelMode = data.parallelMode; - suite._hooks = data.hooks.map((h: any) => ({ ...h, fn: () => { } })); + suite._hooks = data.hooks.map((h: any) => ({ type: h.type, location: h.location, title: h.title, fn: () => { } })); suite._fileId = data.fileId; return suite; } @@ -262,7 +245,6 @@ export class Suite extends Base { const suite = Suite._parse(data); suite._use = this._use.slice(); suite._hooks = this._hooks.slice(); - suite._modifiers = this._modifiers.slice(); suite._fullProject = this._fullProject; return suite; } diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 7440c473e54f7..e8736b667735b 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -223,7 +223,7 @@ export class TestTypeImpl { } if (typeof modifierArgs[0] === 'function') { - suite._modifiers.push({ type, fn: modifierArgs[0], location, description: modifierArgs[1], testType: this }); + suite._modifiers.push({ type, fn: modifierArgs[0], location, description: modifierArgs[1] }); } else { if (modifierArgs.length >= 1 && !modifierArgs[0]) return; diff --git a/packages/playwright/src/worker/fixtureRunner.ts b/packages/playwright/src/worker/fixtureRunner.ts index 7f8e0eacf79f1..c00b9fe626b87 100644 --- a/packages/playwright/src/worker/fixtureRunner.ts +++ b/packages/playwright/src/worker/fixtureRunner.ts @@ -289,6 +289,16 @@ export class FixtureRunner { await fixture.setup(testInfo, runnable); return fixture; } + + dependsOnWorkerFixturesOnly(fn: Function, location: Location): boolean { + const names = getRequiredFixtureNames(fn, location); + for (const name of names) { + const registration = this.pool!.resolve(name)!; + if (registration.scope !== 'worker') + return false; + } + return true; + } } function getRequiredFixtureNames(fn: Function, location?: Location) { diff --git a/packages/playwright/src/worker/timeoutManager.ts b/packages/playwright/src/worker/timeoutManager.ts index 8d18fe098d225..c003342d15806 100644 --- a/packages/playwright/src/worker/timeoutManager.ts +++ b/packages/playwright/src/worker/timeoutManager.ts @@ -188,6 +188,12 @@ export class TimeoutManager { message = `Worker teardown timeout of ${timeout}ms exceeded.`; break; } + case 'skip': + case 'slow': + case 'fixme': + case 'fail': + message = `"${runnable.type}" modifier timeout of ${timeout}ms exceeded.`; + break; } const fixtureWithSlot = runnable.fixture?.slot ? runnable.fixture : undefined; if (fixtureWithSlot) diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index 8a7f5a960ce56..bdd46abaf00ad 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -24,12 +24,14 @@ import { debugTest, relativeFilePath } from '../util'; import { FixtureRunner } from './fixtureRunner'; import { TestSkipError, TestInfoImpl, emtpyTestInfoCallbacks } from './testInfo'; import { testInfoError } from './util'; +import { inheritFixtureNames } from '../common/fixtures'; import { PoolBuilder } from '../common/poolBuilder'; import { ProcessRunner } from '../common/process'; import { applyRepeatEachIndex, bindFileSuiteToProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; import { loadTestFile } from '../common/testLoader'; import type { TimeSlot } from './timeoutManager'; +import type { Location } from '../../types/testReporter'; import type { FullConfigInternal, FullProjectInternal } from '../common/config'; import type * as ipc from '../common/ipc'; import type { Suite, TestCase } from '../common/test'; @@ -525,6 +527,30 @@ export class WorkerMain extends ProcessRunner { await removeFolders([testInfo.outputDir]); } + private _collectHooksAndModifiers(suite: Suite, type: 'beforeAll' | 'beforeEach' | 'afterAll' | 'afterEach', testInfo: TestInfoImpl) { + type Runnable = { type: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll' | 'fixme' | 'skip' | 'slow' | 'fail', fn: Function, title: string, location: Location }; + const runnables: Runnable[] = []; + for (const modifier of suite._modifiers) { + const modifierType = this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location) ? 'beforeAll' : 'beforeEach'; + if (modifierType !== type) + continue; + const fn = async (fixtures: any) => { + const result = await modifier.fn(fixtures); + testInfo._modifier(modifier.type, modifier.location, [!!result, modifier.description]); + }; + inheritFixtureNames(modifier.fn, fn); + runnables.push({ + title: `${modifier.type} modifier`, + location: modifier.location, + type: modifier.type, + fn, + }); + } + // Modifiers first, then hooks. + runnables.push(...suite._hooks.filter(hook => hook.type === type)); + return runnables; + } + private async _runBeforeAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl) { if (this._activeSuites.has(suite)) return; @@ -536,7 +562,7 @@ export class WorkerMain extends ProcessRunner { private async _runAllHooksForSuite(suite: Suite, testInfo: TestInfoImpl, type: 'beforeAll' | 'afterAll', extraAnnotations?: TestAnnotation[]) { // Always run all the hooks, and capture the first error. let firstError: Error | undefined; - for (const hook of suite._hooks.filter(hook => hook.type === type)) { + for (const hook of this._collectHooksAndModifiers(suite, type, testInfo)) { try { await testInfo._runAsStep({ title: hook.title, category: 'hook', location: hook.location }, async () => { // Separate time slot for each beforeAll/afterAll hook. @@ -583,7 +609,7 @@ export class WorkerMain extends ProcessRunner { private async _runEachHooksForSuites(suites: Suite[], type: 'beforeEach' | 'afterEach', testInfo: TestInfoImpl, slot?: TimeSlot) { // Always run all the hooks, unless one of the times out, and capture the first error. let firstError: Error | undefined; - const hooks = suites.map(suite => suite._hooks.filter(hook => hook.type === type)).flat(); + const hooks = suites.map(suite => this._collectHooksAndModifiers(suite, type, testInfo)).flat(); for (const hook of hooks) { const runnable = { type: hook.type, location: hook.location, slot }; if (testInfo._timeoutManager.isTimeExhaustedFor(runnable)) { diff --git a/tests/playwright-test/test-modifiers.spec.ts b/tests/playwright-test/test-modifiers.spec.ts index b6ff2f35d6c35..e73e483b278c4 100644 --- a/tests/playwright-test/test-modifiers.spec.ts +++ b/tests/playwright-test/test-modifiers.spec.ts @@ -283,13 +283,13 @@ test.describe('test modifier annotations', () => { const result = await runInlineTest({ 'a.test.ts': ` import { test, expect } from '@playwright/test'; - + test.describe.only("suite", () => { test.skip('focused skip by suite', () => {}); test.fixme('focused fixme by suite', () => {}); test.fail.only('focused fail by suite', () => { expect(1).toBe(2); }); }); - + test.describe.skip('not focused', () => { test('no marker', () => {}); }); @@ -527,7 +527,7 @@ test('modifier timeout should be reported', async ({ runInlineTest }) => { }, { timeout: 2000 }); expect(result.exitCode).toBe(1); expect(result.failed).toBe(1); - expect(result.output).toContain('"beforeAll" hook timeout of 2000ms exceeded.'); + expect(result.output).toContain('"skip" modifier timeout of 2000ms exceeded.'); expect(result.output).toContain('3 | test.skip(async () => new Promise(() => {}));'); }); From 0b7635ee98aba7d9e3745b243a57cced9b0ad48e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 6 Mar 2026 18:54:04 -0800 Subject: [PATCH 4/5] chore: normalize remote pipe name handling (#39546) --- docs/src/api/class-android.md | 4 ++-- docs/src/api/class-browsertype.md | 4 ++-- packages/playwright-client/types/types.d.ts | 20 +++++++++---------- .../playwright-core/src/client/android.ts | 4 ++-- .../playwright-core/src/client/browserType.ts | 18 ++++++++--------- packages/playwright-core/src/client/types.ts | 12 +++++------ .../playwright-core/src/client/webSocket.ts | 2 +- .../playwright-core/src/protocol/validator.ts | 3 +-- .../dispatchers/localUtilsDispatcher.ts | 10 +++++----- packages/playwright-core/types/types.d.ts | 20 +++++++++---------- packages/playwright/src/index.ts | 4 ++-- packages/protocol/src/channels.d.ts | 5 +---- packages/protocol/src/protocol.yml | 3 +-- tests/config/remoteServer.ts | 2 +- tests/library/browser-server.spec.ts | 2 +- tests/library/browsertype-connect.spec.ts | 5 ++--- .../library/browsertype-launch-server.spec.ts | 2 +- 17 files changed, 55 insertions(+), 65 deletions(-) diff --git a/docs/src/api/class-android.md b/docs/src/api/class-android.md index c976c9bba80ca..ffef68509dc6d 100644 --- a/docs/src/api/class-android.md +++ b/docs/src/api/class-android.md @@ -81,9 +81,9 @@ const { _android: android } = require('playwright'); This methods attaches Playwright to an existing Android device. Use [`method: Android.launchServer`] to launch a new Android server instance. -### param: Android.connect.wsEndpoint +### param: Android.connect.endpoint * since: v1.28 -- `wsEndpoint` <[string]> +- `endpoint` <[string]> A browser websocket endpoint to connect to. diff --git a/docs/src/api/class-browsertype.md b/docs/src/api/class-browsertype.md index ccb2af7cb72ef..fa7a0b05f7f9e 100644 --- a/docs/src/api/class-browsertype.md +++ b/docs/src/api/class-browsertype.md @@ -95,9 +95,9 @@ This method attaches Playwright to an existing browser instance created via `Bro The major and minor version of the Playwright instance that connects needs to match the version of Playwright that launches the browser (1.2.3 → is compatible with 1.2.x). ::: -### param: BrowserType.connect.wsEndpoint +### param: BrowserType.connect.endpoint * since: v1.10 -- `wsEndpoint` <[string]> +- `endpoint` <[string]> A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`. diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 3089d164b44a1..815eb462afc7f 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -15169,9 +15169,9 @@ export interface BrowserType { * **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. * * **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). * If you are experiencing issues or attempting to use advanced functionality, you probably want to use - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). * * **Usage** * @@ -15199,9 +15199,9 @@ export interface BrowserType { * **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. * * **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). * If you are experiencing issues or attempting to use advanced functionality, you probably want to use - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). * * **Usage** * @@ -15222,7 +15222,7 @@ export interface BrowserType { * **NOTE** The major and minor version of the Playwright instance that connects needs to match the version of * Playwright that launches the browser (1.2.3 → is compatible with 1.2.x). * - * @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`. + * @param endpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`. * @param options */ connect(wsEndpoint: string, options?: ConnectOptions): Promise; @@ -15238,7 +15238,7 @@ export interface BrowserType { * **NOTE** The major and minor version of the Playwright instance that connects needs to match the version of * Playwright that launches the browser (1.2.3 → is compatible with 1.2.x). * - * @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`. + * @param endpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`. * @param options */ connect(options: ConnectOptions & { wsEndpoint?: string }): Promise; @@ -15819,7 +15819,7 @@ export interface BrowserType { /** * Returns the browser app instance. You can connect to it via - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect), + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect), * which requires the major/minor client/server version to match (1.2.3 → is compatible with 1.2.x). * * **Usage** @@ -16818,10 +16818,10 @@ export interface Android { * This methods attaches Playwright to an existing Android device. Use * [android.launchServer([options])](https://playwright.dev/docs/api/class-android#android-launch-server) to launch a * new Android server instance. - * @param wsEndpoint A browser websocket endpoint to connect to. + * @param endpoint A browser websocket endpoint to connect to. * @param options */ - connect(wsEndpoint: string, options?: { + connect(endpoint: string, options?: { /** * Additional HTTP headers to be sent with web socket connect request. Optional. */ @@ -19103,7 +19103,7 @@ export interface BrowserServer { * Browser websocket url. * * Browser websocket endpoint which can be used as an argument to - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect) + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect) * to establish connection to the browser. * * Note that if the listen `host` option in `launchServer` options is not specified, localhost will be output anyway, diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index 834a30bbe11d7..8e4c900b512b7 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -66,11 +66,11 @@ export class Android extends ChannelOwner implements ap return await this._serverLauncher.launchServer(options); } - async connect(wsEndpoint: string, options: Parameters[1] = {}): Promise { + async connect(endpoint: string, options: Parameters[1] = {}): Promise { return await this._wrapApiCall(async () => { const deadline = options.timeout ? monotonicTime() + options.timeout : 0; const headers = { 'x-playwright-browser': 'android', ...options.headers }; - const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: options.slowMo, timeout: options.timeout || 0 }; + const connectParams: channels.LocalUtilsConnectParams = { endpoint, headers, slowMo: options.slowMo, timeout: options.timeout || 0 }; const connection = await connectOverWebSocket(this._connection, connectParams); let device: AndroidDevice; diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index e6ae29ca8c0a2..44da774a51690 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -122,13 +122,12 @@ export class BrowserType extends ChannelOwner imple } connect(options: api.ConnectOptions & { wsEndpoint: string }): Promise; - connect(options: api.ConnectOptions & { pipeName: string }): Promise; - connect(wsEndpoint: string, options?: api.ConnectOptions): Promise; - async connect(optionsOrWsEndpoint: string | (api.ConnectOptions & { wsEndpoint?: string, pipeName?: string }), options?: api.ConnectOptions): Promise{ - if (typeof optionsOrWsEndpoint === 'string') - return await this._connect({ ...options, wsEndpoint: optionsOrWsEndpoint }); - assert(optionsOrWsEndpoint.wsEndpoint || optionsOrWsEndpoint.pipeName, 'Either options.wsEndpoint or options.pipeName is required'); - return await this._connect(optionsOrWsEndpoint); + connect(endpoint: string, options?: api.ConnectOptions): Promise; + async connect(optionsOrEndpoint: string | (api.ConnectOptions & { wsEndpoint?: string, pipeName?: string }), options?: api.ConnectOptions): Promise{ + if (typeof optionsOrEndpoint === 'string') + return await this._connect({ ...options, endpoint: optionsOrEndpoint }); + assert(optionsOrEndpoint.wsEndpoint, 'options.wsEndpoint is required'); + return await this._connect(optionsOrEndpoint); } async _connect(params: ConnectOptions): Promise { @@ -137,10 +136,9 @@ export class BrowserType extends ChannelOwner imple const deadline = params.timeout ? monotonicTime() + params.timeout : 0; const headers = { 'x-playwright-browser': this.name(), ...params.headers }; const connectParams: channels.LocalUtilsConnectParams = { - wsEndpoint: params.wsEndpoint, - pipeName: params.pipeName, + endpoint: params.endpoint!, headers, - exposeNetwork: params.exposeNetwork ?? params._exposeNetwork, + exposeNetwork: params.exposeNetwork, slowMo: params.slowMo, timeout: params.timeout || 0, }; diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index 920cc3ea98fff..d926e9f8ccf2d 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -96,14 +96,12 @@ export type LaunchOptions = Omit; export type ConnectOptions = { - wsEndpoint?: string, - pipeName?: string, + endpoint?: string; headers?: { [key: string]: string; }; - exposeNetwork?: string, - _exposeNetwork?: string, - slowMo?: number, - timeout?: number, - logger?: Logger, + exposeNetwork?: string; + slowMo?: number; + timeout?: number; + logger?: Logger; }; export type LaunchServerOptions = LaunchOptions & { host?: string, diff --git a/packages/playwright-core/src/client/webSocket.ts b/packages/playwright-core/src/client/webSocket.ts index 0e56a2401bab6..7afc749573b21 100644 --- a/packages/playwright-core/src/client/webSocket.ts +++ b/packages/playwright-core/src/client/webSocket.ts @@ -88,7 +88,7 @@ class WebSocketTransport implements Transport { private _ws: WebSocket | undefined; async connect(params: channels.LocalUtilsConnectParams) { - this._ws = new window.WebSocket(params.wsEndpoint!); + this._ws = new window.WebSocket(params.endpoint); return []; } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 5a0f5b36bed94..2a31b6c52183d 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -314,8 +314,7 @@ scheme.LocalUtilsHarUnzipParams = tObject({ }); scheme.LocalUtilsHarUnzipResult = tOptional(tObject({})); scheme.LocalUtilsConnectParams = tObject({ - wsEndpoint: tOptional(tString), - pipeName: tOptional(tString), + endpoint: tString, headers: tOptional(tAny), exposeNetwork: tOptional(tString), slowMo: tOptional(tFloat), diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index 655aa5df1cfba..b52b0dd33a575 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -84,9 +84,9 @@ export class LocalUtilsDispatcher extends Dispatcher { - if (params.pipeName) - return await this._connectOverPipe(params, progress); - return await this._connectOverWebSocket(params, progress); + if (URL.canParse(params.endpoint)) + return await this._connectOverWebSocket(params, progress); + return await this._connectOverPipe(params, progress); } private async _connectOverWebSocket(params: channels.LocalUtilsConnectParams, progress: Progress): Promise { @@ -95,7 +95,7 @@ export class LocalUtilsDispatcher extends Dispatcher { const socket = await new Promise((resolve, reject) => { - const conn = net.connect(params.pipeName!, () => resolve(conn)); + const conn = net.connect(params.endpoint, () => resolve(conn)); conn.on('error', reject); }); const transport = new PipeTransport(socket, socket); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 3089d164b44a1..815eb462afc7f 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -15169,9 +15169,9 @@ export interface BrowserType { * **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. * * **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). * If you are experiencing issues or attempting to use advanced functionality, you probably want to use - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). * * **Usage** * @@ -15199,9 +15199,9 @@ export interface BrowserType { * **NOTE** Connecting over the Chrome DevTools Protocol is only supported for Chromium-based browsers. * * **NOTE** This connection is significantly lower fidelity than the Playwright protocol connection via - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). * If you are experiencing issues or attempting to use advanced functionality, you probably want to use - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect). * * **Usage** * @@ -15222,7 +15222,7 @@ export interface BrowserType { * **NOTE** The major and minor version of the Playwright instance that connects needs to match the version of * Playwright that launches the browser (1.2.3 → is compatible with 1.2.x). * - * @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`. + * @param endpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`. * @param options */ connect(wsEndpoint: string, options?: ConnectOptions): Promise; @@ -15238,7 +15238,7 @@ export interface BrowserType { * **NOTE** The major and minor version of the Playwright instance that connects needs to match the version of * Playwright that launches the browser (1.2.3 → is compatible with 1.2.x). * - * @param wsEndpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`. + * @param endpoint A Playwright browser websocket endpoint to connect to. You obtain this endpoint via `BrowserServer.wsEndpoint`. * @param options */ connect(options: ConnectOptions & { wsEndpoint?: string }): Promise; @@ -15819,7 +15819,7 @@ export interface BrowserType { /** * Returns the browser app instance. You can connect to it via - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect), + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect), * which requires the major/minor client/server version to match (1.2.3 → is compatible with 1.2.x). * * **Usage** @@ -16818,10 +16818,10 @@ export interface Android { * This methods attaches Playwright to an existing Android device. Use * [android.launchServer([options])](https://playwright.dev/docs/api/class-android#android-launch-server) to launch a * new Android server instance. - * @param wsEndpoint A browser websocket endpoint to connect to. + * @param endpoint A browser websocket endpoint to connect to. * @param options */ - connect(wsEndpoint: string, options?: { + connect(endpoint: string, options?: { /** * Additional HTTP headers to be sent with web socket connect request. Optional. */ @@ -19103,7 +19103,7 @@ export interface BrowserServer { * Browser websocket url. * * Browser websocket endpoint which can be used as an argument to - * [browserType.connect(wsEndpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect) + * [browserType.connect(endpoint[, options])](https://playwright.dev/docs/api/class-browsertype#browser-type-connect) * to establish connection to the browser. * * Note that if the listen `host` option in `launchServer` options is not specified, localhost will be output anyway, diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 3f4356d15f779..72582d06fb96c 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -106,9 +106,9 @@ const playwrightFixtures: Fixtures = ({ throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`); if (connectOptions) { - const browser = await playwright[browserName].connect({ + const browser = await playwright[browserName].connect(connectOptions.wsEndpoint, { ...connectOptions, - exposeNetwork: connectOptions.exposeNetwork ?? (connectOptions as any)._exposeNetwork, + exposeNetwork: connectOptions.exposeNetwork, headers: { // HTTP headers are ASCII only (not UTF-8). 'x-playwright-launch-options': jsonStringifyForceASCII(_browserOptions), diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 5e8c20cf200d8..ed98799521805 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -532,8 +532,7 @@ export type LocalUtilsHarUnzipOptions = { }; export type LocalUtilsHarUnzipResult = void; export type LocalUtilsConnectParams = { - wsEndpoint?: string, - pipeName?: string, + endpoint: string, headers?: any, exposeNetwork?: string, slowMo?: number, @@ -541,8 +540,6 @@ export type LocalUtilsConnectParams = { socksProxyRedirectPortForTest?: number, }; export type LocalUtilsConnectOptions = { - wsEndpoint?: string, - pipeName?: string, headers?: any, exposeNetwork?: string, slowMo?: number, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 344101d69e5e6..a812775aee472 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -706,8 +706,7 @@ LocalUtils: connect: internal: true parameters: - wsEndpoint: string? - pipeName: string? + endpoint: string headers: json? exposeNetwork: string? slowMo: float? diff --git a/tests/config/remoteServer.ts b/tests/config/remoteServer.ts index 1e04c0c8db610..6d5f0cc4528ee 100644 --- a/tests/config/remoteServer.ts +++ b/tests/config/remoteServer.ts @@ -126,7 +126,7 @@ export class RemoteServer implements PlaywrightServer { this._wsEndpoint = await this.out('wsEndpoint'); if (remoteServerOptions.url) { - this._browser = await this._browserType.connect({ wsEndpoint: this._wsEndpoint }); + this._browser = await this._browserType.connect(this._wsEndpoint); const page = await this._browser.newPage(); await page.goto(remoteServerOptions.url); } diff --git a/tests/library/browser-server.spec.ts b/tests/library/browser-server.spec.ts index 84b2bacc1551d..d34e5cdc617e1 100644 --- a/tests/library/browser-server.spec.ts +++ b/tests/library/browser-server.spec.ts @@ -32,7 +32,7 @@ it('should start and stop pipe server', async ({ browserType, browser }) => { pipeName: expect.stringMatching(/browser@.*\.sock/), })); - const browser2 = await (browserType as any).connect(serverInfo); + const browser2 = await (browserType as any).connect(serverInfo.pipeName); const page = await browser2.newPage(); await page.goto('data:text/html,

Hello via pipe

'); expect(await page.locator('h1').textContent()).toBe('Hello via pipe'); diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index b3ccee17ab55c..e146492aad382 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -620,8 +620,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { test('should be able to connect when the wsEndpoint is passed as an option', async ({ browserType, startRemoteServer }) => { const remoteServer = await startRemoteServer(kind); - const browser = await browserType.connect({ - wsEndpoint: remoteServer.wsEndpoint(), + const browser = await browserType.connect(remoteServer.wsEndpoint(), { headers: { 'x-playwright-launch-options': JSON.stringify((browserType as any)._playwright._defaultLaunchOptions || {}), }, @@ -850,7 +849,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { }); const examplePort = 20_000 + testInfo.workerIndex * 3; const remoteServer = await startRemoteServer(kind); - const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any, dummyServerPort); + const browser = await connect(remoteServer.wsEndpoint(), { exposeNetwork: '*' } as any, dummyServerPort); const page = await browser.newPage(); { await page.setContent('empty'); diff --git a/tests/library/browsertype-launch-server.spec.ts b/tests/library/browsertype-launch-server.spec.ts index fed973dde57d1..cc6490fb270fd 100644 --- a/tests/library/browsertype-launch-server.spec.ts +++ b/tests/library/browsertype-launch-server.spec.ts @@ -66,7 +66,7 @@ it.describe('launch server', () => { it('should provide an error when ws endpoint is incorrect', async ({ browserType }) => { const browserServer = await browserType.launchServer(); - const error = await browserType.connect({ wsEndpoint: browserServer.wsEndpoint() + '-foo' }).catch(e => e); + const error = await browserType.connect(browserServer.wsEndpoint() + '-foo').catch(e => e); await browserServer.close(); expect(error.message).toContain('400 Bad Request'); }); From d444bbb1e950ea03e67f9b3559d38fb5e205e9ba Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 6 Mar 2026 19:04:30 -0800 Subject: [PATCH 5/5] fix(cli): fix killAllDaemons to match actual daemon process command line (#39549) --- packages/playwright-core/src/cli/client/program.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playwright-core/src/cli/client/program.ts b/packages/playwright-core/src/cli/client/program.ts index 08ce1f0546032..9fcbacbf6d5bf 100644 --- a/packages/playwright-core/src/cli/client/program.ts +++ b/packages/playwright-core/src/cli/client/program.ts @@ -294,7 +294,7 @@ async function killAllDaemons(): Promise { const result = execSync( `powershell -NoProfile -NonInteractive -Command ` + `"Get-CimInstance Win32_Process ` - + `| Where-Object { $_.CommandLine -like '*-server*' -and $_.CommandLine -like '*--daemon-*' } ` + + `| Where-Object { $_.CommandLine -like '*run-mcp-server*' -or $_.CommandLine -like '*run-cli-server*' } ` + `| ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue; $_.ProcessId }"`, { encoding: 'utf-8' } ); @@ -308,7 +308,7 @@ async function killAllDaemons(): Promise { const result = execSync('ps aux', { encoding: 'utf-8' }); const lines = result.split('\n'); for (const line of lines) { - if ((line.includes('-server')) && line.includes('--daemon-')) { + if (line.includes('run-mcp-server') || line.includes('run-cli-server')) { const parts = line.trim().split(/\s+/); const pid = parts[1]; if (pid && /^\d+$/.test(pid)) {