feat(adb): support webviews (#4657)
This commit is contained in:
parent
f939fdc1a1
commit
8fc49c98fa
77
android-types-internal.d.ts
vendored
77
android-types-internal.d.ts
vendored
|
|
@ -14,6 +14,52 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export interface AndroidDevice<BrowserContextOptions, BrowserContext, Page> extends EventEmitter {
|
||||
input: AndroidInput;
|
||||
|
||||
setDefaultTimeout(timeout: number): void;
|
||||
on(event: 'webview', handler: (webView: AndroidWebView<Page>) => void): this;
|
||||
waitForEvent(event: string, predicate?: (data: any) => boolean): Promise<any>;
|
||||
|
||||
serial(): string;
|
||||
model(): string;
|
||||
webViews(): AndroidWebView<Page>[];
|
||||
shell(command: string): Promise<string>;
|
||||
launchBrowser(options?: BrowserContextOptions & { packageName?: string }): Promise<BrowserContext>;
|
||||
close(): Promise<void>;
|
||||
|
||||
wait(selector: AndroidSelector, options?: { state?: 'gone' } & { timeout?: number }): Promise<void>;
|
||||
fill(selector: AndroidSelector, text: string, options?: { timeout?: number }): Promise<void>;
|
||||
press(selector: AndroidSelector, key: AndroidKey, options?: { duration?: number } & { timeout?: number }): Promise<void>;
|
||||
tap(selector: AndroidSelector, options?: { duration?: number } & { timeout?: number }): Promise<void>;
|
||||
drag(selector: AndroidSelector, dest: { x: number, y: number }, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
fling(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
longTap(selector: AndroidSelector, options?: { timeout?: number }): Promise<void>;
|
||||
pinchClose(selector: AndroidSelector, percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
pinchOpen(selector: AndroidSelector, percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
scroll(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
swipe(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
|
||||
info(selector: AndroidSelector): Promise<AndroidElementInfo>;
|
||||
}
|
||||
|
||||
export interface AndroidInput {
|
||||
type(text: string): Promise<void>;
|
||||
press(key: AndroidKey): Promise<void>;
|
||||
tap(point: { x: number, y: number }): Promise<void>;
|
||||
swipe(from: { x: number, y: number }, segments: { x: number, y: number }[], steps: number): Promise<void>;
|
||||
drag(from: { x: number, y: number }, to: { x: number, y: number }, steps: number): Promise<void>;
|
||||
}
|
||||
|
||||
export interface AndroidWebView<Page> extends EventEmitter {
|
||||
on(event: 'close', handler: () => void): this;
|
||||
pid(): number;
|
||||
pkg(): string;
|
||||
page(): Promise<Page>;
|
||||
}
|
||||
|
||||
export type AndroidElementInfo = {
|
||||
clazz: string;
|
||||
desc: string;
|
||||
|
|
@ -52,37 +98,6 @@ export type AndroidSelector = {
|
|||
text?: string | RegExp,
|
||||
};
|
||||
|
||||
export interface AndroidDevice<BrowserContextOptions, BrowserContext> {
|
||||
input: AndroidInput;
|
||||
|
||||
serial(): string;
|
||||
model(): string;
|
||||
shell(command: string): Promise<string>;
|
||||
launchBrowser(options?: BrowserContextOptions & { packageName?: string }): Promise<BrowserContext>;
|
||||
close(): Promise<void>;
|
||||
|
||||
wait(selector: AndroidSelector, options?: { state?: 'gone' } & { timeout?: number }): Promise<void>;
|
||||
fill(selector: AndroidSelector, text: string, options?: { timeout?: number }): Promise<void>;
|
||||
tap(selector: AndroidSelector, options?: { duration?: number } & { timeout?: number }): Promise<void>;
|
||||
drag(selector: AndroidSelector, dest: { x: number, y: number }, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
fling(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
longTap(selector: AndroidSelector, options?: { timeout?: number }): Promise<void>;
|
||||
pinchClose(selector: AndroidSelector, percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
pinchOpen(selector: AndroidSelector, percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
scroll(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
swipe(selector: AndroidSelector, direction: 'down' | 'up' | 'left' | 'right', percent: number, options?: { speed?: number } & { timeout?: number }): Promise<void>;
|
||||
|
||||
info(selector: AndroidSelector): Promise<AndroidElementInfo>;
|
||||
}
|
||||
|
||||
export interface AndroidInput {
|
||||
type(text: string): Promise<void>;
|
||||
press(key: AndroidKey): Promise<void>;
|
||||
tap(point: { x: number, y: number }): Promise<void>;
|
||||
swipe(from: { x: number, y: number }, segments: { x: number, y: number }[], steps: number): Promise<void>;
|
||||
drag(from: { x: number, y: number }, to: { x: number, y: number }, steps: number): Promise<void>;
|
||||
}
|
||||
|
||||
export type AndroidKey =
|
||||
'Unknown' |
|
||||
'SoftLeft' | 'SoftRight' |
|
||||
|
|
|
|||
13
android-types.d.ts
vendored
13
android-types.d.ts
vendored
|
|
@ -14,8 +14,15 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BrowserContext, BrowserContextOptions } from './types/types';
|
||||
import { Page, BrowserContext, BrowserContextOptions } from './types/types';
|
||||
import * as apiInternal from './android-types-internal';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export * from './android-types-internal';
|
||||
export type AndroidDevice = apiInternal.AndroidDevice<BrowserContext, BrowserContextOptions>;
|
||||
export { AndroidElementInfo, AndroidSelector } from './android-types-internal';
|
||||
export type AndroidDevice = apiInternal.AndroidDevice<BrowserContextOptions, BrowserContext, Page>;
|
||||
export type AndroidWebView = apiInternal.AndroidWebView<Page>;
|
||||
|
||||
export interface Android extends EventEmitter {
|
||||
setDefaultTimeout(timeout: number): void;
|
||||
devices(): Promise<AndroidDevice[]>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const cpAsync = util.promisify(ncp);
|
|||
const SCRIPT_NAME = path.basename(__filename);
|
||||
const ROOT_PATH = path.join(__dirname, '..');
|
||||
|
||||
const PLAYWRIGHT_CORE_FILES = ['bin', 'lib', 'types', 'NOTICE', 'LICENSE'];
|
||||
const PLAYWRIGHT_CORE_FILES = ['bin/PrintDeps.exe', 'lib', 'types', 'NOTICE', 'LICENSE'];
|
||||
const FFMPEG_FILES = ['third_party/ffmpeg'];
|
||||
|
||||
const PACKAGES = {
|
||||
|
|
@ -65,10 +65,10 @@ const PACKAGES = {
|
|||
files: [...PLAYWRIGHT_CORE_FILES, ...FFMPEG_FILES, 'electron-types.d.ts'],
|
||||
},
|
||||
'playwright-android': {
|
||||
version: '0.0.2', // Manually manage playwright-android version.
|
||||
version: '0.0.7', // Manually manage playwright-android version.
|
||||
description: 'A high-level API to automate Chrome for Android',
|
||||
browsers: [],
|
||||
files: [...PLAYWRIGHT_CORE_FILES, ...FFMPEG_FILES, 'android-types.d.ts', 'bin/android-driver.apk', 'bin/android-driver-target.apk'],
|
||||
files: [...PLAYWRIGHT_CORE_FILES, ...FFMPEG_FILES, 'android-types.d.ts', 'android-types-internal.d.ts', 'bin/android-driver.apk', 'bin/android-driver-target.apk'],
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ lib/server/injected/
|
|||
# Include generated types and entrypoint.
|
||||
!types/*
|
||||
!index.d.ts
|
||||
# Include separate android types.
|
||||
!android-types.d.ts
|
||||
!android-types-internal.d.ts
|
||||
# Include separate electron types.
|
||||
!electron-types.d.ts
|
||||
# Include main entrypoint.
|
||||
|
|
|
|||
|
|
@ -15,28 +15,32 @@ const { android } = require('playwright-android');
|
|||
|
||||
(async () => {
|
||||
const [device] = await android.devices();
|
||||
|
||||
// Android automation.
|
||||
console.log(`Model: ${device.model()}`);
|
||||
console.log(`Serial: ${device.serial()}`);
|
||||
|
||||
await device.tap({ desc: 'Home' });
|
||||
console.log(await device.info({ text: 'Chrome' }));
|
||||
await device.tap({ text: 'Chrome' });
|
||||
await device.fill({ res: 'com.android.chrome:id/url_bar' }, 'www.chromium.org');
|
||||
await device.input.press('Enter');
|
||||
await new Promise(f => setTimeout(f, 1000));
|
||||
await device.shell('am force-stop org.chromium.webview_shell');
|
||||
await device.shell('am start org.chromium.webview_shell/.WebViewBrowserActivity');
|
||||
|
||||
await device.tap({ res: 'com.android.chrome:id/tab_switcher_button' });
|
||||
await device.tap({ desc: 'More options' });
|
||||
await device.tap({ desc: 'Close all tabs' });
|
||||
await device.fill({ res: 'org.chromium.webview_shell:id/url_field' }, 'github.com/microsoft/playwright');
|
||||
|
||||
// Browser automation.
|
||||
const context = await device.launchBrowser();
|
||||
const [page] = context.pages();
|
||||
await page.goto('https://webkit.org/');
|
||||
console.log(await page.evaluate(() => window.location.href));
|
||||
await context.close();
|
||||
let [webview] = device.webViews();
|
||||
if (!webview)
|
||||
webview = await device.waitForEvent('webview');
|
||||
|
||||
const page = await webview.page();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
device.press({ res: 'org.chromium.webview_shell:id/url_field' }, 'Enter')
|
||||
]);
|
||||
console.log(await page.title());
|
||||
|
||||
{
|
||||
const context = await device.launchBrowser();
|
||||
const [page] = context.pages();
|
||||
await page.goto('https://webkit.org/');
|
||||
console.log(await page.evaluate(() => window.location.href));
|
||||
await context.close();
|
||||
}
|
||||
|
||||
await device.close();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -15,21 +15,34 @@
|
|||
*/
|
||||
|
||||
import * as channels from '../protocol/channels';
|
||||
import { Events } from './events';
|
||||
import { BrowserContext, validateBrowserContextOptions } from './browserContext';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import * as apiInternal from '../../android-types-internal';
|
||||
import * as types from './types';
|
||||
import { Page } from './page';
|
||||
import { TimeoutSettings } from '../utils/timeoutSettings';
|
||||
import { Waiter } from './waiter';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
type Direction = 'down' | 'up' | 'left' | 'right';
|
||||
type SpeedOptions = { speed?: number };
|
||||
|
||||
export class Android extends ChannelOwner<channels.AndroidChannel, channels.AndroidInitializer> {
|
||||
readonly _timeoutSettings: TimeoutSettings;
|
||||
|
||||
static from(android: channels.AndroidChannel): Android {
|
||||
return (android as any)._object;
|
||||
}
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.AndroidInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._timeoutSettings = new TimeoutSettings();
|
||||
}
|
||||
|
||||
setDefaultTimeout(timeout: number) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
this._channel.setDefaultTimeoutNoReply({ timeout });
|
||||
}
|
||||
|
||||
async devices(): Promise<AndroidDevice[]> {
|
||||
|
|
@ -41,6 +54,9 @@ export class Android extends ChannelOwner<channels.AndroidChannel, channels.Andr
|
|||
}
|
||||
|
||||
export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, channels.AndroidDeviceInitializer> {
|
||||
readonly _timeoutSettings: TimeoutSettings;
|
||||
private _webViews = new Map<number, AndroidWebView>();
|
||||
|
||||
static from(androidDevice: channels.AndroidDeviceChannel): AndroidDevice {
|
||||
return (androidDevice as any)._object;
|
||||
}
|
||||
|
|
@ -50,6 +66,27 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, c
|
|||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.AndroidDeviceInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this.input = new Input(this);
|
||||
this._timeoutSettings = new TimeoutSettings((parent as Android)._timeoutSettings);
|
||||
this._channel.on('webViewAdded', ({ webView }) => this._onWebViewAdded(webView));
|
||||
this._channel.on('webViewRemoved', ({ pid }) => this._onWebViewRemoved(pid));
|
||||
}
|
||||
|
||||
private _onWebViewAdded(webView: channels.AndroidWebView) {
|
||||
const view = new AndroidWebView(this, webView);
|
||||
this._webViews.set(webView.pid, view);
|
||||
this.emit(Events.AndroidDevice.WebView, view);
|
||||
}
|
||||
|
||||
private _onWebViewRemoved(pid: number) {
|
||||
const view = this._webViews.get(pid);
|
||||
this._webViews.delete(pid);
|
||||
if (view)
|
||||
view.emit(Events.AndroidWebView.Close);
|
||||
}
|
||||
|
||||
setDefaultTimeout(timeout: number) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
this._channel.setDefaultTimeoutNoReply({ timeout });
|
||||
}
|
||||
|
||||
serial(): string {
|
||||
|
|
@ -60,6 +97,10 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, c
|
|||
return this._initializer.model;
|
||||
}
|
||||
|
||||
webViews(): AndroidWebView[] {
|
||||
return [...this._webViews.values()];
|
||||
}
|
||||
|
||||
async wait(selector: apiInternal.AndroidSelector, options?: { state?: 'gone' } & types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.wait', async () => {
|
||||
await this._channel.wait({ selector: toSelectorChannel(selector), ...options });
|
||||
|
|
@ -72,6 +113,11 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, c
|
|||
});
|
||||
}
|
||||
|
||||
async press(selector: apiInternal.AndroidSelector, key: apiInternal.AndroidKey, options?: types.TimeoutOptions) {
|
||||
await this.tap(selector, options);
|
||||
await this.input.press(key);
|
||||
}
|
||||
|
||||
async tap(selector: apiInternal.AndroidSelector, options?: { duration?: number } & types.TimeoutOptions) {
|
||||
await this._wrapApiCall('androidDevice.tap', async () => {
|
||||
await this._channel.tap({ selector: toSelectorChannel(selector), ...options });
|
||||
|
|
@ -129,6 +175,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, c
|
|||
async close() {
|
||||
return this._wrapApiCall('androidDevice.close', async () => {
|
||||
await this._channel.close();
|
||||
this.emit(Events.AndroidDevice.Close);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -146,6 +193,18 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel, c
|
|||
return BrowserContext.from(context);
|
||||
});
|
||||
}
|
||||
|
||||
async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise<any> {
|
||||
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||
const waiter = new Waiter();
|
||||
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
||||
if (event !== Events.AndroidDevice.Close)
|
||||
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, new Error('Device closed'));
|
||||
const result = await waiter.waitForEvent(this, event, predicate as any);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class Input implements apiInternal.AndroidInput {
|
||||
|
|
@ -235,3 +294,36 @@ function toSelectorChannel(selector: apiInternal.AndroidSelector): channels.Andr
|
|||
selected,
|
||||
};
|
||||
}
|
||||
|
||||
export class AndroidWebView extends EventEmitter {
|
||||
private _device: AndroidDevice;
|
||||
private _data: channels.AndroidWebView;
|
||||
private _pagePromise: Promise<Page> | undefined;
|
||||
|
||||
constructor(device: AndroidDevice, data: channels.AndroidWebView) {
|
||||
super();
|
||||
this._device = device;
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
pid(): number {
|
||||
return this._data.pid;
|
||||
}
|
||||
|
||||
pkg(): string {
|
||||
return this._data.pkg;
|
||||
}
|
||||
|
||||
async page(): Promise<Page> {
|
||||
if (!this._pagePromise)
|
||||
this._pagePromise = this._fetchPage();
|
||||
return this._pagePromise;
|
||||
}
|
||||
|
||||
private async _fetchPage(): Promise<Page> {
|
||||
return this._device._wrapApiCall('androidWebView.page', async () => {
|
||||
const { context } = await this._device._channel.connectToWebView({ pid: this._data.pid });
|
||||
return BrowserContext.from(context).pages()[0];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@
|
|||
*/
|
||||
|
||||
export const Events = {
|
||||
AndroidDevice: {
|
||||
WebView: 'webview',
|
||||
Close: 'close'
|
||||
},
|
||||
|
||||
AndroidWebView: {
|
||||
Close: 'close'
|
||||
},
|
||||
|
||||
Browser: {
|
||||
Disconnected: 'disconnected'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import * as channels from '../protocol/channels';
|
|||
import { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||
|
||||
export class AndroidDispatcher extends Dispatcher<Android, channels.AndroidInitializer> implements channels.AndroidChannel {
|
||||
constructor(scope: DispatcherScope, electron: Android) {
|
||||
super(scope, electron, 'Android', {}, true);
|
||||
constructor(scope: DispatcherScope, android: Android) {
|
||||
super(scope, android, 'Android', {}, true);
|
||||
}
|
||||
|
||||
async devices(params: channels.AndroidDevicesParams): Promise<channels.AndroidDevicesResult> {
|
||||
|
|
@ -30,14 +30,22 @@ export class AndroidDispatcher extends Dispatcher<Android, channels.AndroidIniti
|
|||
devices: devices.map(d => new AndroidDeviceDispatcher(this._scope, d))
|
||||
};
|
||||
}
|
||||
|
||||
async setDefaultTimeoutNoReply(params: channels.AndroidSetDefaultTimeoutNoReplyParams) {
|
||||
this._object.setDefaultTimeout(params.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
export class AndroidDeviceDispatcher extends Dispatcher<AndroidDevice, channels.AndroidDeviceInitializer> implements channels.AndroidDeviceChannel {
|
||||
constructor(scope: DispatcherScope, device: AndroidDevice) {
|
||||
super(scope, device, 'AndroidDevice', {
|
||||
model: device.model,
|
||||
serial: device.serial
|
||||
serial: device.serial,
|
||||
}, true);
|
||||
for (const webView of device.webViews())
|
||||
this._dispatchEvent('webViewAdded', { webView });
|
||||
device.on(AndroidDevice.Events.WebViewAdded, webView => this._dispatchEvent('webViewAdded', { webView }));
|
||||
device.on(AndroidDevice.Events.WebViewRemoved, pid => this._dispatchEvent('webViewRemoved', { pid }));
|
||||
}
|
||||
|
||||
async wait(params: channels.AndroidDeviceWaitParams) {
|
||||
|
|
@ -129,6 +137,14 @@ export class AndroidDeviceDispatcher extends Dispatcher<AndroidDevice, channels.
|
|||
async close(params: channels.AndroidDeviceCloseParams) {
|
||||
await this._object.close();
|
||||
}
|
||||
|
||||
async setDefaultTimeoutNoReply(params: channels.AndroidDeviceSetDefaultTimeoutNoReplyParams) {
|
||||
this._object.setDefaultTimeout(params.timeout);
|
||||
}
|
||||
|
||||
async connectToWebView(params: channels.AndroidDeviceConnectToWebViewParams): Promise<channels.AndroidDeviceConnectToWebViewResult> {
|
||||
return { context: new BrowserContextDispatcher(this._scope, await this._object.connectToWebView(params.pid)) };
|
||||
}
|
||||
}
|
||||
|
||||
const keyMap = new Map<string, number>([
|
||||
|
|
|
|||
|
|
@ -2406,55 +2406,20 @@ export type ElectronApplicationCloseResult = void;
|
|||
export type AndroidInitializer = {};
|
||||
export interface AndroidChannel extends Channel {
|
||||
devices(params?: AndroidDevicesParams, metadata?: Metadata): Promise<AndroidDevicesResult>;
|
||||
setDefaultTimeoutNoReply(params: AndroidSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<AndroidSetDefaultTimeoutNoReplyResult>;
|
||||
}
|
||||
export type AndroidDevicesParams = {};
|
||||
export type AndroidDevicesOptions = {};
|
||||
export type AndroidDevicesResult = {
|
||||
devices: AndroidDeviceChannel[],
|
||||
};
|
||||
|
||||
export type AndroidSelector = {
|
||||
checkable?: boolean,
|
||||
checked?: boolean,
|
||||
clazz?: string,
|
||||
clickable?: boolean,
|
||||
depth?: number,
|
||||
desc?: string,
|
||||
enabled?: boolean,
|
||||
focusable?: boolean,
|
||||
focused?: boolean,
|
||||
hasChild?: {
|
||||
selector: AndroidSelector,
|
||||
},
|
||||
hasDescendant?: {
|
||||
selector: AndroidSelector,
|
||||
maxDepth?: number,
|
||||
},
|
||||
longClickable?: boolean,
|
||||
pkg?: string,
|
||||
res?: string,
|
||||
scrollable?: boolean,
|
||||
selected?: boolean,
|
||||
text?: string,
|
||||
export type AndroidSetDefaultTimeoutNoReplyParams = {
|
||||
timeout: number,
|
||||
};
|
||||
export type AndroidSetDefaultTimeoutNoReplyOptions = {
|
||||
|
||||
export type AndroidElementInfo = {
|
||||
clazz: string,
|
||||
desc: string,
|
||||
res: string,
|
||||
pkg: string,
|
||||
text: string,
|
||||
bounds: Rect,
|
||||
checkable: boolean,
|
||||
checked: boolean,
|
||||
clickable: boolean,
|
||||
enabled: boolean,
|
||||
focusable: boolean,
|
||||
focused: boolean,
|
||||
longClickable: boolean,
|
||||
scrollable: boolean,
|
||||
selected: boolean,
|
||||
};
|
||||
export type AndroidSetDefaultTimeoutNoReplyResult = void;
|
||||
|
||||
// ----------- AndroidDevice -----------
|
||||
export type AndroidDeviceInitializer = {
|
||||
|
|
@ -2462,6 +2427,8 @@ export type AndroidDeviceInitializer = {
|
|||
serial: string,
|
||||
};
|
||||
export interface AndroidDeviceChannel extends Channel {
|
||||
on(event: 'webViewAdded', callback: (params: AndroidDeviceWebViewAddedEvent) => void): this;
|
||||
on(event: 'webViewRemoved', callback: (params: AndroidDeviceWebViewRemovedEvent) => void): this;
|
||||
wait(params: AndroidDeviceWaitParams, metadata?: Metadata): Promise<AndroidDeviceWaitResult>;
|
||||
fill(params: AndroidDeviceFillParams, metadata?: Metadata): Promise<AndroidDeviceFillResult>;
|
||||
tap(params: AndroidDeviceTapParams, metadata?: Metadata): Promise<AndroidDeviceTapResult>;
|
||||
|
|
@ -2480,8 +2447,16 @@ export interface AndroidDeviceChannel extends Channel {
|
|||
inputDrag(params: AndroidDeviceInputDragParams, metadata?: Metadata): Promise<AndroidDeviceInputDragResult>;
|
||||
launchBrowser(params: AndroidDeviceLaunchBrowserParams, metadata?: Metadata): Promise<AndroidDeviceLaunchBrowserResult>;
|
||||
shell(params: AndroidDeviceShellParams, metadata?: Metadata): Promise<AndroidDeviceShellResult>;
|
||||
setDefaultTimeoutNoReply(params: AndroidDeviceSetDefaultTimeoutNoReplyParams, metadata?: Metadata): Promise<AndroidDeviceSetDefaultTimeoutNoReplyResult>;
|
||||
connectToWebView(params: AndroidDeviceConnectToWebViewParams, metadata?: Metadata): Promise<AndroidDeviceConnectToWebViewResult>;
|
||||
close(params?: AndroidDeviceCloseParams, metadata?: Metadata): Promise<AndroidDeviceCloseResult>;
|
||||
}
|
||||
export type AndroidDeviceWebViewAddedEvent = {
|
||||
webView: AndroidWebView,
|
||||
};
|
||||
export type AndroidDeviceWebViewRemovedEvent = {
|
||||
pid: number,
|
||||
};
|
||||
export type AndroidDeviceWaitParams = {
|
||||
selector: AndroidSelector,
|
||||
state?: 'gone',
|
||||
|
|
@ -2736,6 +2711,70 @@ export type AndroidDeviceShellOptions = {
|
|||
export type AndroidDeviceShellResult = {
|
||||
result: string,
|
||||
};
|
||||
export type AndroidDeviceSetDefaultTimeoutNoReplyParams = {
|
||||
timeout: number,
|
||||
};
|
||||
export type AndroidDeviceSetDefaultTimeoutNoReplyOptions = {
|
||||
|
||||
};
|
||||
export type AndroidDeviceSetDefaultTimeoutNoReplyResult = void;
|
||||
export type AndroidDeviceConnectToWebViewParams = {
|
||||
pid: number,
|
||||
};
|
||||
export type AndroidDeviceConnectToWebViewOptions = {
|
||||
|
||||
};
|
||||
export type AndroidDeviceConnectToWebViewResult = {
|
||||
context: BrowserContextChannel,
|
||||
};
|
||||
export type AndroidDeviceCloseParams = {};
|
||||
export type AndroidDeviceCloseOptions = {};
|
||||
export type AndroidDeviceCloseResult = void;
|
||||
|
||||
export type AndroidWebView = {
|
||||
pid: number,
|
||||
pkg: string,
|
||||
};
|
||||
|
||||
export type AndroidSelector = {
|
||||
checkable?: boolean,
|
||||
checked?: boolean,
|
||||
clazz?: string,
|
||||
clickable?: boolean,
|
||||
depth?: number,
|
||||
desc?: string,
|
||||
enabled?: boolean,
|
||||
focusable?: boolean,
|
||||
focused?: boolean,
|
||||
hasChild?: {
|
||||
selector: AndroidSelector,
|
||||
},
|
||||
hasDescendant?: {
|
||||
selector: AndroidSelector,
|
||||
maxDepth?: number,
|
||||
},
|
||||
longClickable?: boolean,
|
||||
pkg?: string,
|
||||
res?: string,
|
||||
scrollable?: boolean,
|
||||
selected?: boolean,
|
||||
text?: string,
|
||||
};
|
||||
|
||||
export type AndroidElementInfo = {
|
||||
clazz: string,
|
||||
desc: string,
|
||||
res: string,
|
||||
pkg: string,
|
||||
text: string,
|
||||
bounds: Rect,
|
||||
checkable: boolean,
|
||||
checked: boolean,
|
||||
clickable: boolean,
|
||||
enabled: boolean,
|
||||
focusable: boolean,
|
||||
focused: boolean,
|
||||
longClickable: boolean,
|
||||
scrollable: boolean,
|
||||
selected: boolean,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2079,54 +2079,9 @@ Android:
|
|||
type: array
|
||||
items: AndroidDevice
|
||||
|
||||
|
||||
AndroidSelector:
|
||||
type: object
|
||||
properties:
|
||||
checkable: boolean?
|
||||
checked: boolean?
|
||||
clazz: string?
|
||||
clickable: boolean?
|
||||
depth: number?
|
||||
desc: string?
|
||||
enabled: boolean?
|
||||
focusable: boolean?
|
||||
focused: boolean?
|
||||
hasChild:
|
||||
type: object?
|
||||
properties:
|
||||
selector: AndroidSelector
|
||||
hasDescendant:
|
||||
type: object?
|
||||
properties:
|
||||
selector: AndroidSelector
|
||||
maxDepth: number?
|
||||
longClickable: boolean?
|
||||
pkg: string?
|
||||
res: string?
|
||||
scrollable: boolean?
|
||||
selected: boolean?
|
||||
text: string?
|
||||
|
||||
|
||||
AndroidElementInfo:
|
||||
type: object
|
||||
properties:
|
||||
clazz: string
|
||||
desc: string
|
||||
res: string
|
||||
pkg: string
|
||||
text: string
|
||||
bounds: Rect
|
||||
checkable: boolean
|
||||
checked: boolean
|
||||
clickable: boolean
|
||||
enabled: boolean
|
||||
focusable: boolean
|
||||
focused: boolean
|
||||
longClickable: boolean
|
||||
scrollable: boolean
|
||||
selected: boolean
|
||||
setDefaultTimeoutNoReply:
|
||||
parameters:
|
||||
timeout: number
|
||||
|
||||
|
||||
AndroidDevice:
|
||||
|
|
@ -2326,4 +2281,79 @@ AndroidDevice:
|
|||
returns:
|
||||
result: string
|
||||
|
||||
setDefaultTimeoutNoReply:
|
||||
parameters:
|
||||
timeout: number
|
||||
|
||||
connectToWebView:
|
||||
parameters:
|
||||
pid: number
|
||||
returns:
|
||||
context: BrowserContext
|
||||
|
||||
close:
|
||||
|
||||
events:
|
||||
webViewAdded:
|
||||
parameters:
|
||||
webView: AndroidWebView
|
||||
|
||||
webViewRemoved:
|
||||
parameters:
|
||||
pid: number
|
||||
|
||||
|
||||
AndroidWebView:
|
||||
type: object
|
||||
properties:
|
||||
pid: number
|
||||
pkg: string
|
||||
|
||||
|
||||
AndroidSelector:
|
||||
type: object
|
||||
properties:
|
||||
checkable: boolean?
|
||||
checked: boolean?
|
||||
clazz: string?
|
||||
clickable: boolean?
|
||||
depth: number?
|
||||
desc: string?
|
||||
enabled: boolean?
|
||||
focusable: boolean?
|
||||
focused: boolean?
|
||||
hasChild:
|
||||
type: object?
|
||||
properties:
|
||||
selector: AndroidSelector
|
||||
hasDescendant:
|
||||
type: object?
|
||||
properties:
|
||||
selector: AndroidSelector
|
||||
maxDepth: number?
|
||||
longClickable: boolean?
|
||||
pkg: string?
|
||||
res: string?
|
||||
scrollable: boolean?
|
||||
selected: boolean?
|
||||
text: string?
|
||||
|
||||
|
||||
AndroidElementInfo:
|
||||
type: object
|
||||
properties:
|
||||
clazz: string
|
||||
desc: string
|
||||
res: string
|
||||
pkg: string
|
||||
text: string
|
||||
bounds: Rect
|
||||
checkable: boolean
|
||||
checked: boolean
|
||||
clickable: boolean
|
||||
enabled: boolean
|
||||
focusable: boolean
|
||||
focused: boolean
|
||||
longClickable: boolean
|
||||
scrollable: boolean
|
||||
selected: boolean
|
||||
|
|
|
|||
|
|
@ -898,46 +898,8 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
});
|
||||
scheme.ElectronApplicationCloseParams = tOptional(tObject({}));
|
||||
scheme.AndroidDevicesParams = tOptional(tObject({}));
|
||||
scheme.AndroidSelector = tObject({
|
||||
checkable: tOptional(tBoolean),
|
||||
checked: tOptional(tBoolean),
|
||||
clazz: tOptional(tString),
|
||||
clickable: tOptional(tBoolean),
|
||||
depth: tOptional(tNumber),
|
||||
desc: tOptional(tString),
|
||||
enabled: tOptional(tBoolean),
|
||||
focusable: tOptional(tBoolean),
|
||||
focused: tOptional(tBoolean),
|
||||
hasChild: tOptional(tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
})),
|
||||
hasDescendant: tOptional(tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
maxDepth: tOptional(tNumber),
|
||||
})),
|
||||
longClickable: tOptional(tBoolean),
|
||||
pkg: tOptional(tString),
|
||||
res: tOptional(tString),
|
||||
scrollable: tOptional(tBoolean),
|
||||
selected: tOptional(tBoolean),
|
||||
text: tOptional(tString),
|
||||
});
|
||||
scheme.AndroidElementInfo = tObject({
|
||||
clazz: tString,
|
||||
desc: tString,
|
||||
res: tString,
|
||||
pkg: tString,
|
||||
text: tString,
|
||||
bounds: tType('Rect'),
|
||||
checkable: tBoolean,
|
||||
checked: tBoolean,
|
||||
clickable: tBoolean,
|
||||
enabled: tBoolean,
|
||||
focusable: tBoolean,
|
||||
focused: tBoolean,
|
||||
longClickable: tBoolean,
|
||||
scrollable: tBoolean,
|
||||
selected: tBoolean,
|
||||
scheme.AndroidSetDefaultTimeoutNoReplyParams = tObject({
|
||||
timeout: tNumber,
|
||||
});
|
||||
scheme.AndroidDeviceWaitParams = tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
|
|
@ -1065,7 +1027,58 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
|||
scheme.AndroidDeviceShellParams = tObject({
|
||||
command: tString,
|
||||
});
|
||||
scheme.AndroidDeviceSetDefaultTimeoutNoReplyParams = tObject({
|
||||
timeout: tNumber,
|
||||
});
|
||||
scheme.AndroidDeviceConnectToWebViewParams = tObject({
|
||||
pid: tNumber,
|
||||
});
|
||||
scheme.AndroidDeviceCloseParams = tOptional(tObject({}));
|
||||
scheme.AndroidWebView = tObject({
|
||||
pid: tNumber,
|
||||
pkg: tString,
|
||||
});
|
||||
scheme.AndroidSelector = tObject({
|
||||
checkable: tOptional(tBoolean),
|
||||
checked: tOptional(tBoolean),
|
||||
clazz: tOptional(tString),
|
||||
clickable: tOptional(tBoolean),
|
||||
depth: tOptional(tNumber),
|
||||
desc: tOptional(tString),
|
||||
enabled: tOptional(tBoolean),
|
||||
focusable: tOptional(tBoolean),
|
||||
focused: tOptional(tBoolean),
|
||||
hasChild: tOptional(tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
})),
|
||||
hasDescendant: tOptional(tObject({
|
||||
selector: tType('AndroidSelector'),
|
||||
maxDepth: tOptional(tNumber),
|
||||
})),
|
||||
longClickable: tOptional(tBoolean),
|
||||
pkg: tOptional(tString),
|
||||
res: tOptional(tString),
|
||||
scrollable: tOptional(tBoolean),
|
||||
selected: tOptional(tBoolean),
|
||||
text: tOptional(tString),
|
||||
});
|
||||
scheme.AndroidElementInfo = tObject({
|
||||
clazz: tString,
|
||||
desc: tString,
|
||||
res: tString,
|
||||
pkg: tString,
|
||||
text: tString,
|
||||
bounds: tType('Rect'),
|
||||
checkable: tBoolean,
|
||||
checked: tBoolean,
|
||||
clickable: tBoolean,
|
||||
enabled: tBoolean,
|
||||
focusable: tBoolean,
|
||||
focused: tBoolean,
|
||||
longClickable: tBoolean,
|
||||
scrollable: tBoolean,
|
||||
selected: tBoolean,
|
||||
});
|
||||
|
||||
return scheme;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import { CRBrowser } from '../chromium/crBrowser';
|
|||
import { helper } from '../helper';
|
||||
import { Transport } from '../../protocol/transport';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { TimeoutSettings } from '../../utils/timeoutSettings';
|
||||
import { AndroidWebView } from '../../protocol/channels';
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
|
||||
|
|
@ -51,35 +53,67 @@ export interface SocketBackend extends EventEmitter {
|
|||
|
||||
export class Android {
|
||||
private _backend: Backend;
|
||||
readonly _timeoutSettings: TimeoutSettings;
|
||||
|
||||
constructor(backend: Backend) {
|
||||
this._backend = backend;
|
||||
this._timeoutSettings = new TimeoutSettings();
|
||||
}
|
||||
|
||||
setDefaultTimeout(timeout: number) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
|
||||
async devices(): Promise<AndroidDevice[]> {
|
||||
const devices = await this._backend.devices();
|
||||
return await Promise.all(devices.map(d => AndroidDevice.create(d)));
|
||||
return await Promise.all(devices.map(d => AndroidDevice.create(this, d)));
|
||||
}
|
||||
}
|
||||
|
||||
export class AndroidDevice {
|
||||
export class AndroidDevice extends EventEmitter {
|
||||
readonly _backend: DeviceBackend;
|
||||
readonly model: string;
|
||||
readonly serial: string;
|
||||
private _driverPromise: Promise<Transport> | undefined;
|
||||
private _lastId = 0;
|
||||
private _callbacks = new Map<number, { fulfill: (result: any) => void, reject: (error: Error) => void }>();
|
||||
private _pollingWebViews: NodeJS.Timeout | undefined;
|
||||
readonly _timeoutSettings: TimeoutSettings;
|
||||
private _webViews = new Map<number, AndroidWebView>();
|
||||
|
||||
constructor(backend: DeviceBackend, model: string) {
|
||||
static Events = {
|
||||
WebViewAdded: 'webViewAdded',
|
||||
WebViewRemoved: 'webViewRemoved',
|
||||
};
|
||||
|
||||
private _browserConnections = new Set<AndroidBrowser>();
|
||||
|
||||
constructor(android: Android, backend: DeviceBackend, model: string) {
|
||||
super();
|
||||
this._backend = backend;
|
||||
this.model = model;
|
||||
this.serial = backend.serial;
|
||||
this._timeoutSettings = new TimeoutSettings(android._timeoutSettings);
|
||||
}
|
||||
|
||||
static async create(backend: DeviceBackend): Promise<AndroidDevice> {
|
||||
static async create(android: Android, backend: DeviceBackend): Promise<AndroidDevice> {
|
||||
await backend.init();
|
||||
const model = await backend.runCommand('shell:getprop ro.product.model');
|
||||
return new AndroidDevice(backend, model);
|
||||
const device = new AndroidDevice(android, backend, model.trim());
|
||||
await device._init();
|
||||
return device;
|
||||
}
|
||||
|
||||
async _init() {
|
||||
await this._refreshWebViews();
|
||||
const poll = () => {
|
||||
this._pollingWebViews = setTimeout(() => this._refreshWebViews().then(poll).catch(() => {}), 500);
|
||||
};
|
||||
poll();
|
||||
}
|
||||
|
||||
setDefaultTimeout(timeout: number) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
|
||||
async shell(command: string): Promise<string> {
|
||||
|
|
@ -101,7 +135,9 @@ export class AndroidDevice {
|
|||
|
||||
debug('pw:android')('Installing the new driver');
|
||||
for (const file of ['android-driver.apk', 'android-driver-target.apk']) {
|
||||
debug('pw:android')('Reading ' + require.resolve(`../../../bin/${file}`));
|
||||
const driverFile = await readFileAsync(require.resolve(`../../../bin/${file}`));
|
||||
debug('pw:android')('Opening install socket');
|
||||
const installSocket = await this._backend.open(`shell:cmd package install -r -t -S ${driverFile.length}`);
|
||||
debug('pw:android')('Writing driver bytes: ' + driverFile.length);
|
||||
await installSocket.write(driverFile);
|
||||
|
|
@ -150,20 +186,26 @@ export class AndroidDevice {
|
|||
}
|
||||
|
||||
async close() {
|
||||
const driver = await this._driver();
|
||||
driver.close();
|
||||
if (this._pollingWebViews)
|
||||
clearTimeout(this._pollingWebViews);
|
||||
for (const connection of this._browserConnections)
|
||||
await connection.close();
|
||||
if (this._driverPromise) {
|
||||
const driver = await this._driver();
|
||||
driver.close();
|
||||
}
|
||||
await this._backend.close();
|
||||
}
|
||||
|
||||
async launchBrowser(packageName: string = 'com.android.chrome', options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||
debug('pw:android')('Force-stopping', packageName);
|
||||
await this._backend.runCommand(`shell:am force-stop ${packageName}`);
|
||||
async launchBrowser(pkg: string = 'com.android.chrome', options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||
debug('pw:android')('Force-stopping', pkg);
|
||||
await this._backend.runCommand(`shell:am force-stop ${pkg}`);
|
||||
|
||||
const socketName = createGuid();
|
||||
const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`;
|
||||
debug('pw:android')('Starting', packageName, commandLine);
|
||||
debug('pw:android')('Starting', pkg, commandLine);
|
||||
await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
|
||||
await this._backend.runCommand(`shell:am start -n ${packageName}/com.google.android.apps.chrome.Main about:blank`);
|
||||
await this._backend.runCommand(`shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`);
|
||||
|
||||
debug('pw:android')('Polling for socket', socketName);
|
||||
while (true) {
|
||||
|
|
@ -173,8 +215,20 @@ export class AndroidDevice {
|
|||
await new Promise(f => setTimeout(f, 100));
|
||||
}
|
||||
debug('pw:android')('Got the socket, connecting');
|
||||
const androidBrowser = new AndroidBrowser(this, packageName, socketName);
|
||||
return await this._connectToBrowser(socketName, options);
|
||||
}
|
||||
|
||||
connectToWebView(pid: number): Promise<BrowserContext> {
|
||||
const webView = this._webViews.get(pid);
|
||||
if (!webView)
|
||||
throw new Error('WebView has been closed');
|
||||
return this._connectToBrowser(`webview_devtools_remote_${pid}`);
|
||||
}
|
||||
|
||||
private async _connectToBrowser(socketName: string, options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||
const androidBrowser = new AndroidBrowser(this, socketName);
|
||||
await androidBrowser._open();
|
||||
this._browserConnections.add(androidBrowser);
|
||||
|
||||
const browserOptions: BrowserOptions = {
|
||||
name: 'clank',
|
||||
|
|
@ -195,6 +249,49 @@ export class AndroidDevice {
|
|||
});
|
||||
return browser._defaultContext!;
|
||||
}
|
||||
|
||||
webViews(): AndroidWebView[] {
|
||||
return [...this._webViews.values()];
|
||||
}
|
||||
|
||||
private async _refreshWebViews() {
|
||||
const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).split('\n');
|
||||
|
||||
const newPids = new Set<number>();
|
||||
for (const line of sockets) {
|
||||
const match = line.match(/[^@]+@webview_devtools_remote_(\d+)/);
|
||||
if (!match)
|
||||
continue;
|
||||
const pid = +match[1];
|
||||
newPids.add(pid);
|
||||
}
|
||||
for (const pid of newPids) {
|
||||
if (this._webViews.has(pid))
|
||||
continue;
|
||||
|
||||
const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).split('\n');
|
||||
let pkg = '';
|
||||
for (const proc of procs) {
|
||||
const match = proc.match(/[^\s]+\s+(\d+).*$/);
|
||||
if (!match)
|
||||
continue;
|
||||
const p = match[1];
|
||||
if (+p !== pid)
|
||||
continue;
|
||||
pkg = proc.substring(proc.lastIndexOf(' '));
|
||||
}
|
||||
const webView = { pid, pkg };
|
||||
this._webViews.set(pid, webView);
|
||||
this.emit(AndroidDevice.Events.WebViewAdded, webView);
|
||||
}
|
||||
|
||||
for (const p of this._webViews.keys()) {
|
||||
if (!newPids.has(p)) {
|
||||
this._webViews.delete(p);
|
||||
this.emit(AndroidDevice.Events.WebViewRemoved, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AndroidBrowser extends EventEmitter {
|
||||
|
|
@ -205,11 +302,9 @@ class AndroidBrowser extends EventEmitter {
|
|||
private _waitForNextTask = makeWaitForNextTask();
|
||||
onmessage?: (message: any) => void;
|
||||
onclose?: () => void;
|
||||
private _packageName: string;
|
||||
|
||||
constructor(device: AndroidDevice, packageName: string, socketName: string) {
|
||||
constructor(device: AndroidDevice, socketName: string) {
|
||||
super();
|
||||
this._packageName = packageName;
|
||||
this.device = device;
|
||||
this.socketName = socketName;
|
||||
this._receiver = new (ws as any).Receiver() as stream.Writable;
|
||||
|
|
@ -249,7 +344,6 @@ Sec-WebSocket-Version: 13\r
|
|||
|
||||
async close() {
|
||||
await this._socket!.close();
|
||||
await this.device._backend.runCommand(`shell:am force-stop ${this._packageName}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class AdbDevice implements DeviceBackend {
|
|||
|
||||
async function runCommand(command: string, serial?: string): Promise<string> {
|
||||
debug('pw:adb:runCommand')(command, serial);
|
||||
const socket = new BufferedSocketWrapper(net.createConnection({ port: 5037 }));
|
||||
const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 }));
|
||||
if (serial) {
|
||||
await socket.write(encodeMessage(`host:transport:${serial}`));
|
||||
const status = await socket.read(4);
|
||||
|
|
@ -72,7 +72,7 @@ async function runCommand(command: string, serial?: string): Promise<string> {
|
|||
}
|
||||
|
||||
async function open(command: string, serial?: string): Promise<BufferedSocketWrapper> {
|
||||
const socket = new BufferedSocketWrapper(net.createConnection({ port: 5037 }));
|
||||
const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 }));
|
||||
if (serial) {
|
||||
await socket.write(encodeMessage(`host:transport:${serial}`));
|
||||
const status = await socket.read(4);
|
||||
|
|
@ -97,13 +97,15 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
|
|||
private _notifyReader: (() => void) | undefined;
|
||||
private _connectPromise: Promise<void>;
|
||||
private _isClosed = false;
|
||||
private _command: string;
|
||||
|
||||
constructor(socket: net.Socket) {
|
||||
constructor(command: string, socket: net.Socket) {
|
||||
super();
|
||||
this._command = command;
|
||||
this._socket = socket;
|
||||
this._connectPromise = new Promise(f => this._socket.on('connect', f));
|
||||
this._socket.on('data', data => {
|
||||
debug('pw:android:adb:data')(data.toString());
|
||||
debug('pw:adb:data')(data.toString());
|
||||
if (this._isSocket) {
|
||||
this.emit('data', data);
|
||||
return;
|
||||
|
|
@ -122,13 +124,13 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
|
|||
}
|
||||
|
||||
async write(data: Buffer) {
|
||||
debug('pw:android:adb:send')(data.toString());
|
||||
debug('pw:adb:send')(data.toString().substring(0, 100) + '...');
|
||||
await this._connectPromise;
|
||||
await new Promise(f => this._socket.write(data, f));
|
||||
}
|
||||
|
||||
async close() {
|
||||
debug('pw:android:adb')('Close');
|
||||
debug('pw:adb')('Close ' + this._command);
|
||||
this._socket.destroy();
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +141,7 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
|
|||
await new Promise(f => this._notifyReader = f);
|
||||
const result = this._buffer.slice(0, length);
|
||||
this._buffer = this._buffer.slice(length);
|
||||
debug('pw:android:adb:recv')(result.toString());
|
||||
debug('pw:adb:recv')(result.toString().substring(0, 100) + '...');
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue