appium-lg-webos-driver
Version:
LG WebOS support for Appium
524 lines • 23.5 kB
JavaScript
"use strict";
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _WebOSDriver_instances, _WebOSDriver_chromedriver, _WebOSDriver_isChromedriverAutodownloadEnabled, _WebOSDriver_executeChromedriverScript, _WebOSDriver_pressKeyViaJs;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebOSDriver = exports.DEFAULT_PRESS_DURATION_MS = exports.KEYS = void 0;
const driver_1 = require("appium/driver");
const bluebird_1 = __importDefault(require("bluebird"));
const lodash_1 = __importDefault(require("lodash"));
const ares_1 = require("./cli/ares");
const constraints_1 = require("./constraints");
const scripts_1 = require("./scripts");
// @ts-ignore
const appium_chromedriver_1 = __importDefault(require("appium-chromedriver"));
const get_port_1 = __importDefault(require("get-port"));
const got_1 = __importDefault(require("got"));
const keys_1 = require("./keys");
const logger_1 = __importDefault(require("./logger"));
const lg_remote_client_1 = require("./remote/lg-remote-client");
const lg_socket_client_1 = require("./remote/lg-socket-client");
// eslint-disable-next-line import/no-unresolved
const valuebox_1 = require("./remote/valuebox");
var keys_2 = require("./keys");
Object.defineProperty(exports, "KEYS", { enumerable: true, get: function () { return keys_2.KEYS; } });
// this is the ID for the 'Developer' application, which we launch after a session ends to ensure
// some app stays running (otherwise the TV might shut off)
const DEV_MODE_ID = 'com.palmdts.devmode';
/**
* A security flag to enable chromedriver auto download feature
*/
const CHROMEDRIVER_AUTODOWNLOAD_FEATURE = 'chromedriver_autodownload';
/**
* To get chrome driver version in the UA
*/
const REGEXP_CHROME_VERSION_IN_UA = new RegExp('Chrome\\/(\\S+)');
/**
* To get chrome version from the browser info.
*/
const VERSION_PATTERN = /([\d.]+)/;
/**
* Minimal chrome browser for autodownload.
* Chromedriver for older than this chrome version could have an issue
* to raise no chrome binary error.
*/
const MIN_CHROME_MAJOR_VERSION = 63;
const MIN_CHROME_VERSION = 'Chrome/63.0.3239.0';
// don't proxy any 'appium' routes
/** @type {RouteMatcher[]} */
const NO_PROXY = [
['POST', new RegExp('^/session/[^/]+/appium')],
['GET', new RegExp('^/session/[^/]+/appium')],
['GET', new RegExp('^/session/[^/]+/context')],
['POST', new RegExp('^/session/[^/]+/execute/sync')],
];
exports.DEFAULT_PRESS_DURATION_MS = 100;
/**
* @extends {BaseDriver<WebOsConstraints>}
*/
class WebOSDriver extends driver_1.BaseDriver {
constructor() {
super(...arguments);
_WebOSDriver_instances.add(this);
/** @type {RouteMatcher[]} */
this.jwpProxyAvoid = lodash_1.default.clone(NO_PROXY); // why clone?
/** @type {boolean} */
this.jwpProxyActive = false;
this.desiredCapConstraints = constraints_1.CAP_CONSTRAINTS;
/** @type {Chromedriver|undefined} */
_WebOSDriver_chromedriver.set(this, void 0);
}
/**
*
* @param {any} name
* @returns {name is ScriptId}
*/
static isExecuteScript(name) {
return name in WebOSDriver.executeMethodMap;
}
/**
* @param {W3CWebOsCaps} w3cCaps1
* @param {W3CWebOsCaps} w3cCaps2
* @param {W3CWebOsCaps} w3cCaps3
* @returns {Promise<[string,WebOsCaps]>}
*/
async createSession(w3cCaps1, w3cCaps2, w3cCaps3) {
w3cCaps3.alwaysMatch = { ...constraints_1.DEFAULT_CAPS, ...w3cCaps3.alwaysMatch };
let [sessionId, caps] = await super.createSession(w3cCaps1, w3cCaps2, w3cCaps3);
const { autoExtendDevMode, deviceName, app, appId, appLaunchParams, noReset, fullReset, deviceHost, debuggerPort, showChromedriverLog, chromedriverExecutable, chromedriverExecutableDir, appLaunchCooldown, remoteOnly, websocketPort, websocketPortSecure, useSecureWebsocket, keyCooldown, } = caps;
if (noReset && fullReset) {
throw new Error(`Cannot use both noReset and fullReset`);
}
if (autoExtendDevMode) {
await (0, ares_1.extendDevMode)(deviceName);
}
try {
caps.deviceInfo = await (0, ares_1.getDeviceInfo)(deviceName);
}
catch (error) {
throw new Error(`Could not retrieve device info for device with ` +
`name '${deviceName}'. Are you sure the device is ` +
`connected? (Original error: ${error})`);
}
if (fullReset) {
try {
await (0, ares_1.uninstallApp)(appId, deviceName);
}
catch (err) {
// if the app is not installed, we expect the following error message, so if we get any
// message other than that one, bubble the error up. Otherwise, just ignore!
if (!/FAILED_REMOVE/.test(/** @type {Error} */ (err).message)) {
throw err;
}
}
}
if (app) {
await (0, ares_1.installApp)(app, appId, deviceName);
}
this.valueBox = valuebox_1.ValueBox.create('appium-lg-webos-driver');
this.socketClient = new lg_socket_client_1.LGWSClient({
valueBox: this.valueBox,
deviceName,
url: `ws://${deviceHost}:${websocketPort}`,
urlSecure: `wss://${deviceHost}:${websocketPortSecure}`,
useSecureWebsocket,
remoteKeyCooldown: keyCooldown,
});
logger_1.default.info(`Connecting remote; address any prompts on screen now!`);
await this.socketClient.initialize();
this.remoteClient = await this.socketClient.getRemoteClient();
await (0, ares_1.launchApp)(appId, deviceName, appLaunchParams);
const waitMsgInterval = setInterval(() => {
logger_1.default.info('Waiting for app launch to take effect');
}, 1000);
await bluebird_1.default.delay(appLaunchCooldown);
clearInterval(waitMsgInterval);
if (remoteOnly) {
logger_1.default.info(`Remote-only mode requested, not starting chromedriver`);
// in remote-only mode, we force rcMode to 'rc' instead of 'js'
this.opts.rcMode = caps.rcMode = 'rc';
return [sessionId, caps];
}
await this.startChromedriver({
debuggerHost: deviceHost,
debuggerPort,
executable: /** @type {string} */ (chromedriverExecutable),
executableDir: /** @type {string} */ (chromedriverExecutableDir),
isAutodownloadEnabled: /** @type {Boolean} */ (__classPrivateFieldGet(this, _WebOSDriver_instances, "m", _WebOSDriver_isChromedriverAutodownloadEnabled).call(this)),
verbose: /** @type {Boolean | undefined} */ (showChromedriverLog),
});
logger_1.default.info('Waiting for app launch to take effect');
await bluebird_1.default.delay(appLaunchCooldown);
if (!noReset) {
logger_1.default.info('Clearing app local storage & reloading');
await this.executeChromedriverScript(scripts_1.SyncScripts.reset);
}
return [sessionId, caps];
}
/**
* @typedef BrowserVersionInfo
* @property {string} Browser
* @property {string} Protocol-Version
* @property {string} User-Agent
* @property {string} [V8-Version]
* @property {string} WebKit-Version
* @property {string} [webSocketDebuggerUrl]
*/
/**
* Use UserAgent info for "Browser" if the chrome response did not include
* browser name properly.
* @param {BrowserVersionInfo} browserVersionInfo
* @return {BrowserVersionInfo}
*/
useUAForBrowserIfNotPresent(browserVersionInfo) {
if (!lodash_1.default.isEmpty(browserVersionInfo.Browser)) {
return browserVersionInfo;
}
const ua = browserVersionInfo['User-Agent'];
if (lodash_1.default.isEmpty(ua)) {
return browserVersionInfo;
}
const chromeVersion = ua.match(REGEXP_CHROME_VERSION_IN_UA);
if (lodash_1.default.isEmpty(chromeVersion)) {
return browserVersionInfo;
}
logger_1.default.info(`The response did not have Browser, thus set the Browser value from UA as ${JSON.stringify(browserVersionInfo)}`);
// @ts-ignore isEmpty already checked as null.
browserVersionInfo.Browser = chromeVersion[0];
return browserVersionInfo;
}
/**
* Set chrome version v63.0.3239.0 as the minimal version
* for autodownload to use proper chromedriver version if
* - the 'Browser' info does not have proper chrome version, or
* - older than the chromedriver version could raise no Chrome binary found error,
* which no makes sense for TV automation usage.
*
* @param {BrowserVersionInfo} browserVersionInfo
* @return {BrowserVersionInfo}
*/
fixChromeVersionForAutodownload(browserVersionInfo) {
const chromeVersion = VERSION_PATTERN.exec(browserVersionInfo.Browser ?? '');
if (!chromeVersion) {
browserVersionInfo.Browser = MIN_CHROME_VERSION;
return browserVersionInfo;
}
const majorV = chromeVersion[1].split('.')[0];
if (lodash_1.default.toInteger(majorV) < MIN_CHROME_MAJOR_VERSION) {
logger_1.default.info(`The device chrome version is ${chromeVersion[1]}, ` +
`which could cause an issue for the matched chromedriver version. ` +
`Setting ${MIN_CHROME_VERSION} as browser forcefully`);
browserVersionInfo.Browser = MIN_CHROME_VERSION;
}
return browserVersionInfo;
}
/**
* @param {StartChromedriverOptions} opts
*/
async startChromedriver({ debuggerHost, debuggerPort, executable, executableDir, isAutodownloadEnabled, verbose }) {
const debuggerAddress = `${debuggerHost}:${debuggerPort}`;
let result;
if (executableDir) {
// get the result of chrome info to use auto detection.
try {
result = await got_1.default.get(`http://${debuggerAddress}/json/version`).json();
logger_1.default.info(`The response of http://${debuggerAddress}/json/version was ${JSON.stringify(result)}`);
result = this.useUAForBrowserIfNotPresent(result);
result = this.fixChromeVersionForAutodownload(result);
logger_1.default.info(`Fixed browser info is ${JSON.stringify(result)}`);
// To respect the executableDir.
executable = undefined;
if (lodash_1.default.isEmpty(result.Browser)) {
this.log.info(`No browser version info was available. If no proper chromedrivers exist in ${executableDir}, the session creation will fail.`);
}
}
catch (err) {
throw new driver_1.errors.SessionNotCreatedError(`Could not get the chrome browser information to detect proper chromedriver version. Is it a debuggable build? Error: ${err.message}`);
}
}
__classPrivateFieldSet(this, _WebOSDriver_chromedriver, new appium_chromedriver_1.default({
// @ts-ignore bad types
port: await (0, get_port_1.default)(),
executable,
executableDir,
isAutodownloadEnabled,
// @ts-ignore
details: { info: result },
verbose
}), "f");
// XXX: goog:chromeOptions in newer versions, chromeOptions in older
await __classPrivateFieldGet(this, _WebOSDriver_chromedriver, "f").start({
chromeOptions: {
debuggerAddress,
},
});
this.proxyReqRes = __classPrivateFieldGet(this, _WebOSDriver_chromedriver, "f").proxyReq.bind(__classPrivateFieldGet(this, _WebOSDriver_chromedriver, "f"));
this.jwpProxyActive = true;
}
/**
* Execute some arbitrary JS via Chromedriver.
* @template [TReturn=any]
* @template [TArg=any]
* @param {((...args: any[]) => TReturn)|string} script
* @param {TArg[]} [args]
* @returns {Promise<{value: TReturn}>}
*/
async executeChromedriverScript(script, args = []) {
return await __classPrivateFieldGet(this, _WebOSDriver_instances, "m", _WebOSDriver_executeChromedriverScript).call(this, '/execute/sync', script, args);
}
/**
* Given a script of {@linkcode ScriptId} or some arbitrary JS, figure out
* which it is and run it.
*
* @template [TArg=any]
* @template [TReturn=unknown]
* @template {import('type-fest').LiteralUnion<ScriptId, string>} [S=string]
* @param {S} script
* @param {S extends ScriptId ? [Record<string,any>] : TArg[]} args
* @returns {Promise<S extends ScriptId ? import('type-fest').AsyncReturnType<ExecuteMethod<S>> : {value: TReturn}>}
*/
async execute(script, args) {
if (WebOSDriver.isExecuteScript(script)) {
logger_1.default.debug(`Calling script "${script}" with arg ${JSON.stringify(args[0])}`);
const methodArgs = /** @type {[Record<string,any>]} */ (args);
return await this.executeMethod(script, [methodArgs[0]]);
}
return await /** @type {Promise<S extends ScriptId ? import('type-fest').AsyncReturnType<ExecuteMethod<S>> : {value: TReturn}>} */ (this.executeChromedriverScript(script, /** @type {TArg[]} */ (args)));
}
/**
*
* @param {string} sessionId
* @param {import('@appium/types').DriverData[]} [driverData]
*/
async deleteSession(sessionId, driverData) {
// TODO decide if we want to extend at the end of the session too
//if (this.opts.autoExtendDevMode) {
//await extendDevMode(this.opts.deviceName);
//}
if (__classPrivateFieldGet(this, _WebOSDriver_chromedriver, "f")) {
logger_1.default.debug(`Stopping chromedriver`);
// stop listening for the stopped state event
// @ts-ignore
__classPrivateFieldGet(this, _WebOSDriver_chromedriver, "f").removeAllListeners(appium_chromedriver_1.default.EVENT_CHANGED);
try {
await __classPrivateFieldGet(this, _WebOSDriver_chromedriver, "f").stop();
}
catch (err) {
logger_1.default.warn(`Error stopping Chromedriver: ${ /** @type {Error} */(err).message}`);
}
__classPrivateFieldSet(this, _WebOSDriver_chromedriver, undefined, "f");
}
try {
await (0, ares_1.closeApp)(this.opts.appId, this.opts.deviceName);
}
catch (err) {
logger_1.default.warn(`Error in closing ${this.opts.appId}: ${ /** @type {Error} */(err).message}`);
}
if (this.remoteClient) {
logger_1.default.info(`Pressing HOME and launching dev app to prevent auto off`);
await this.remoteClient.pressKey(lg_remote_client_1.LGRemoteKeys.HOME);
await (0, ares_1.launchApp)(DEV_MODE_ID, this.opts.deviceName);
}
if (this.socketClient) {
logger_1.default.debug(`Stopping socket clients`);
try {
await this.socketClient.disconnect();
}
catch (err) {
logger_1.default.warn(`Error stopping socket clients: ${err}`);
}
this.socketClient = undefined;
this.remoteClient = undefined;
}
await super.deleteSession(sessionId, driverData);
}
proxyActive() {
return this.jwpProxyActive;
}
getProxyAvoidList() {
return this.jwpProxyAvoid;
}
canProxy() {
return true;
}
/**
* Automates a keypress
* @param {import('./keys').KnownKey} key
* @param {number} [duration]
*/
async pressKey(key, duration) {
if (this.opts.rcMode === 'js') {
return await __classPrivateFieldGet(this, _WebOSDriver_instances, "m", _WebOSDriver_pressKeyViaJs).call(this, key, duration);
}
else {
if (duration) {
this.log.warn(`Attempted to send a duration for a remote-based ` + `key press; duration will be ignored`);
}
return await this.pressKeyViaRemote(key);
}
}
/**
* Automates a press of a button on a remote control.
* @param {string} key
*/
async pressKeyViaRemote(key) {
const sc = /** @type {import('./remote/lg-socket-client').LGWSClient} */ (this.socketClient);
const rc = /** @type {import('./remote/lg-remote-client').LGRemoteClient} */ (this.remoteClient);
const keyMap = Object.freeze(
/** @type {const} */ ({
VOL_UP: sc.volumeUp,
VOL_DOWN: sc.volumeDown,
MUTE: sc.mute,
UNMUTE: sc.unmute,
PLAY: sc.play,
STOP: sc.stop,
REWIND: sc.rewind,
FF: sc.fastForward,
CHAN_UP: sc.channelUp,
CHAN_DOWN: sc.channelDown,
}));
/**
*
* @param {any} key
* @returns {key is keyof typeof keyMap}
*/
const isMappedKey = (key) => key in keyMap;
const knownKeys = [...Object.keys(keyMap), ...Object.keys(lg_remote_client_1.LGRemoteKeys)];
if (!knownKeys.includes(lodash_1.default.upperCase(key))) {
this.log.warn(`Unknown key '${key}'; will send to remote as-is`);
return await rc.pressKey(key);
}
key = lodash_1.default.upperCase(key);
if (isMappedKey(key)) {
this.log.info(`Found virtual 'key' to be sent as socket command`);
return await keyMap[key].call(sc);
}
return await rc.pressKey(key);
}
/**
*
* @returns {Promise<[object]>} Return the list of installed applications
*/
async listApps() {
const sc = /** @type {import('./remote/lg-socket-client').LGWSClient} */ (this.socketClient);
if (sc) {
return (await sc.getListApps()).apps;
}
;
throw new driver_1.errors.UnknownError('Socket connection to the device might be missed');
}
/**
*
* @returns {Promise<object>} Return current active application information.
*/
async getCurrentForegroundAppInfo() {
const sc = /** @type {import('./remote/lg-socket-client').LGWSClient} */ (this.socketClient);
if (sc) {
// {"returnValue"=>true, "appId"=>"com.your.app", "processId"=>"", "windowId"=>""}
return await sc.getForegroundAppInfo();
}
;
throw new driver_1.errors.UnknownError('Socket connection to the device might be missed');
}
/**
* A dummy implementation to return 200 ok with NATIVE_APP context for
* webdriverio compatibility. https://github.com/headspinio/appium-roku-driver/issues/175
*
* @returns {Promise<string>}
*/
// eslint-disable-next-line require-await
async getCurrentContext() {
return 'NATIVE_APP';
}
}
exports.WebOSDriver = WebOSDriver;
_WebOSDriver_chromedriver = new WeakMap(), _WebOSDriver_instances = new WeakSet(), _WebOSDriver_isChromedriverAutodownloadEnabled = function _WebOSDriver_isChromedriverAutodownloadEnabled() {
if (this.isFeatureEnabled(CHROMEDRIVER_AUTODOWNLOAD_FEATURE)) {
return true;
}
this.log.debug(`Automated Chromedriver download is disabled. ` +
`Use '${CHROMEDRIVER_AUTODOWNLOAD_FEATURE}' server feature to enable it`);
return false;
}, _WebOSDriver_executeChromedriverScript =
/**
* Execute some arbitrary JS via Chromedriver.
* @template [TReturn=unknown]
* @template [TArg=any]
* @param {string} endpointPath - Relative path of the endpoint URL
* @param {((...args: any[]) => TReturn)|string} script
* @param {TArg[]} [args]
* @returns {Promise<{value: TReturn}>}
*/
async function _WebOSDriver_executeChromedriverScript(endpointPath, script, args = []) {
const wrappedScript = typeof script === 'string' ? script : `return (${script}).apply(null, arguments)`;
// @ts-ignore
return await __classPrivateFieldGet(this, _WebOSDriver_chromedriver, "f").sendCommand(endpointPath, 'POST', {
script: wrappedScript,
args,
});
}, _WebOSDriver_pressKeyViaJs =
/**
* Press key via Chromedriver.
* @param {import('./keys').KnownKey} key
* @param {number} [duration]
*/
async function _WebOSDriver_pressKeyViaJs(key, duration = exports.DEFAULT_PRESS_DURATION_MS) {
key = /** @type {typeof key} */ (key.toLowerCase());
const [keyCode, keyName] = keys_1.KEYMAP[key];
if (!keyCode) {
throw new driver_1.errors.InvalidArgumentError(`Key name '${key}' is not supported`);
}
await __classPrivateFieldGet(this, _WebOSDriver_instances, "m", _WebOSDriver_executeChromedriverScript).call(this, '/execute/sync', scripts_1.AsyncScripts.pressKey, [
keyCode,
keyName,
duration,
]);
};
WebOSDriver.executeMethodMap = {
'webos: pressKey': Object.freeze({
command: 'pressKey',
params: { required: ['key'], optional: ['duration'] },
}),
'webos: listApps': Object.freeze({
command: 'listApps'
}),
'webos: activeAppInfo': Object.freeze({
command: 'getCurrentForegroundAppInfo'
}),
};
/**
* @typedef {import('./types').ExtraWebOsCaps} WebOSCapabilities
* @typedef {import('./constraints').WebOsConstraints} WebOsConstraints
* @typedef {import('./keys').KnownKey} Key
* @typedef {import('./types').StartChromedriverOptions} StartChromedriverOptions
*/
/**
* @typedef {import('@appium/types').DriverCaps<WebOsConstraints, WebOSCapabilities>} WebOsCaps
* @typedef {import('@appium/types').W3CDriverCaps<WebOsConstraints, WebOSCapabilities>} W3CWebOsCaps
* @typedef {import('@appium/types').RouteMatcher} RouteMatcher
*/
/**
* @typedef {typeof WebOSDriver.executeMethodMap} WebOSDriverExecuteMethodMap
*/
/**
* A known script identifier (e.g., `tizen: pressKey`)
* @typedef {keyof WebOSDriverExecuteMethodMap} ScriptId
*/
/**
* Lookup a method by its script ID.
* @template {ScriptId} S
* @typedef {WebOSDriver[WebOSDriverExecuteMethodMap[S]['command']]} ExecuteMethod
*/
//# sourceMappingURL=driver.js.map