feat(android): add Android.{launchServer,connect} (#18263)
Fixes https://github.com/microsoft/playwright/issues/17538
This commit is contained in:
parent
d3948d1308
commit
805312b722
|
|
@ -78,6 +78,39 @@ Note that since you don't need Playwright to install web browsers when testing A
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -D playwright
|
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm i -D playwright
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## async method: Android.connect
|
||||||
|
* since: v1.28
|
||||||
|
- returns: <[AndroidDevice]>
|
||||||
|
|
||||||
|
This methods attaches Playwright to an existing Android device.
|
||||||
|
Use [`method: Android.launchServer`] to launch a new Android server instance.
|
||||||
|
|
||||||
|
### param: Android.connect.wsEndpoint
|
||||||
|
* since: v1.28
|
||||||
|
- `wsEndpoint` <[string]>
|
||||||
|
|
||||||
|
A browser websocket endpoint to connect to.
|
||||||
|
|
||||||
|
### option: Android.connect.headers
|
||||||
|
* since: v1.28
|
||||||
|
- `headers` <[Object]<[string], [string]>>
|
||||||
|
|
||||||
|
Additional HTTP headers to be sent with web socket connect request. Optional.
|
||||||
|
|
||||||
|
### option: Android.connect.slowMo
|
||||||
|
* since: v1.28
|
||||||
|
- `slowMo` <[float]>
|
||||||
|
|
||||||
|
Slows down Playwright operations by the specified amount of milliseconds. Useful so that you
|
||||||
|
can see what is going on. Defaults to `0`.
|
||||||
|
|
||||||
|
### option: Android.connect.timeout
|
||||||
|
* since: v1.28
|
||||||
|
- `timeout` <[float]>
|
||||||
|
|
||||||
|
Maximum time in milliseconds to wait for the connection to be established. Defaults to
|
||||||
|
`30000` (30 seconds). Pass `0` to disable timeout.
|
||||||
|
|
||||||
## async method: Android.devices
|
## async method: Android.devices
|
||||||
* since: v1.9
|
* since: v1.9
|
||||||
- returns: <[Array]<[AndroidDevice]>>
|
- returns: <[Array]<[AndroidDevice]>>
|
||||||
|
|
@ -102,6 +135,94 @@ Optional port to establish ADB server connection. Default to `5037`.
|
||||||
|
|
||||||
Prevents automatic playwright driver installation on attach. Assumes that the drivers have been installed already.
|
Prevents automatic playwright driver installation on attach. Assumes that the drivers have been installed already.
|
||||||
|
|
||||||
|
## async method: Android.launchServer
|
||||||
|
* since: v1.28
|
||||||
|
* langs: js
|
||||||
|
- returns: <[BrowserServer]>
|
||||||
|
|
||||||
|
Launches Playwright Android server that clients can connect to. See the following example:
|
||||||
|
|
||||||
|
Server Side:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { _android } = require('playwright');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const browserServer = await _android.launchServer({
|
||||||
|
// If you have multiple devices connected and want to use a specific one.
|
||||||
|
// deviceSerialNumber: '<deviceSerialNumber>',
|
||||||
|
});
|
||||||
|
const wsEndpoint = browserServer.wsEndpoint();
|
||||||
|
console.log(wsEndpoint);
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
Client Side:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { _android } = require('playwright');
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const device = await _android.connect('<wsEndpoint>');
|
||||||
|
|
||||||
|
console.log(device.model());
|
||||||
|
console.log(device.serial());
|
||||||
|
await device.shell('am force-stop com.android.chrome');
|
||||||
|
const context = await device.launchBrowser();
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
await page.goto('https://webkit.org/');
|
||||||
|
console.log(await page.evaluate(() => window.location.href));
|
||||||
|
await page.screenshot({ path: 'page-chrome-1.png' });
|
||||||
|
|
||||||
|
await context.close();
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### option: Android.launchServer.adbHost
|
||||||
|
* since: v1.28
|
||||||
|
- `adbHost` <[string]>
|
||||||
|
|
||||||
|
Optional host to establish ADB server connection. Default to `127.0.0.1`.
|
||||||
|
|
||||||
|
### option: Android.launchServer.adbPort
|
||||||
|
* since: v1.28
|
||||||
|
- `adbPort` <[int]>
|
||||||
|
|
||||||
|
Optional port to establish ADB server connection. Default to `5037`.
|
||||||
|
|
||||||
|
### option: Android.launchServer.omitDriverInstall
|
||||||
|
* since: v1.28
|
||||||
|
- `omitDriverInstall` <[boolean]>
|
||||||
|
|
||||||
|
Prevents automatic playwright driver installation on attach. Assumes that the drivers have been installed already.
|
||||||
|
|
||||||
|
### option: Android.launchServer.deviceSerialNumber
|
||||||
|
* since: v1.28
|
||||||
|
- `deviceSerialNumber` <[string]>
|
||||||
|
|
||||||
|
Optional device serial number to launch the browser on. If not specified, it will
|
||||||
|
throw if multiple devices are connected.
|
||||||
|
|
||||||
|
### option: Android.launchServer.port
|
||||||
|
* since: v1.28
|
||||||
|
- `port` <[int]>
|
||||||
|
|
||||||
|
Port to use for the web socket. Defaults to 0 that picks any available port.
|
||||||
|
|
||||||
|
### option: Android.launchServer.wsPath
|
||||||
|
* since: v1.28
|
||||||
|
- `wsPath` <[string]>
|
||||||
|
|
||||||
|
Path at which to serve the Android Server. For security, this defaults to an
|
||||||
|
unguessable string.
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
Any process or web page (including those running in Playwright) with knowledge
|
||||||
|
of the `wsPath` can take control of the OS user. For this reason, you should
|
||||||
|
use an unguessable token when using this option.
|
||||||
|
:::
|
||||||
|
|
||||||
## method: Android.setDefaultTimeout
|
## method: Android.setDefaultTimeout
|
||||||
* since: v1.9
|
* since: v1.9
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
[browserServerImpl.ts]
|
[browserServerImpl.ts]
|
||||||
**
|
**
|
||||||
|
|
||||||
|
[androidServerImpl.ts]
|
||||||
|
**
|
||||||
|
|
||||||
[inProcessFactory.ts]
|
[inProcessFactory.ts]
|
||||||
**
|
**
|
||||||
|
|
||||||
|
|
|
||||||
62
packages/playwright-core/src/androidServerImpl.ts
Normal file
62
packages/playwright-core/src/androidServerImpl.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the 'License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { LaunchAndroidServerOptions } from './client/types';
|
||||||
|
import { ws } from './utilsBundle';
|
||||||
|
import type { WebSocketEventEmitter } from './utilsBundle';
|
||||||
|
import type { BrowserServer } from './client/browserType';
|
||||||
|
import { createGuid } from './utils';
|
||||||
|
import { createPlaywright } from './server/playwright';
|
||||||
|
import { PlaywrightServer } from './remote/playwrightServer';
|
||||||
|
|
||||||
|
export class AndroidServerLauncherImpl {
|
||||||
|
async launchServer(options: LaunchAndroidServerOptions = {}): Promise<BrowserServer> {
|
||||||
|
const playwright = createPlaywright('javascript');
|
||||||
|
// 1. Pre-connect to the device
|
||||||
|
let devices = await playwright.android.devices({
|
||||||
|
host: options.adbHost,
|
||||||
|
port: options.adbPort,
|
||||||
|
omitDriverInstall: options.omitDriverInstall,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (devices.length === 0)
|
||||||
|
throw new Error('No devices found');
|
||||||
|
|
||||||
|
if (options.deviceSerialNumber) {
|
||||||
|
devices = devices.filter(d => d.serial === options.deviceSerialNumber);
|
||||||
|
if (devices.length === 0)
|
||||||
|
throw new Error(`No device with serial number '${options.deviceSerialNumber}' not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (devices.length > 1)
|
||||||
|
throw new Error(`More than one device found. Please specify deviceSerialNumber`);
|
||||||
|
|
||||||
|
const device = devices[0];
|
||||||
|
|
||||||
|
const path = options.wsPath ? (options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`) : `/${createGuid()}`;
|
||||||
|
|
||||||
|
// 2. Start the server
|
||||||
|
const server = new PlaywrightServer({ path, maxConnections: 1, enableSocksProxy: false, preLaunchedAndroidDevice: device });
|
||||||
|
const wsEndpoint = await server.listen(options.port);
|
||||||
|
|
||||||
|
// 3. Return the BrowserServer interface
|
||||||
|
const browserServer = new ws.EventEmitter() as (BrowserServer & WebSocketEventEmitter);
|
||||||
|
browserServer.wsEndpoint = () => wsEndpoint;
|
||||||
|
browserServer.close = () => device.close();
|
||||||
|
browserServer.kill = () => device.close();
|
||||||
|
return browserServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,9 +49,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
let path = `/${createGuid()}`;
|
const path = options.wsPath ? (options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`) : `/${createGuid()}`;
|
||||||
if (options.wsPath)
|
|
||||||
path = options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`;
|
|
||||||
|
|
||||||
// 2. Start the server
|
// 2. Start the server
|
||||||
const server = new PlaywrightServer({ path, maxConnections: Infinity, enableSocksProxy: false, preLaunchedBrowser: browser });
|
const server = new PlaywrightServer({ path, maxConnections: Infinity, enableSocksProxy: false, preLaunchedBrowser: browser });
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { isString, isRegExp } from '../utils';
|
import { isString, isRegExp, monotonicTime } from '../utils';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
||||||
|
|
@ -26,12 +26,17 @@ import type { Page } from './page';
|
||||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||||
import { Waiter } from './waiter';
|
import { Waiter } from './waiter';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { Connection } from './connection';
|
||||||
|
import { isSafeCloseError, kBrowserClosedError } from '../common/errors';
|
||||||
|
import { raceAgainstTimeout } from '../utils/timeoutRunner';
|
||||||
|
import type { AndroidServerLauncherImpl } from '../androidServerImpl';
|
||||||
|
|
||||||
type Direction = 'down' | 'up' | 'left' | 'right';
|
type Direction = 'down' | 'up' | 'left' | 'right';
|
||||||
type SpeedOptions = { speed?: number };
|
type SpeedOptions = { speed?: number };
|
||||||
|
|
||||||
export class Android extends ChannelOwner<channels.AndroidChannel> implements api.Android {
|
export class Android extends ChannelOwner<channels.AndroidChannel> implements api.Android {
|
||||||
readonly _timeoutSettings: TimeoutSettings;
|
readonly _timeoutSettings: TimeoutSettings;
|
||||||
|
_serverLauncher?: AndroidServerLauncherImpl;
|
||||||
|
|
||||||
static from(android: channels.AndroidChannel): Android {
|
static from(android: channels.AndroidChannel): Android {
|
||||||
return (android as any)._object;
|
return (android as any)._object;
|
||||||
|
|
@ -51,11 +56,68 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
|
||||||
const { devices } = await this._channel.devices(options);
|
const { devices } = await this._channel.devices(options);
|
||||||
return devices.map(d => AndroidDevice.from(d));
|
return devices.map(d => AndroidDevice.from(d));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async launchServer(options: types.LaunchServerOptions = {}): Promise<api.BrowserServer> {
|
||||||
|
if (!this._serverLauncher)
|
||||||
|
throw new Error('Launching server is not supported');
|
||||||
|
return this._serverLauncher.launchServer(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(wsEndpoint: string, options: Parameters<api.Android['connect']>[1] = {}): Promise<api.AndroidDevice> {
|
||||||
|
return await this._wrapApiCall(async () => {
|
||||||
|
const deadline = options.timeout ? monotonicTime() + options.timeout : 0;
|
||||||
|
const headers = { 'x-playwright-browser': 'android', ...options.headers };
|
||||||
|
const localUtils = this._connection.localUtils();
|
||||||
|
const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: options.slowMo, timeout: options.timeout };
|
||||||
|
const { pipe } = await localUtils._channel.connect(connectParams);
|
||||||
|
const closePipe = () => pipe.close().catch(() => {});
|
||||||
|
const connection = new Connection(localUtils);
|
||||||
|
connection.markAsRemote();
|
||||||
|
connection.on('close', closePipe);
|
||||||
|
|
||||||
|
let device: AndroidDevice;
|
||||||
|
let closeError: string | undefined;
|
||||||
|
const onPipeClosed = () => {
|
||||||
|
device?._didClose();
|
||||||
|
connection.close(closeError || kBrowserClosedError);
|
||||||
|
};
|
||||||
|
pipe.on('closed', onPipeClosed);
|
||||||
|
connection.onmessage = message => pipe.send({ message }).catch(onPipeClosed);
|
||||||
|
|
||||||
|
pipe.on('message', ({ message }) => {
|
||||||
|
try {
|
||||||
|
connection!.dispatch(message);
|
||||||
|
} catch (e) {
|
||||||
|
closeError = e.toString();
|
||||||
|
closePipe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await raceAgainstTimeout(async () => {
|
||||||
|
const playwright = await connection!.initializePlaywright();
|
||||||
|
if (!playwright._initializer.preConnectedAndroidDevice) {
|
||||||
|
closePipe();
|
||||||
|
throw new Error('Malformed endpoint. Did you use Android.launchServer method?');
|
||||||
|
}
|
||||||
|
device = AndroidDevice.from(playwright._initializer.preConnectedAndroidDevice!);
|
||||||
|
device._shouldCloseConnectionOnClose = true;
|
||||||
|
device.on(Events.AndroidDevice.Close, closePipe);
|
||||||
|
return device;
|
||||||
|
}, deadline ? deadline - monotonicTime() : 0);
|
||||||
|
if (!result.timedOut) {
|
||||||
|
return result.result;
|
||||||
|
} else {
|
||||||
|
closePipe();
|
||||||
|
throw new Error(`Timeout ${options.timeout}ms exceeded`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> implements api.AndroidDevice {
|
export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> implements api.AndroidDevice {
|
||||||
readonly _timeoutSettings: TimeoutSettings;
|
readonly _timeoutSettings: TimeoutSettings;
|
||||||
private _webViews = new Map<string, AndroidWebView>();
|
private _webViews = new Map<string, AndroidWebView>();
|
||||||
|
_shouldCloseConnectionOnClose = false;
|
||||||
|
|
||||||
static from(androidDevice: channels.AndroidDeviceChannel): AndroidDevice {
|
static from(androidDevice: channels.AndroidDeviceChannel): AndroidDevice {
|
||||||
return (androidDevice as any)._object;
|
return (androidDevice as any)._object;
|
||||||
|
|
@ -172,7 +234,20 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
|
||||||
}
|
}
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
await this._channel.close();
|
try {
|
||||||
|
this._didClose();
|
||||||
|
if (this._shouldCloseConnectionOnClose)
|
||||||
|
this._connection.close(kBrowserClosedError);
|
||||||
|
else
|
||||||
|
await this._channel.close();
|
||||||
|
} catch (e) {
|
||||||
|
if (isSafeCloseError(e))
|
||||||
|
return;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_didClose() {
|
||||||
this.emit(Events.AndroidDevice.Close);
|
this.emit(Events.AndroidDevice.Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
const logger = params.logger;
|
const logger = params.logger;
|
||||||
return await this._wrapApiCall(async () => {
|
return await this._wrapApiCall(async () => {
|
||||||
const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
|
const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
|
||||||
let browser: Browser;
|
|
||||||
const headers = { 'x-playwright-browser': this.name(), ...params.headers };
|
const headers = { 'x-playwright-browser': this.name(), ...params.headers };
|
||||||
const localUtils = this._connection.localUtils();
|
const localUtils = this._connection.localUtils();
|
||||||
const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: params.slowMo, timeout: params.timeout };
|
const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: params.slowMo, timeout: params.timeout };
|
||||||
|
|
@ -153,10 +152,11 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
|
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
|
||||||
const { pipe } = await localUtils._channel.connect(connectParams);
|
const { pipe } = await localUtils._channel.connect(connectParams);
|
||||||
const closePipe = () => pipe.close().catch(() => {});
|
const closePipe = () => pipe.close().catch(() => {});
|
||||||
const connection = new Connection(this._connection.localUtils());
|
const connection = new Connection(localUtils);
|
||||||
connection.markAsRemote();
|
connection.markAsRemote();
|
||||||
connection.on('close', closePipe);
|
connection.on('close', closePipe);
|
||||||
|
|
||||||
|
let browser: Browser;
|
||||||
let closeError: string | undefined;
|
let closeError: string | undefined;
|
||||||
const onPipeClosed = () => {
|
const onPipeClosed = () => {
|
||||||
// Emulate all pages, contexts and the browser closing upon disconnect.
|
// Emulate all pages, contexts and the browser closing upon disconnect.
|
||||||
|
|
@ -188,7 +188,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
||||||
const playwright = await connection!.initializePlaywright();
|
const playwright = await connection!.initializePlaywright();
|
||||||
if (!playwright._initializer.preLaunchedBrowser) {
|
if (!playwright._initializer.preLaunchedBrowser) {
|
||||||
closePipe();
|
closePipe();
|
||||||
throw new Error('Malformed endpoint. Did you use launchServer method?');
|
throw new Error('Malformed endpoint. Did you use BrowserType.launchServer method?');
|
||||||
}
|
}
|
||||||
playwright._setSelectors(this._playwright.selectors);
|
playwright._setSelectors(this._playwright.selectors);
|
||||||
browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
|
browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,15 @@ export type LaunchServerOptions = {
|
||||||
logger?: Logger,
|
logger?: Logger,
|
||||||
} & FirefoxUserPrefs;
|
} & FirefoxUserPrefs;
|
||||||
|
|
||||||
|
export type LaunchAndroidServerOptions = {
|
||||||
|
deviceSerialNumber?: string,
|
||||||
|
adbHost?: string,
|
||||||
|
adbPort?: number,
|
||||||
|
omitDriverInstall?: boolean,
|
||||||
|
port?: number,
|
||||||
|
wsPath?: string,
|
||||||
|
};
|
||||||
|
|
||||||
export type SelectorEngine = {
|
export type SelectorEngine = {
|
||||||
/**
|
/**
|
||||||
* Returns the first element matching given selector in the root's subtree.
|
* Returns the first element matching given selector in the root's subtree.
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ function launchGridBrowserWorker(gridURL: string, agentId: string, workerId: str
|
||||||
const log = debug(`pw:grid:worker:${workerId}`);
|
const log = debug(`pw:grid:worker:${workerId}`);
|
||||||
log('created');
|
log('created');
|
||||||
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
|
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
|
||||||
new PlaywrightConnection(Promise.resolve(), 'launch-browser', ws, { enableSocksProxy: true, browserName, launchOptions: {} }, { playwright: null, browser: null }, log, async () => {
|
new PlaywrightConnection(Promise.resolve(), 'launch-browser', ws, { enableSocksProxy: true, browserName, launchOptions: {} }, { }, log, async () => {
|
||||||
log('exiting process');
|
log('exiting process');
|
||||||
setTimeout(() => process.exit(0), 30000);
|
setTimeout(() => process.exit(0), 30000);
|
||||||
// Meanwhile, try to gracefully close all browsers.
|
// Meanwhile, try to gracefully close all browsers.
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import type { Playwright as PlaywrightAPI } from './client/playwright';
|
||||||
import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from './server';
|
import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from './server';
|
||||||
import { Connection } from './client/connection';
|
import { Connection } from './client/connection';
|
||||||
import { BrowserServerLauncherImpl } from './browserServerImpl';
|
import { BrowserServerLauncherImpl } from './browserServerImpl';
|
||||||
|
import { AndroidServerLauncherImpl } from './androidServerImpl';
|
||||||
|
|
||||||
export function createInProcessPlaywright(): PlaywrightAPI {
|
export function createInProcessPlaywright(): PlaywrightAPI {
|
||||||
const playwright = createPlaywright('javascript');
|
const playwright = createPlaywright('javascript');
|
||||||
|
|
@ -37,6 +38,7 @@ export function createInProcessPlaywright(): PlaywrightAPI {
|
||||||
playwrightAPI.chromium._serverLauncher = new BrowserServerLauncherImpl('chromium');
|
playwrightAPI.chromium._serverLauncher = new BrowserServerLauncherImpl('chromium');
|
||||||
playwrightAPI.firefox._serverLauncher = new BrowserServerLauncherImpl('firefox');
|
playwrightAPI.firefox._serverLauncher = new BrowserServerLauncherImpl('firefox');
|
||||||
playwrightAPI.webkit._serverLauncher = new BrowserServerLauncherImpl('webkit');
|
playwrightAPI.webkit._serverLauncher = new BrowserServerLauncherImpl('webkit');
|
||||||
|
playwrightAPI._android._serverLauncher = new AndroidServerLauncherImpl();
|
||||||
|
|
||||||
// Switch to async dispatch after we got Playwright object.
|
// Switch to async dispatch after we got Playwright object.
|
||||||
dispatcherConnection.onmessage = message => setImmediate(() => clientConnection.dispatch(message));
|
dispatcherConnection.onmessage = message => setImmediate(() => clientConnection.dispatch(message));
|
||||||
|
|
|
||||||
|
|
@ -289,6 +289,7 @@ scheme.PlaywrightInitializer = tObject({
|
||||||
})),
|
})),
|
||||||
selectors: tChannel(['Selectors']),
|
selectors: tChannel(['Selectors']),
|
||||||
preLaunchedBrowser: tOptional(tChannel(['Browser'])),
|
preLaunchedBrowser: tOptional(tChannel(['Browser'])),
|
||||||
|
preConnectedAndroidDevice: tOptional(tChannel(['AndroidDevice'])),
|
||||||
socksSupport: tOptional(tChannel(['SocksSupport'])),
|
socksSupport: tOptional(tChannel(['SocksSupport'])),
|
||||||
});
|
});
|
||||||
scheme.PlaywrightNewRequestParams = tObject({
|
scheme.PlaywrightNewRequestParams = tObject({
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
../client/
|
../client/
|
||||||
../common/
|
../common/
|
||||||
../server/
|
../server/
|
||||||
|
../server/android/
|
||||||
../server/dispatchers/
|
../server/dispatchers/
|
||||||
../utils/
|
../utils/
|
||||||
../utilsBundle.ts
|
../utilsBundle.ts
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { gracefullyCloseAll } from '../utils/processLauncher';
|
||||||
import { SocksProxy } from '../common/socksProxy';
|
import { SocksProxy } from '../common/socksProxy';
|
||||||
import { assert } from '../utils';
|
import { assert } from '../utils';
|
||||||
import type { LaunchOptions } from '../server/types';
|
import type { LaunchOptions } from '../server/types';
|
||||||
|
import { AndroidDevice } from '../server/android/android';
|
||||||
import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher';
|
import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher';
|
||||||
|
|
||||||
export type ClientType = 'controller' | 'playwright' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser';
|
export type ClientType = 'controller' | 'playwright' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser';
|
||||||
|
|
@ -34,8 +35,9 @@ type Options = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type PreLaunched = {
|
type PreLaunched = {
|
||||||
playwright: Playwright | null;
|
playwright?: Playwright | undefined;
|
||||||
browser: Browser | null;
|
browser?: Browser | undefined;
|
||||||
|
androidDevice?: AndroidDevice | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PlaywrightConnection {
|
export class PlaywrightConnection {
|
||||||
|
|
@ -56,7 +58,7 @@ export class PlaywrightConnection {
|
||||||
if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser')
|
if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser')
|
||||||
assert(preLaunched.playwright);
|
assert(preLaunched.playwright);
|
||||||
if (clientType === 'pre-launched-browser')
|
if (clientType === 'pre-launched-browser')
|
||||||
assert(preLaunched.browser);
|
assert(preLaunched.browser || preLaunched.androidDevice);
|
||||||
this._onClose = onClose;
|
this._onClose = onClose;
|
||||||
this._debugLog = log;
|
this._debugLog = log;
|
||||||
|
|
||||||
|
|
@ -72,7 +74,7 @@ export class PlaywrightConnection {
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on('close', () => this._onDisconnect());
|
ws.on('close', () => this._onDisconnect());
|
||||||
ws.on('error', error => this._onDisconnect(error));
|
ws.on('error', (error: Error) => this._onDisconnect(error));
|
||||||
|
|
||||||
if (clientType === 'controller') {
|
if (clientType === 'controller') {
|
||||||
this._root = this._initDebugControllerMode();
|
this._root = this._initDebugControllerMode();
|
||||||
|
|
@ -83,7 +85,7 @@ export class PlaywrightConnection {
|
||||||
if (clientType === 'reuse-browser')
|
if (clientType === 'reuse-browser')
|
||||||
return await this._initReuseBrowsersMode(scope);
|
return await this._initReuseBrowsersMode(scope);
|
||||||
if (clientType === 'pre-launched-browser')
|
if (clientType === 'pre-launched-browser')
|
||||||
return await this._initPreLaunchedBrowserMode(scope);
|
return this._preLaunched.browser ? await this._initPreLaunchedBrowserMode(scope) : await this._initPreLaunchedAndroidMode(scope);
|
||||||
if (clientType === 'launch-browser')
|
if (clientType === 'launch-browser')
|
||||||
return await this._initLaunchBrowserMode(scope);
|
return await this._initLaunchBrowserMode(scope);
|
||||||
if (clientType === 'playwright')
|
if (clientType === 'playwright')
|
||||||
|
|
@ -122,7 +124,7 @@ export class PlaywrightConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _initPreLaunchedBrowserMode(scope: RootDispatcher) {
|
private async _initPreLaunchedBrowserMode(scope: RootDispatcher) {
|
||||||
this._debugLog(`engaged pre-launched mode`);
|
this._debugLog(`engaged pre-launched (browser) mode`);
|
||||||
const playwright = this._preLaunched.playwright!;
|
const playwright = this._preLaunched.playwright!;
|
||||||
const browser = this._preLaunched.browser!;
|
const browser = this._preLaunched.browser!;
|
||||||
browser.on(Browser.Events.Disconnected, () => {
|
browser.on(Browser.Events.Disconnected, () => {
|
||||||
|
|
@ -139,6 +141,19 @@ export class PlaywrightConnection {
|
||||||
return playwrightDispatcher;
|
return playwrightDispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _initPreLaunchedAndroidMode(scope: RootDispatcher) {
|
||||||
|
this._debugLog(`engaged pre-launched (Android) mode`);
|
||||||
|
const playwright = this._preLaunched.playwright!;
|
||||||
|
const androidDevice = this._preLaunched.androidDevice!;
|
||||||
|
androidDevice.on(AndroidDevice.Events.Closed, () => {
|
||||||
|
// Underlying browser did close for some reason - force disconnect the client.
|
||||||
|
this.close({ code: 1001, reason: 'Android device disconnected' });
|
||||||
|
});
|
||||||
|
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, undefined, undefined, androidDevice);
|
||||||
|
this._cleanups.push(() => playwrightDispatcher.cleanup());
|
||||||
|
return playwrightDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
private _initDebugControllerMode(): DebugControllerDispatcher {
|
private _initDebugControllerMode(): DebugControllerDispatcher {
|
||||||
this._debugLog(`engaged reuse controller mode`);
|
this._debugLog(`engaged reuse controller mode`);
|
||||||
const playwright = this._preLaunched.playwright!;
|
const playwright = this._preLaunched.playwright!;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { PlaywrightConnection } from './playwrightConnection';
|
||||||
import type { ClientType } from './playwrightConnection';
|
import type { ClientType } from './playwrightConnection';
|
||||||
import type { LaunchOptions } from '../server/types';
|
import type { LaunchOptions } from '../server/types';
|
||||||
import { ManualPromise } from '../utils/manualPromise';
|
import { ManualPromise } from '../utils/manualPromise';
|
||||||
|
import type { AndroidDevice } from '../server/android/android';
|
||||||
|
|
||||||
const debugLog = debug('pw:server');
|
const debugLog = debug('pw:server');
|
||||||
|
|
||||||
|
|
@ -40,10 +41,11 @@ type ServerOptions = {
|
||||||
maxConnections: number;
|
maxConnections: number;
|
||||||
enableSocksProxy: boolean;
|
enableSocksProxy: boolean;
|
||||||
preLaunchedBrowser?: Browser
|
preLaunchedBrowser?: Browser
|
||||||
|
preLaunchedAndroidDevice?: AndroidDevice
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PlaywrightServer {
|
export class PlaywrightServer {
|
||||||
private _preLaunchedPlaywright: Playwright | null = null;
|
private _preLaunchedPlaywright: Playwright | undefined;
|
||||||
private _wsServer: WebSocketServer | undefined;
|
private _wsServer: WebSocketServer | undefined;
|
||||||
private _options: ServerOptions;
|
private _options: ServerOptions;
|
||||||
|
|
||||||
|
|
@ -51,6 +53,8 @@ export class PlaywrightServer {
|
||||||
this._options = options;
|
this._options = options;
|
||||||
if (options.preLaunchedBrowser)
|
if (options.preLaunchedBrowser)
|
||||||
this._preLaunchedPlaywright = options.preLaunchedBrowser.options.rootSdkObject as Playwright;
|
this._preLaunchedPlaywright = options.preLaunchedBrowser.options.rootSdkObject as Playwright;
|
||||||
|
if (options.preLaunchedAndroidDevice)
|
||||||
|
this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android._playwrightOptions.rootSdkObject as Playwright;
|
||||||
}
|
}
|
||||||
|
|
||||||
preLaunchedPlaywright(): Playwright {
|
preLaunchedPlaywright(): Playwright {
|
||||||
|
|
@ -121,7 +125,7 @@ export class PlaywrightServer {
|
||||||
clientType = 'controller';
|
clientType = 'controller';
|
||||||
else if (shouldReuseBrowser)
|
else if (shouldReuseBrowser)
|
||||||
clientType = 'reuse-browser';
|
clientType = 'reuse-browser';
|
||||||
else if (this._options.preLaunchedBrowser)
|
else if (this._options.preLaunchedBrowser || this._options.preLaunchedAndroidDevice)
|
||||||
clientType = 'pre-launched-browser';
|
clientType = 'pre-launched-browser';
|
||||||
else if (browserName)
|
else if (browserName)
|
||||||
clientType = 'launch-browser';
|
clientType = 'launch-browser';
|
||||||
|
|
@ -130,7 +134,7 @@ export class PlaywrightServer {
|
||||||
semaphore.aquire(),
|
semaphore.aquire(),
|
||||||
clientType, ws,
|
clientType, ws,
|
||||||
{ enableSocksProxy, browserName, launchOptions },
|
{ enableSocksProxy, browserName, launchOptions },
|
||||||
{ playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null },
|
{ playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser, androidDevice: this._options.preLaunchedAndroidDevice },
|
||||||
log, () => semaphore.release());
|
log, () => semaphore.release());
|
||||||
(ws as any)[kConnectionSymbol] = connection;
|
(ws as any)[kConnectionSymbol] = connection;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export class AndroidDevice extends SdkObject {
|
||||||
};
|
};
|
||||||
|
|
||||||
private _browserConnections = new Set<AndroidBrowser>();
|
private _browserConnections = new Set<AndroidBrowser>();
|
||||||
private _android: Android;
|
readonly _android: Android;
|
||||||
private _isClosed = false;
|
private _isClosed = false;
|
||||||
|
|
||||||
constructor(android: Android, backend: DeviceBackend, model: string, options: channels.AndroidDevicesOptions) {
|
constructor(android: Android, backend: DeviceBackend, model: string, options: channels.AndroidDevicesOptions) {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ class AdbDevice implements DeviceBackend {
|
||||||
status: string;
|
status: string;
|
||||||
host: string | undefined;
|
host: string | undefined;
|
||||||
port: number | undefined;
|
port: number | undefined;
|
||||||
|
private _closed = false;
|
||||||
|
|
||||||
constructor(serial: string, status: string, host?: string, port?: number) {
|
constructor(serial: string, status: string, host?: string, port?: number) {
|
||||||
this.serial = serial;
|
this.serial = serial;
|
||||||
|
|
@ -49,13 +50,18 @@ class AdbDevice implements DeviceBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
async close() {
|
async close() {
|
||||||
|
this._closed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
runCommand(command: string): Promise<Buffer> {
|
runCommand(command: string): Promise<Buffer> {
|
||||||
|
if (this._closed)
|
||||||
|
throw new Error('Device is closed');
|
||||||
return runCommand(command, this.host, this.port, this.serial);
|
return runCommand(command, this.host, this.port, this.serial);
|
||||||
}
|
}
|
||||||
|
|
||||||
async open(command: string): Promise<SocketBackend> {
|
async open(command: string): Promise<SocketBackend> {
|
||||||
|
if (this._closed)
|
||||||
|
throw new Error('Device is closed');
|
||||||
const result = await open(command, this.host, this.port, this.serial);
|
const result = await open(command, this.host, this.port, this.serial);
|
||||||
result.becomeSocket();
|
result.becomeSocket();
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -31,26 +31,31 @@ import { APIRequestContextDispatcher } from './networkDispatchers';
|
||||||
import { SelectorsDispatcher } from './selectorsDispatcher';
|
import { SelectorsDispatcher } from './selectorsDispatcher';
|
||||||
import { ConnectedBrowserDispatcher } from './browserDispatcher';
|
import { ConnectedBrowserDispatcher } from './browserDispatcher';
|
||||||
import { createGuid } from '../../utils';
|
import { createGuid } from '../../utils';
|
||||||
|
import type { AndroidDevice } from '../android/android';
|
||||||
|
import { AndroidDeviceDispatcher } from './androidDispatcher';
|
||||||
|
|
||||||
export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightChannel, RootDispatcher> implements channels.PlaywrightChannel {
|
export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.PlaywrightChannel, RootDispatcher> implements channels.PlaywrightChannel {
|
||||||
_type_Playwright;
|
_type_Playwright;
|
||||||
private _browserDispatcher: ConnectedBrowserDispatcher | undefined;
|
private _browserDispatcher: ConnectedBrowserDispatcher | undefined;
|
||||||
|
|
||||||
constructor(scope: RootDispatcher, playwright: Playwright, socksProxy?: SocksProxy, preLaunchedBrowser?: Browser) {
|
constructor(scope: RootDispatcher, playwright: Playwright, socksProxy?: SocksProxy, preLaunchedBrowser?: Browser, prelaunchedAndroidDevice?: AndroidDevice) {
|
||||||
const descriptors = require('../deviceDescriptors') as types.Devices;
|
const descriptors = require('../deviceDescriptors') as types.Devices;
|
||||||
const deviceDescriptors = Object.entries(descriptors)
|
const deviceDescriptors = Object.entries(descriptors)
|
||||||
.map(([name, descriptor]) => ({ name, descriptor }));
|
.map(([name, descriptor]) => ({ name, descriptor }));
|
||||||
const browserDispatcher = preLaunchedBrowser ? new ConnectedBrowserDispatcher(scope, preLaunchedBrowser) : undefined;
|
const browserDispatcher = preLaunchedBrowser ? new ConnectedBrowserDispatcher(scope, preLaunchedBrowser) : undefined;
|
||||||
|
const android = new AndroidDispatcher(scope, playwright.android);
|
||||||
|
const prelaunchedAndroidDeviceDispatcher = prelaunchedAndroidDevice ? new AndroidDeviceDispatcher(android, prelaunchedAndroidDevice) : undefined;
|
||||||
super(scope, playwright, 'Playwright', {
|
super(scope, playwright, 'Playwright', {
|
||||||
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
|
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
|
||||||
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
|
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
|
||||||
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
||||||
android: new AndroidDispatcher(scope, playwright.android),
|
android,
|
||||||
electron: new ElectronDispatcher(scope, playwright.electron),
|
electron: new ElectronDispatcher(scope, playwright.electron),
|
||||||
utils: new LocalUtilsDispatcher(scope, playwright),
|
utils: new LocalUtilsDispatcher(scope, playwright),
|
||||||
deviceDescriptors,
|
deviceDescriptors,
|
||||||
selectors: new SelectorsDispatcher(scope, browserDispatcher?.selectors || playwright.selectors),
|
selectors: new SelectorsDispatcher(scope, browserDispatcher?.selectors || playwright.selectors),
|
||||||
preLaunchedBrowser: browserDispatcher,
|
preLaunchedBrowser: browserDispatcher,
|
||||||
|
preConnectedAndroidDevice: prelaunchedAndroidDeviceDispatcher,
|
||||||
socksSupport: socksProxy ? new SocksSupportDispatcher(scope, socksProxy) : undefined,
|
socksSupport: socksProxy ? new SocksSupportDispatcher(scope, socksProxy) : undefined,
|
||||||
});
|
});
|
||||||
this._type_Playwright = true;
|
this._type_Playwright = true;
|
||||||
|
|
|
||||||
104
packages/playwright-core/types/types.d.ts
vendored
104
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -12181,6 +12181,32 @@ export {};
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export interface Android {
|
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 options
|
||||||
|
*/
|
||||||
|
connect(wsEndpoint: string, options?: {
|
||||||
|
/**
|
||||||
|
* Additional HTTP headers to be sent with web socket connect request. Optional.
|
||||||
|
*/
|
||||||
|
headers?: { [key: string]: string; };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on.
|
||||||
|
* Defaults to `0`.
|
||||||
|
*/
|
||||||
|
slowMo?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum time in milliseconds to wait for the connection to be established. Defaults to `30000` (30 seconds). Pass `0` to
|
||||||
|
* disable timeout.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
}): Promise<AndroidDevice>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of detected Android devices.
|
* Returns the list of detected Android devices.
|
||||||
* @param options
|
* @param options
|
||||||
|
|
@ -12202,6 +12228,84 @@ export interface Android {
|
||||||
port?: number;
|
port?: number;
|
||||||
}): Promise<Array<AndroidDevice>>;
|
}): Promise<Array<AndroidDevice>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launches Playwright Android server that clients can connect to. See the following example:
|
||||||
|
*
|
||||||
|
* Server Side:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* const { _android } = require('playwright');
|
||||||
|
*
|
||||||
|
* (async () => {
|
||||||
|
* const browserServer = await _android.launchServer({
|
||||||
|
* // If you have multiple devices connected and want to use a specific one.
|
||||||
|
* // deviceSerialNumber: '<deviceSerialNumber>',
|
||||||
|
* });
|
||||||
|
* const wsEndpoint = browserServer.wsEndpoint();
|
||||||
|
* console.log(wsEndpoint);
|
||||||
|
* })();
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Client Side:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* const { _android } = require('playwright');
|
||||||
|
*
|
||||||
|
* (async () => {
|
||||||
|
* const device = await _android.connect('<wsEndpoint>');
|
||||||
|
*
|
||||||
|
* console.log(device.model());
|
||||||
|
* console.log(device.serial());
|
||||||
|
* await device.shell('am force-stop com.android.chrome');
|
||||||
|
* const context = await device.launchBrowser();
|
||||||
|
*
|
||||||
|
* const page = await context.newPage();
|
||||||
|
* await page.goto('https://webkit.org/');
|
||||||
|
* console.log(await page.evaluate(() => window.location.href));
|
||||||
|
* await page.screenshot({ path: 'page-chrome-1.png' });
|
||||||
|
*
|
||||||
|
* await context.close();
|
||||||
|
* })();
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
launchServer(options?: {
|
||||||
|
/**
|
||||||
|
* Optional host to establish ADB server connection. Default to `127.0.0.1`.
|
||||||
|
*/
|
||||||
|
adbHost?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional port to establish ADB server connection. Default to `5037`.
|
||||||
|
*/
|
||||||
|
adbPort?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional device serial number to launch the browser on. If not specified, it will throw if multiple devices are
|
||||||
|
* connected.
|
||||||
|
*/
|
||||||
|
deviceSerialNumber?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents automatic playwright driver installation on attach. Assumes that the drivers have been installed already.
|
||||||
|
*/
|
||||||
|
omitDriverInstall?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Port to use for the web socket. Defaults to 0 that picks any available port.
|
||||||
|
*/
|
||||||
|
port?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path at which to serve the Android Server. For security, this defaults to an unguessable string.
|
||||||
|
*
|
||||||
|
* > NOTE: Any process or web page (including those running in Playwright) with knowledge of the `wsPath` can take control
|
||||||
|
* of the OS user. For this reason, you should use an unguessable token when using this option.
|
||||||
|
*/
|
||||||
|
wsPath?: string;
|
||||||
|
}): Promise<BrowserServer>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This setting will change the default maximum time for all the methods accepting `timeout` option.
|
* This setting will change the default maximum time for all the methods accepting `timeout` option.
|
||||||
* @param timeout Maximum time in milliseconds
|
* @param timeout Maximum time in milliseconds
|
||||||
|
|
|
||||||
|
|
@ -512,6 +512,7 @@ export type PlaywrightInitializer = {
|
||||||
}[],
|
}[],
|
||||||
selectors: SelectorsChannel,
|
selectors: SelectorsChannel,
|
||||||
preLaunchedBrowser?: BrowserChannel,
|
preLaunchedBrowser?: BrowserChannel,
|
||||||
|
preConnectedAndroidDevice?: AndroidDeviceChannel,
|
||||||
socksSupport?: SocksSupportChannel,
|
socksSupport?: SocksSupportChannel,
|
||||||
};
|
};
|
||||||
export interface PlaywrightEventTarget {
|
export interface PlaywrightEventTarget {
|
||||||
|
|
|
||||||
|
|
@ -597,6 +597,8 @@ Playwright:
|
||||||
selectors: Selectors
|
selectors: Selectors
|
||||||
# Only present when connecting remotely via BrowserType.connect() method.
|
# Only present when connecting remotely via BrowserType.connect() method.
|
||||||
preLaunchedBrowser: Browser?
|
preLaunchedBrowser: Browser?
|
||||||
|
# Only present when connecting remotely via Android.connect() method.
|
||||||
|
preConnectedAndroidDevice: AndroidDevice?
|
||||||
# Only present when socks proxy is supported.
|
# Only present when socks proxy is supported.
|
||||||
socksSupport: SocksSupport?
|
socksSupport: SocksSupport?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,13 @@ test('androidDevice.screenshot', async function({ androidDevice }, testInfo) {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('androidDevice.push', async function({ androidDevice }) {
|
test('androidDevice.push', async function({ androidDevice }) {
|
||||||
await androidDevice.shell('rm /data/local/tmp/hello-world');
|
try {
|
||||||
await androidDevice.push(Buffer.from('hello world'), '/data/local/tmp/hello-world');
|
await androidDevice.push(Buffer.from('hello world'), '/data/local/tmp/hello-world');
|
||||||
const data = await androidDevice.shell('cat /data/local/tmp/hello-world');
|
const data = await androidDevice.shell('cat /data/local/tmp/hello-world');
|
||||||
expect(data).toEqual(Buffer.from('hello world'));
|
expect(data).toEqual(Buffer.from('hello world'));
|
||||||
|
} finally {
|
||||||
|
await androidDevice.shell('rm /data/local/tmp/hello-world');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('androidDevice.fill', async function({ androidDevice }) {
|
test('androidDevice.fill', async function({ androidDevice }) {
|
||||||
|
|
|
||||||
114
tests/android/launch-server.spec.ts
Normal file
114
tests/android/launch-server.spec.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2020 Microsoft Corporation. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import ws from 'ws';
|
||||||
|
import { androidTest as test, expect } from './androidTest';
|
||||||
|
|
||||||
|
test('android.launchServer should connect to a device', async ({ playwright }) => {
|
||||||
|
const browserServer = await playwright._android.launchServer();
|
||||||
|
const device = await playwright._android.connect(browserServer.wsEndpoint());
|
||||||
|
const output = await device.shell('echo 123');
|
||||||
|
expect(output.toString()).toBe('123\n');
|
||||||
|
await device.close();
|
||||||
|
await browserServer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('android.launchServer should be abe to reconnect to a device', async ({ playwright }) => {
|
||||||
|
const browserServer = await playwright._android.launchServer();
|
||||||
|
try {
|
||||||
|
{
|
||||||
|
const device = await playwright._android.connect(browserServer.wsEndpoint());
|
||||||
|
await device.push(Buffer.from('hello world'), '/data/local/tmp/hello-world');
|
||||||
|
await device.close();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const device = await playwright._android.connect(browserServer.wsEndpoint());
|
||||||
|
const data = await device.shell('cat /data/local/tmp/hello-world');
|
||||||
|
expect(data).toEqual(Buffer.from('hello world'));
|
||||||
|
await device.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Cleanup
|
||||||
|
const device = await playwright._android.connect(browserServer.wsEndpoint());
|
||||||
|
await device.shell('rm /data/local/tmp/hello-world');
|
||||||
|
await device.close();
|
||||||
|
await browserServer.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('android.launchServer should throw if there is no device with a specified serial number', async ({ playwright }) => {
|
||||||
|
await expect(playwright._android.launchServer({
|
||||||
|
deviceSerialNumber: 'does-not-exist',
|
||||||
|
})).rejects.toThrow(`No device with serial number 'does-not-exist'`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('android.launchServer should not allow multiple connections', async ({ playwright }) => {
|
||||||
|
const browserServer = await playwright._android.launchServer();
|
||||||
|
try {
|
||||||
|
await playwright._android.connect(browserServer.wsEndpoint());
|
||||||
|
await expect(playwright._android.connect(browserServer.wsEndpoint(), { timeout: 2_000 })).rejects.toThrow('android.connect: Timeout 2000ms exceeded');
|
||||||
|
} finally {
|
||||||
|
await browserServer.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('android.launchServer BrowserServer.close() will disconnect the device', async ({ playwright }) => {
|
||||||
|
const browserServer = await playwright._android.launchServer();
|
||||||
|
try {
|
||||||
|
const device = await playwright._android.connect(browserServer.wsEndpoint());
|
||||||
|
await browserServer.close();
|
||||||
|
await expect(device.shell('echo 123')).rejects.toThrow('androidDevice.shell: Browser has been closed');
|
||||||
|
} finally {
|
||||||
|
await browserServer.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('android.launchServer BrowserServer.kill() will disconnect the device', async ({ playwright }) => {
|
||||||
|
const browserServer = await playwright._android.launchServer();
|
||||||
|
try {
|
||||||
|
const device = await playwright._android.connect(browserServer.wsEndpoint());
|
||||||
|
await browserServer.kill();
|
||||||
|
await expect(device.shell('echo 123')).rejects.toThrow('androidDevice.shell: Browser has been closed');
|
||||||
|
} finally {
|
||||||
|
await browserServer.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('android.launchServer should terminate WS connection when device gets disconnected', async ({ playwright }) => {
|
||||||
|
const browserServer = await playwright._android.launchServer();
|
||||||
|
const forwardingServer = new ws.Server({ port: 0, path: '/connect' });
|
||||||
|
let receivedConnection: ws.WebSocket;
|
||||||
|
forwardingServer.on('connection', connection => {
|
||||||
|
receivedConnection = connection;
|
||||||
|
const actualConnection = new ws.WebSocket(browserServer.wsEndpoint());
|
||||||
|
actualConnection.on('message', message => connection.send(message));
|
||||||
|
connection.on('message', message => actualConnection.send(message));
|
||||||
|
connection.on('close', () => actualConnection.close());
|
||||||
|
actualConnection.on('close', () => connection.close());
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const device = await playwright._android.connect(`ws://localhost:${(forwardingServer.address() as ws.AddressInfo).port}/connect`);
|
||||||
|
expect((await device.shell('echo 123')).toString()).toBe('123\n');
|
||||||
|
expect(receivedConnection.readyState).toBe(ws.OPEN);
|
||||||
|
const waitToClose = new Promise(f => receivedConnection.on('close', f));
|
||||||
|
await device.close();
|
||||||
|
await waitToClose;
|
||||||
|
expect(receivedConnection.readyState).toBe(ws.CLOSED);
|
||||||
|
} finally {
|
||||||
|
await browserServer.close();
|
||||||
|
await new Promise(f => forwardingServer.close(f));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -13,7 +13,7 @@ bash $PWD/utils/avd_stop.sh
|
||||||
echo "Starting emulator"
|
echo "Starting emulator"
|
||||||
# On normal macOS GitHub Action runners, the host GPU is not available. So 'swiftshader_indirect' would have to be used.
|
# On normal macOS GitHub Action runners, the host GPU is not available. So 'swiftshader_indirect' would have to be used.
|
||||||
# Since we (Playwright) run our tests on a selfhosted mac, the host GPU is available, so we use it.
|
# Since we (Playwright) run our tests on a selfhosted mac, the host GPU is available, so we use it.
|
||||||
nohup ${ANDROID_HOME}/emulator/emulator -avd android33 -no-audio -no-window -gpu host -no-boot-anim &
|
nohup ${ANDROID_HOME}/emulator/emulator -avd android33 -no-audio -no-window -gpu host -no-boot-anim -no-snapshot &
|
||||||
${ANDROID_HOME}/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82'
|
${ANDROID_HOME}/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed | tr -d '\r') ]]; do sleep 1; done; input keyevent 82'
|
||||||
${ANDROID_HOME}/platform-tools/adb devices
|
${ANDROID_HOME}/platform-tools/adb devices
|
||||||
echo "Emulator started"
|
echo "Emulator started"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue