diff --git a/docs/src/api/class-android.md b/docs/src/api/class-android.md index 88fa87826a..a5148a2a2f 100644 --- a/docs/src/api/class-android.md +++ b/docs/src/api/class-android.md @@ -82,6 +82,11 @@ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -D playwright Returns the list of detected Android devices. +### option: Android.devices.port +- `port` <[int]> + +Optional port to establish ADB server connection. + ## method: Android.setDefaultTimeout This setting will change the default maximum time for all the methods accepting [`param: timeout`] option. diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index 4928cf5bfe..a929e8f37b 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -47,8 +47,8 @@ export class Android extends ChannelOwner implements ap this._channel.setDefaultTimeoutNoReply({ timeout }); } - async devices(): Promise { - const { devices } = await this._channel.devices(); + async devices(options: { port?: number }): Promise { + const { devices } = await this._channel.devices(options); return devices.map(d => AndroidDevice.from(d)); } } @@ -190,7 +190,7 @@ export class AndroidDevice extends ChannelOwner i await this._channel.push({ file: await loadFile(file), path, mode: options ? options.mode : undefined }); } - async launchBrowser(options: types.BrowserContextOptions & { pkg?: string } = {}): Promise { + async launchBrowser(options: types.BrowserContextOptions & { pkg?: string } = {}): Promise { const contextOptions = await prepareBrowserContextParams(options); const { context } = await this._channel.launchBrowser(contextOptions); return BrowserContext.from(context) as BrowserContext; diff --git a/packages/playwright-core/src/dispatchers/androidDispatcher.ts b/packages/playwright-core/src/dispatchers/androidDispatcher.ts index 29ca1baa80..897e23cf7b 100644 --- a/packages/playwright-core/src/dispatchers/androidDispatcher.ts +++ b/packages/playwright-core/src/dispatchers/androidDispatcher.ts @@ -27,7 +27,7 @@ export class AndroidDispatcher extends Dispatcher { - const devices = await this._object.devices(); + const devices = await this._object.devices(params); return { devices: devices.map(d => AndroidDeviceDispatcher.from(this._scope, d)) }; diff --git a/packages/playwright-core/src/protocol/channels.ts b/packages/playwright-core/src/protocol/channels.ts index 77caec0804..80a413ebd5 100644 --- a/packages/playwright-core/src/protocol/channels.ts +++ b/packages/playwright-core/src/protocol/channels.ts @@ -3592,11 +3592,15 @@ export interface AndroidEventTarget { } export interface AndroidChannel extends AndroidEventTarget, Channel { _type_Android: boolean; - devices(params?: AndroidDevicesParams, metadata?: Metadata): Promise; + devices(params: AndroidDevicesParams, metadata?: Metadata): Promise; setDefaultTimeoutNoReply(params: AndroidSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise; } -export type AndroidDevicesParams = {}; -export type AndroidDevicesOptions = {}; +export type AndroidDevicesParams = { + port?: number, +}; +export type AndroidDevicesOptions = { + port?: number, +}; export type AndroidDevicesResult = { devices: AndroidDeviceChannel[], }; diff --git a/packages/playwright-core/src/protocol/protocol.yml b/packages/playwright-core/src/protocol/protocol.yml index 227eb1e79a..c50c36f529 100644 --- a/packages/playwright-core/src/protocol/protocol.yml +++ b/packages/playwright-core/src/protocol/protocol.yml @@ -2793,6 +2793,8 @@ Android: commands: devices: + parameters: + port: number? returns: devices: type: array diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 67d81ff6bd..997a5a7e1b 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1271,7 +1271,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { arg: tType('SerializedArgument'), }); scheme.ElectronApplicationCloseParams = tOptional(tObject({})); - scheme.AndroidDevicesParams = tOptional(tObject({})); + scheme.AndroidDevicesParams = tObject({ + port: tOptional(tNumber), + }); scheme.AndroidSetDefaultTimeoutNoReplyParams = tObject({ timeout: tNumber, }); diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index 674b6a87e6..fefd9f2ece 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -38,7 +38,7 @@ import { SdkObject, internalCallMetadata } from '../instrumentation'; const ARTIFACTS_FOLDER = path.join(os.tmpdir(), 'playwright-artifacts-'); export interface Backend { - devices(): Promise; + devices(options: types.AndroidDeviceOptions): Promise; } export interface DeviceBackend { @@ -73,8 +73,8 @@ export class Android extends SdkObject { this._timeoutSettings.setDefaultTimeout(timeout); } - async devices(): Promise { - const devices = (await this._backend.devices()).filter(d => d.status === 'device'); + async devices(options: types.AndroidDeviceOptions): Promise { + const devices = (await this._backend.devices(options)).filter(d => d.status === 'device'); const newSerials = new Set(); for (const d of devices) { newSerials.add(d.serial); diff --git a/packages/playwright-core/src/server/android/backendAdb.ts b/packages/playwright-core/src/server/android/backendAdb.ts index 168850f04a..9c579823a0 100644 --- a/packages/playwright-core/src/server/android/backendAdb.ts +++ b/packages/playwright-core/src/server/android/backendAdb.ts @@ -16,30 +16,26 @@ import assert from 'assert'; import debug from 'debug'; +import * as types from '../types'; import * as net from 'net'; import { EventEmitter } from 'events'; import { Backend, DeviceBackend, SocketBackend } from './android'; import { createGuid } from '../../utils/utils'; export class AdbBackend implements Backend { - async devices(): Promise { - const result = await runCommand('host:devices'); + async devices(options: types.AndroidDeviceOptions = {}): Promise { + const port = options.port ? options.port : 5037; + const result = await runCommand('host:devices', port); const lines = result.toString().trim().split('\n'); return lines.map(line => { const [serial, status] = line.trim().split('\t'); - return new AdbDevice(serial, status); + return new AdbDevice(serial, status, port); }); } } class AdbDevice implements DeviceBackend { - readonly serial: string; - readonly status: string; - - constructor(serial: string, status: string) { - this.serial = serial; - this.status = status; - } + constructor(readonly serial: string, readonly status: string, readonly port: number) { } async init() { } @@ -48,19 +44,19 @@ class AdbDevice implements DeviceBackend { } runCommand(command: string): Promise { - return runCommand(command, this.serial); + return runCommand(command, this.port, this.serial); } async open(command: string): Promise { - const result = await open(command, this.serial); + const result = await open(command, this.port, this.serial); result.becomeSocket(); return result; } } -async function runCommand(command: string, serial?: string): Promise { +async function runCommand(command: string, port: number = 5037, serial?: string): Promise { debug('pw:adb:runCommand')(command, serial); - const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 })); + const socket = new BufferedSocketWrapper(command, net.createConnection({ port })); if (serial) { await socket.write(encodeMessage(`host:transport:${serial}`)); const status = await socket.read(4); @@ -80,8 +76,8 @@ async function runCommand(command: string, serial?: string): Promise { return commandOutput; } -async function open(command: string, serial?: string): Promise { - const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 })); +async function open(command: string, port: number = 5037, serial?: string): Promise { + const socket = new BufferedSocketWrapper(command, net.createConnection({ port })); if (serial) { await socket.write(encodeMessage(`host:transport:${serial}`)); const status = await socket.read(4); diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index a3e802df74..df2d844a7d 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -366,3 +366,7 @@ export type APIResponse = { headers: HeadersArray, body: Buffer, }; + +export type AndroidDeviceOptions = { + port?: number +}; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 78fffed093..c4669c0d75 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -11020,8 +11020,14 @@ export {}; export interface Android { /** * Returns the list of detected Android devices. + * @param options */ - devices(): Promise>; + devices(options?: { + /** + * Optional port to establish ADB server connection. + */ + port?: number; + }): Promise>; /** * This setting will change the default maximum time for all the methods accepting `timeout` option. @@ -11032,7 +11038,7 @@ export interface Android { /** * [AndroidDevice] represents a connected device, either real hardware or emulated. Devices can be obtained using - * [android.devices()](https://playwright.dev/docs/api/class-android#android-devices). + * [android.devices([options])](https://playwright.dev/docs/api/class-android#android-devices). */ export interface AndroidDevice { /** diff --git a/tests/android/browser.spec.ts b/tests/android/browser.spec.ts index 4618fe9015..cb0a07a4b8 100644 --- a/tests/android/browser.spec.ts +++ b/tests/android/browser.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import net from 'net'; import { androidTest as test, expect } from './androidTest'; test('androidDevice.model', async function({ androidDevice }) { @@ -56,6 +57,32 @@ test('should be able to send CDP messages', async ({ androidDevice }) => { expect(evalResponse.result.value).toBe(3); }); +test('should be able to use a custom port', async function({ playwright }) { + const proxyPort = 5038; + let countOfIncomingConnections = 0; + let countOfConnections = 0; + const server = net.createServer(socket => { + ++countOfIncomingConnections; + ++countOfConnections; + socket.on('close', () => countOfConnections--); + const client = net.connect(5037); + socket.pipe(client).pipe(socket); + }); + await new Promise(resolve => server.listen(proxyPort, resolve)); + + const devices = await playwright._android.devices({ port: proxyPort }); + expect(countOfIncomingConnections).toBeGreaterThanOrEqual(1); + expect(devices).toHaveLength(1); + const device = devices[0]; + const value = await device.shell('echo foobar'); + expect(value.toString()).toBe('foobar\n'); + await device.close(); + + await new Promise(resolve => server.close(resolve)); + expect(countOfIncomingConnections).toBeGreaterThanOrEqual(1); + expect(countOfConnections).toBe(0); +}); + test('should be able to pass context options', async ({ androidDevice, httpsServer }) => { const context = await androidDevice.launchBrowser({ colorScheme: 'dark',