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/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
}
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/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)) {
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/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/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/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/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/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');
});
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(() => {}));');
});
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': `