519 lines
12 KiB
JavaScript
519 lines
12 KiB
JavaScript
"use strict";
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.AndroidWebView = exports.AndroidInput = exports.AndroidSocket = exports.AndroidDevice = exports.Android = void 0;
|
|
|
|
var _fs = _interopRequireDefault(require("fs"));
|
|
|
|
var _utils = require("../utils/utils");
|
|
|
|
var _events = require("./events");
|
|
|
|
var _browserContext = require("./browserContext");
|
|
|
|
var _channelOwner = require("./channelOwner");
|
|
|
|
var _timeoutSettings = require("../utils/timeoutSettings");
|
|
|
|
var _waiter = require("./waiter");
|
|
|
|
var _events2 = require("events");
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
class Android extends _channelOwner.ChannelOwner {
|
|
static from(android) {
|
|
return android._object;
|
|
}
|
|
|
|
constructor(parent, type, guid, initializer) {
|
|
super(parent, type, guid, initializer);
|
|
this._timeoutSettings = void 0;
|
|
this._timeoutSettings = new _timeoutSettings.TimeoutSettings();
|
|
}
|
|
|
|
setDefaultTimeout(timeout) {
|
|
this._timeoutSettings.setDefaultTimeout(timeout);
|
|
|
|
this._channel.setDefaultTimeoutNoReply({
|
|
timeout
|
|
});
|
|
}
|
|
|
|
async devices() {
|
|
return this._wrapApiCall(async channel => {
|
|
const {
|
|
devices
|
|
} = await channel.devices();
|
|
return devices.map(d => AndroidDevice.from(d));
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
exports.Android = Android;
|
|
|
|
class AndroidDevice extends _channelOwner.ChannelOwner {
|
|
static from(androidDevice) {
|
|
return androidDevice._object;
|
|
}
|
|
|
|
constructor(parent, type, guid, initializer) {
|
|
super(parent, type, guid, initializer);
|
|
this._timeoutSettings = void 0;
|
|
this._webViews = new Map();
|
|
this.input = void 0;
|
|
this.input = new AndroidInput(this);
|
|
this._timeoutSettings = new _timeoutSettings.TimeoutSettings(parent._timeoutSettings);
|
|
|
|
this._channel.on('webViewAdded', ({
|
|
webView
|
|
}) => this._onWebViewAdded(webView));
|
|
|
|
this._channel.on('webViewRemoved', ({
|
|
pid
|
|
}) => this._onWebViewRemoved(pid));
|
|
}
|
|
|
|
_onWebViewAdded(webView) {
|
|
const view = new AndroidWebView(this, webView);
|
|
|
|
this._webViews.set(webView.pid, view);
|
|
|
|
this.emit(_events.Events.AndroidDevice.WebView, view);
|
|
}
|
|
|
|
_onWebViewRemoved(pid) {
|
|
const view = this._webViews.get(pid);
|
|
|
|
this._webViews.delete(pid);
|
|
|
|
if (view) view.emit(_events.Events.AndroidWebView.Close);
|
|
}
|
|
|
|
setDefaultTimeout(timeout) {
|
|
this._timeoutSettings.setDefaultTimeout(timeout);
|
|
|
|
this._channel.setDefaultTimeoutNoReply({
|
|
timeout
|
|
});
|
|
}
|
|
|
|
serial() {
|
|
return this._initializer.serial;
|
|
}
|
|
|
|
model() {
|
|
return this._initializer.model;
|
|
}
|
|
|
|
webViews() {
|
|
return [...this._webViews.values()];
|
|
}
|
|
|
|
async webView(selector, options) {
|
|
const webView = [...this._webViews.values()].find(v => v.pkg() === selector.pkg);
|
|
if (webView) return webView;
|
|
return this.waitForEvent('webview', { ...options,
|
|
predicate: view => view.pkg() === selector.pkg
|
|
});
|
|
}
|
|
|
|
async wait(selector, options) {
|
|
await this._wrapApiCall(async channel => {
|
|
await channel.wait({
|
|
selector: toSelectorChannel(selector),
|
|
...options
|
|
});
|
|
});
|
|
}
|
|
|
|
async fill(selector, text, options) {
|
|
await this._wrapApiCall(async channel => {
|
|
await channel.fill({
|
|
selector: toSelectorChannel(selector),
|
|
text,
|
|
...options
|
|
});
|
|
});
|
|
}
|
|
|
|
async press(selector, key, options) {
|
|
await this.tap(selector, options);
|
|
await this.input.press(key);
|
|
}
|
|
|
|
async tap(selector, options) {
|
|
await this._wrapApiCall(async channel => {
|
|
await channel.tap({
|
|
selector: toSelectorChannel(selector),
|
|
...options
|
|
});
|
|
});
|
|
}
|
|
|
|
async drag(selector, dest, options) {
|
|
await this._wrapApiCall(async channel => {
|
|
await channel.drag({
|
|
selector: toSelectorChannel(selector),
|
|
dest,
|
|
...options
|
|
});
|
|
});
|
|
}
|
|
|
|
async fling(selector, direction, options) {
|
|
await this._wrapApiCall(async channel => {
|
|
await channel.fling({
|
|
selector: toSelectorChannel(selector),
|
|
direction,
|
|
...options
|
|
});
|
|
});
|
|
}
|
|
|
|
async longTap(selector, options) {
|
|
await this._wrapApiCall(async channel => {
|
|
await channel.longTap({
|
|
selector: toSelectorChannel(selector),
|
|
...options
|
|
});
|
|
});
|
|
}
|
|
|
|
async pinchClose(selector, percent, options) {
|
|
await this._wrapApiCall(async channel => {
|
|
await channel.pinchClose({
|
|
selector: toSelectorChannel(selector),
|
|
percent,
|
|
...options
|
|
});
|
|
});
|
|
}
|
|
|
|
async pinchOpen(selector, percent, options) {
|
|
await this._wrapApiCall(async channel => {
|
|
await channel.pinchOpen({
|
|
selector: toSelectorChannel(selector),
|
|
percent,
|
|
...options
|
|
});
|
|
});
|
|
}
|
|
|
|
async scroll(selector, direction, percent, options) {
|
|
await this._wrapApiCall(async channel => {
|
|
await channel.scroll({
|
|
selector: toSelectorChannel(selector),
|
|
direction,
|
|
percent,
|
|
...options
|
|
});
|
|
});
|
|
}
|
|
|
|
async swipe(selector, direction, percent, options) {
|
|
await this._wrapApiCall(async channel => {
|
|
await channel.swipe({
|
|
selector: toSelectorChannel(selector),
|
|
direction,
|
|
percent,
|
|
...options
|
|
});
|
|
});
|
|
}
|
|
|
|
async info(selector) {
|
|
return await this._wrapApiCall(async channel => {
|
|
return (await channel.info({
|
|
selector: toSelectorChannel(selector)
|
|
})).info;
|
|
});
|
|
}
|
|
|
|
async screenshot(options = {}) {
|
|
return await this._wrapApiCall(async channel => {
|
|
const {
|
|
binary
|
|
} = await channel.screenshot();
|
|
const buffer = Buffer.from(binary, 'base64');
|
|
if (options.path) await _fs.default.promises.writeFile(options.path, buffer);
|
|
return buffer;
|
|
});
|
|
}
|
|
|
|
async close() {
|
|
return this._wrapApiCall(async channel => {
|
|
await channel.close();
|
|
this.emit(_events.Events.AndroidDevice.Close);
|
|
});
|
|
}
|
|
|
|
async shell(command) {
|
|
return this._wrapApiCall(async channel => {
|
|
const {
|
|
result
|
|
} = await channel.shell({
|
|
command
|
|
});
|
|
return Buffer.from(result, 'base64');
|
|
});
|
|
}
|
|
|
|
async open(command) {
|
|
return this._wrapApiCall(async channel => {
|
|
return AndroidSocket.from((await channel.open({
|
|
command
|
|
})).socket);
|
|
});
|
|
}
|
|
|
|
async installApk(file, options) {
|
|
return this._wrapApiCall(async channel => {
|
|
await channel.installApk({
|
|
file: await loadFile(file),
|
|
args: options && options.args
|
|
});
|
|
});
|
|
}
|
|
|
|
async push(file, path, options) {
|
|
return this._wrapApiCall(async channel => {
|
|
await channel.push({
|
|
file: await loadFile(file),
|
|
path,
|
|
mode: options ? options.mode : undefined
|
|
});
|
|
});
|
|
}
|
|
|
|
async launchBrowser(options = {}) {
|
|
return this._wrapApiCall(async channel => {
|
|
const contextOptions = await (0, _browserContext.prepareBrowserContextParams)(options);
|
|
const {
|
|
context
|
|
} = await channel.launchBrowser(contextOptions);
|
|
return _browserContext.BrowserContext.from(context);
|
|
});
|
|
}
|
|
|
|
async waitForEvent(event, optionsOrPredicate = {}) {
|
|
return this._wrapApiCall(async channel => {
|
|
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
|
|
|
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
|
|
|
const waiter = _waiter.Waiter.createForEvent(this, event);
|
|
|
|
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
|
|
if (event !== _events.Events.AndroidDevice.Close) waiter.rejectOnEvent(this, _events.Events.AndroidDevice.Close, new Error('Device closed'));
|
|
const result = await waiter.waitForEvent(this, event, predicate);
|
|
waiter.dispose();
|
|
return result;
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
exports.AndroidDevice = AndroidDevice;
|
|
|
|
class AndroidSocket extends _channelOwner.ChannelOwner {
|
|
static from(androidDevice) {
|
|
return androidDevice._object;
|
|
}
|
|
|
|
constructor(parent, type, guid, initializer) {
|
|
super(parent, type, guid, initializer);
|
|
|
|
this._channel.on('data', ({
|
|
data
|
|
}) => this.emit(_events.Events.AndroidSocket.Data, Buffer.from(data, 'base64')));
|
|
|
|
this._channel.on('close', () => this.emit(_events.Events.AndroidSocket.Close));
|
|
}
|
|
|
|
async write(data) {
|
|
return this._wrapApiCall(async channel => {
|
|
await channel.write({
|
|
data: data.toString('base64')
|
|
});
|
|
});
|
|
}
|
|
|
|
async close() {
|
|
return this._wrapApiCall(async channel => {
|
|
await channel.close();
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
exports.AndroidSocket = AndroidSocket;
|
|
|
|
async function loadFile(file) {
|
|
if ((0, _utils.isString)(file)) return _fs.default.promises.readFile(file, {
|
|
encoding: 'base64'
|
|
}).toString();
|
|
return file.toString('base64');
|
|
}
|
|
|
|
class AndroidInput {
|
|
constructor(device) {
|
|
this._device = void 0;
|
|
this._device = device;
|
|
}
|
|
|
|
async type(text) {
|
|
return this._device._wrapApiCall(async channel => {
|
|
await channel.inputType({
|
|
text
|
|
});
|
|
});
|
|
}
|
|
|
|
async press(key) {
|
|
return this._device._wrapApiCall(async channel => {
|
|
await channel.inputPress({
|
|
key
|
|
});
|
|
});
|
|
}
|
|
|
|
async tap(point) {
|
|
return this._device._wrapApiCall(async channel => {
|
|
await channel.inputTap({
|
|
point
|
|
});
|
|
});
|
|
}
|
|
|
|
async swipe(from, segments, steps) {
|
|
return this._device._wrapApiCall(async channel => {
|
|
await channel.inputSwipe({
|
|
segments,
|
|
steps
|
|
});
|
|
});
|
|
}
|
|
|
|
async drag(from, to, steps) {
|
|
return this._device._wrapApiCall(async channel => {
|
|
await channel.inputDrag({
|
|
from,
|
|
to,
|
|
steps
|
|
});
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
exports.AndroidInput = AndroidInput;
|
|
|
|
function toSelectorChannel(selector) {
|
|
const {
|
|
checkable,
|
|
checked,
|
|
clazz,
|
|
clickable,
|
|
depth,
|
|
desc,
|
|
enabled,
|
|
focusable,
|
|
focused,
|
|
hasChild,
|
|
hasDescendant,
|
|
longClickable,
|
|
pkg,
|
|
res,
|
|
scrollable,
|
|
selected,
|
|
text
|
|
} = selector;
|
|
|
|
const toRegex = value => {
|
|
if (value === undefined) return undefined;
|
|
if (value instanceof RegExp) return value.source;
|
|
return '^' + value.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d') + '$';
|
|
};
|
|
|
|
return {
|
|
checkable,
|
|
checked,
|
|
clazz: toRegex(clazz),
|
|
pkg: toRegex(pkg),
|
|
desc: toRegex(desc),
|
|
res: toRegex(res),
|
|
text: toRegex(text),
|
|
clickable,
|
|
depth,
|
|
enabled,
|
|
focusable,
|
|
focused,
|
|
hasChild: hasChild ? {
|
|
selector: toSelectorChannel(hasChild.selector)
|
|
} : undefined,
|
|
hasDescendant: hasDescendant ? {
|
|
selector: toSelectorChannel(hasDescendant.selector),
|
|
maxDepth: hasDescendant.maxDepth
|
|
} : undefined,
|
|
longClickable,
|
|
scrollable,
|
|
selected
|
|
};
|
|
}
|
|
|
|
class AndroidWebView extends _events2.EventEmitter {
|
|
constructor(device, data) {
|
|
super();
|
|
this._device = void 0;
|
|
this._data = void 0;
|
|
this._pagePromise = void 0;
|
|
this._device = device;
|
|
this._data = data;
|
|
}
|
|
|
|
pid() {
|
|
return this._data.pid;
|
|
}
|
|
|
|
pkg() {
|
|
return this._data.pkg;
|
|
}
|
|
|
|
async page() {
|
|
if (!this._pagePromise) this._pagePromise = this._fetchPage();
|
|
return this._pagePromise;
|
|
}
|
|
|
|
async _fetchPage() {
|
|
return this._device._wrapApiCall(async channel => {
|
|
const {
|
|
context
|
|
} = await channel.connectToWebView({
|
|
pid: this._data.pid
|
|
});
|
|
return _browserContext.BrowserContext.from(context).pages()[0];
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
exports.AndroidWebView = AndroidWebView; |