How to use helpers.initDevice method in Appium Android Driver

Best JavaScript code snippet using appium-android-driver

android-helper-specs.js

Source:android-helper-specs.js Github

copy

Full Screen

...699      mocks.adb.expects('startLogcat').once();700      mocks.helpers.expects('pushSettingsApp').once();701      mocks.helpers.expects('ensureDeviceLocale').withExactArgs(adb, opts.language, opts.locale, opts.localeScript).once();702      mocks.helpers.expects('setMockLocationApp').withExactArgs(adb, 'io.appium.settings').once();703      await helpers.initDevice(adb, opts);704      mocks.helpers.verify();705      mocks.adb.verify();706    });707    it('should init device without locale and language', async function () {708      const opts = {};709      mocks.adb.expects('waitForDevice').once();710      mocks.adb.expects('startLogcat').once();711      mocks.helpers.expects('pushSettingsApp').once();712      mocks.helpers.expects('ensureDeviceLocale').never();713      mocks.helpers.expects('setMockLocationApp').withExactArgs(adb, 'io.appium.settings').once();714      await helpers.initDevice(adb, opts);715      mocks.helpers.verify();716      mocks.adb.verify();717    });718    it('should init device with either locale or language', async function () {719      const opts = {language: 'en'};720      mocks.adb.expects('waitForDevice').once();721      mocks.adb.expects('startLogcat').once();722      mocks.helpers.expects('pushSettingsApp').once();723      mocks.helpers.expects('ensureDeviceLocale').withExactArgs(adb, opts.language, opts.locale, opts.localeScript).once();724      mocks.helpers.expects('setMockLocationApp').withExactArgs(adb, 'io.appium.settings').once();725      await helpers.initDevice(adb, opts);726      mocks.helpers.verify();727      mocks.adb.verify();728    });729    it('should not install mock location on emulator', async function () {730      const opts = {avd: 'avd'};731      mocks.adb.expects('waitForDevice').once();732      mocks.adb.expects('startLogcat').once();733      mocks.helpers.expects('pushSettingsApp').once();734      mocks.helpers.expects('ensureDeviceLocale').never();735      mocks.helpers.expects('setMockLocationApp').never();736      await helpers.initDevice(adb, opts);737      mocks.helpers.verify();738      mocks.adb.verify();739    });740    it('should return defaultIME if unicodeKeyboard is setted to true', async function () {741      const opts = {unicodeKeyboard: true};742      mocks.adb.expects('waitForDevice').once();743      mocks.adb.expects('startLogcat').once();744      mocks.helpers.expects('pushSettingsApp').once();745      mocks.helpers.expects('ensureDeviceLocale').never();746      mocks.helpers.expects('setMockLocationApp').once();747      mocks.helpers.expects('initUnicodeKeyboard').withExactArgs(adb).once().returns('defaultIME');748      await helpers.initDevice(adb, opts).should.become('defaultIME');749      mocks.helpers.verify();750      mocks.adb.verify();751    });752    it('should return undefined if unicodeKeyboard is setted to false', async function () {753      const opts = {unicodeKeyboard: false};754      mocks.adb.expects('waitForDevice').once();755      mocks.adb.expects('startLogcat').once();756      mocks.helpers.expects('pushSettingsApp').once();757      mocks.helpers.expects('ensureDeviceLocale').never();758      mocks.helpers.expects('setMockLocationApp').once();759      mocks.helpers.expects('initUnicodeKeyboard').never();760      should.not.exist(await helpers.initDevice(adb, opts));761      mocks.helpers.verify();762      mocks.adb.verify();763    });764    it('should not push unlock app if unlockType is defined', async function () {765      const opts = {unlockType: 'unlock_type'};766      mocks.adb.expects('waitForDevice').once();767      mocks.adb.expects('startLogcat').once();768      mocks.helpers.expects('pushSettingsApp').once();769      mocks.helpers.expects('ensureDeviceLocale').never();770      mocks.helpers.expects('setMockLocationApp').once();771      mocks.helpers.expects('initUnicodeKeyboard').never();772      await helpers.initDevice(adb, opts);773      mocks.helpers.verify();774      mocks.adb.verify();775    });776    it('should init device without starting logcat', async function () {777      const opts = { skipLogcatCapture: true };778      mocks.adb.expects('waitForDevice').once();779      mocks.adb.expects('startLogcat').never();780      mocks.helpers.expects('pushSettingsApp').once();781      mocks.helpers.expects('ensureDeviceLocale').never();782      mocks.helpers.expects('setMockLocationApp').withExactArgs(adb, 'io.appium.settings').once();783      await helpers.initDevice(adb, opts);784      mocks.helpers.verify();785      mocks.adb.verify();786    });787  }));788  describe('removeNullProperties', function () {789    it('should ignore null properties', function () {790      let test = {foo: null, bar: true};791      helpers.removeNullProperties(test);792      _.keys(test).length.should.equal(1);793    });794    it('should ignore undefined properties', function () {795      let test = {foo: undefined, bar: true};796      helpers.removeNullProperties(test);797      _.keys(test).length.should.equal(1);...

Full Screen

Full Screen

driver.js

Source:driver.js Github

copy

Full Screen

...285    this.caps.deviceName = this.adb.curDeviceId;286    this.caps.deviceUDID = this.opts.udid;287    // start an avd, set the language/locale, pick an emulator, etc...288    // TODO with multiple devices we'll need to parameterize this289    this.defaultIME = await helpers.initDevice(this.adb, this.opts);290    // Prepare the device by forwarding the UiAutomator2 port291    // This call mutates this.opts.systemPort if it is not set explicitly292    await this.allocateSystemPort();293    // set up the modified UiAutomator2 server etc294    await this.initUiAutomator2Server();295    // Should be after installing io.appium.settings in helpers.initDevice296    if (this.opts.disableWindowAnimation && (await this.adb.getApiLevel() < 26)) { // API level 26 is Android 8.0.297      // Granting android.permission.SET_ANIMATION_SCALE is necessary to handle animations under API level 26298      // Read https://github.com/appium/appium/pull/11640#issuecomment-438260477299      // `--no-window-animation` works over Android 8 to disable all of animations300      if (await this.adb.isAnimationOn()) {301        logger.info('Disabling animation via io.appium.settings');302        await this.adb.setAnimationState(false);303        this._wasWindowAnimationDisabled = true;...

Full Screen

Full Screen

ah1.js

Source:ah1.js Github

copy

Full Screen

1import _ from 'lodash';2import path from 'path';3import { exec } from 'teen_process';4import { retry, retryInterval } from 'asyncbox';5import logger from './logger';6import { fs } from 'appium-support';7import { path as unicodeIMEPath } from 'appium-android-ime';8import { path as settingsApkPath } from 'io.appium.settings';9import { path as unlockApkPath } from 'appium-unlock';10import Bootstrap from 'appium-android-bootstrap';11import B from 'bluebird';12import ADB from 'appium-adb';13import { default as unlocker, PIN_UNLOCK, PASSWORD_UNLOCK,14  PATTERN_UNLOCK, FINGERPRINT_UNLOCK } from './unlock-helpers';15const PACKAGE_INSTALL_TIMEOUT = 90000; // milliseconds16const CHROME_BROWSER_PACKAGE_ACTIVITY = {17  chrome: {18    pkg: 'com.android.chrome',19    activity: 'com.google.android.apps.chrome.Main',20  },21  chromium: {22    pkg: 'org.chromium.chrome.shell',23    activity: '.ChromeShellActivity',24  },25  chromebeta: {26    pkg: 'com.chrome.beta',27    activity: 'com.google.android.apps.chrome.Main',28  },29  browser: {30    pkg: 'com.android.browser',31    activity: 'com.android.browser.BrowserActivity',32  },33  'chromium-browser': {34    pkg: 'org.chromium.chrome',35    activity: 'com.google.android.apps.chrome.Main',36  },37  'chromium-webview': {38    pkg: 'org.chromium.webview_shell',39    activity: 'org.chromium.webview_shell.WebViewBrowserActivity',40  },41  default: {42    pkg: 'com.android.chrome',43    activity: 'com.google.android.apps.chrome.Main',44  },45};46const SETTINGS_HELPER_PKG_ID = 'io.appium.settings';47const SETTINGS_HELPER_PKG_ACTIVITY = ".Settings";48const UNLOCK_HELPER_PKG_ID = 'io.appium.unlock';49const UNLOCK_HELPER_PKG_ACTIVITY = ".Unlock";50const UNICODE_IME_PKG_ID = 'io.appium.android.ime';51let helpers = {};52helpers.createBaseADB = async function (opts = {}) {53  // filter out any unwanted options sent in54  // this list should be updated as ADB takes more arguments55  const {56    javaVersion,57    adbPort,58    suppressKillServer,59    remoteAdbHost,60    clearDeviceLogsOnStart,61    adbExecTimeout,62  } = opts;63  return await ADB.createADB({64    javaVersion,65    adbPort,66    suppressKillServer,67    remoteAdbHost,68    clearDeviceLogsOnStart,69    adbExecTimeout,70  });71};72helpers.parseJavaVersion = function (stderr) {73  let lines = stderr.split("\n");74  for (let line of lines) {75    if (new RegExp(/(java|openjdk) version/).test(line)) {76      return line.split(" ")[2].replace(/"/g, '');77    }78  }79  return null;80};81helpers.getJavaVersion = async function (logVersion = true) {82  let {stderr} = await exec('java', ['-version']);83  let javaVer = helpers.parseJavaVersion(stderr);84  if (javaVer === null) {85    throw new Error("Could not get the Java version. Is Java installed?");86  }87  if (logVersion) {88    logger.info(`Java version is: ${javaVer}`);89  }90  return javaVer;91};92helpers.prepareEmulator = async function (adb, opts) {93  let {avd, avdArgs, language, locale, avdLaunchTimeout,94       avdReadyTimeout} = opts;95  if (!avd) {96    throw new Error("Cannot launch AVD without AVD name");97  }98  let avdName = avd.replace('@', '');99  let runningAVD = await adb.getRunningAVD(avdName);100  if (runningAVD !== null) {101    if (avdArgs && avdArgs.toLowerCase().indexOf("-wipe-data") > -1) {102      logger.debug(`Killing '${avdName}' because it needs to be wiped at start.`);103      await adb.killEmulator(avdName);104    } else {105      logger.debug("Not launching AVD because it is already running.");106      return;107    }108  }109  avdArgs = this.prepareAVDArgs(opts, adb, avdArgs);110  await adb.launchAVD(avd, avdArgs, language, locale, avdLaunchTimeout,111                      avdReadyTimeout);112};113helpers.prepareAVDArgs = function (opts, adb, avdArgs) {114  let args = avdArgs ? [avdArgs] : [];115  if (!_.isUndefined(opts.networkSpeed)) {116    let networkSpeed = this.ensureNetworkSpeed(adb, opts.networkSpeed);117    args.push('-netspeed', networkSpeed);118  }119  if (opts.isHeadless) {120    args.push('-no-window');121  }122  return args.join(' ');123};124helpers.ensureNetworkSpeed = function (adb, networkSpeed) {125  if (_.values(adb.NETWORK_SPEED).indexOf(networkSpeed) !== -1) {126    return networkSpeed;127  }128  logger.warn(`Wrong network speed param ${networkSpeed}, using default: full. Supported values: ${_.values(adb.NETWORK_SPEED)}`);129  return adb.NETWORK_SPEED.FULL;130};131helpers.ensureDeviceLocale = async function (adb, language, country) {132  if (!_.isString(language) && !_.isString(country)) {133    logger.warn(`setDeviceLanguageCountry requires language or country.`);134    logger.warn(`Got language: '${language}' and country: '${country}'`);135    return;136  }137  await adb.setDeviceLanguageCountry(language, country);138  if (!await adb.ensureCurrentLocale(language, country)) {139    throw new Error(`Failed to set language: ${language} and country: ${country}`);140  }141};142helpers.getDeviceInfoFromCaps = async function (opts = {}) {143  // we can create a throwaway ADB instance here, so there is no dependency144  // on instantiating on earlier (at this point, we have no udid)145  // we can only use this ADB object for commands that would not be confused146  // if multiple devices are connected147  const adb = await helpers.createBaseADB(opts);148  let udid = opts.udid;149  let emPort = null;150  // a specific avd name was given. try to initialize with that151  if (opts.avd) {152    await helpers.prepareEmulator(adb, opts);153    udid = adb.curDeviceId;154    emPort = adb.emulatorPort;155  } else {156    // no avd given. lets try whatever's plugged in devices/emulators157    logger.info("Retrieving device list");158    let devices = await adb.getDevicesWithRetry();159    // udid was given, lets try to init with that device160    if (udid) {161      if (!_.includes(_.map(devices, 'udid'), udid)) {162        logger.errorAndThrow(`Device ${udid} was not in the list ` +163                             `of connected devices`);164      }165      emPort = adb.getPortFromEmulatorString(udid);166    } else if (opts.platformVersion) {167      opts.platformVersion = `${opts.platformVersion}`.trim();168      // a platform version was given. lets try to find a device with the same os169      logger.info(`Looking for a device with Android '${opts.platformVersion}'`);170      // in case we fail to find something, give the user a useful log that has171      // the device udids and os versions so they know what's available172      let availDevicesStr = [];173      // first try started devices/emulators174      for (let device of devices) {175        // direct adb calls to the specific device176        await adb.setDeviceId(device.udid);177        let deviceOS = await adb.getPlatformVersion();178        // build up our info string of available devices as we iterate179        availDevicesStr.push(`${device.udid} (${deviceOS})`);180        // we do a begins with check for implied wildcard matching181        // eg: 4 matches 4.1, 4.0, 4.1.3-samsung, etc182        if (deviceOS.indexOf(opts.platformVersion) === 0) {183          udid = device.udid;184          break;185        }186      }187      // we couldn't find anything! quit188      if (!udid) {189        logger.errorAndThrow(`Unable to find an active device or emulator ` +190                             `with OS ${opts.platformVersion}. The following ` +191                             `are available: ` + availDevicesStr.join(', '));192      }193      emPort = adb.getPortFromEmulatorString(udid);194    } else {195      // a udid was not given, grab the first device we see196      udid = devices[0].udid;197      emPort = adb.getPortFromEmulatorString(udid);198    }199  }200  logger.info(`Using device: ${udid}`);201  return {udid, emPort};202};203// returns a new adb instance with deviceId set204helpers.createADB = async function (opts = {}) {205  const {udid, emPort} = opts;206  const adb = await helpers.createBaseADB(opts);207  adb.setDeviceId(udid);208  if (emPort) {209    adb.setEmulatorPort(emPort);210  }211  return adb;212};213helpers.validatePackageActivityNames = function (opts) {214  for (const key of ['appPackage', 'appActivity', 'appWaitPackage', 'appWaitActivity']) {215    const name = opts[key];216    if (!name) {217      continue;218    }219    const match = /([^\w.*,])+/.exec(name);220    if (!match) {221      continue;222    }223    logger.warn(`Capability '${key}' is expected to only include latin letters, digits, underscore, dot, comma and asterisk characters.`);224    logger.warn(`Current value '${name}' has non-matching character at index ${match.index}: '${name.substring(0, match.index + 1)}'`);225  }226};227helpers.getLaunchInfo = async function (adb, opts) {228  let {app, appPackage, appActivity, appWaitPackage, appWaitActivity} = opts;229  if (!app) {230    logger.warn("No app sent in, not parsing package/activity");231    return;232  }233  this.validatePackageActivityNames(opts);234  if (appPackage && appActivity) {235    return;236  }237  logger.debug("Parsing package and activity from app manifest");238  let {apkPackage, apkActivity} =239    await adb.packageAndLaunchActivityFromManifest(app);240  if (apkPackage && !appPackage) {241    appPackage = apkPackage;242  }243  if (!appWaitPackage) {244    appWaitPackage = appPackage;245  }246  if (apkActivity && !appActivity) {247    appActivity = apkActivity;248  }249  if (!appWaitActivity) {250    appWaitActivity = appActivity;251  }252  logger.debug(`Parsed package and activity are: ${apkPackage}/${apkActivity}`);253  return {appPackage, appWaitPackage, appActivity, appWaitActivity};254};255helpers.resetApp = async function (adb, opts = {}) {256  const {257    app,258    appPackage,259    fastReset,260    fullReset,261    androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT,262    autoGrantPermissions,263    allowTestPackages264  } = opts;265  if (!appPackage) {266    throw new Error("'appPackage' option is required");267  }268  const isInstalled = await adb.isAppInstalled(appPackage);269  if (isInstalled) {270    try {271      await adb.forceStop(appPackage);272    } catch (ign) {}273    // fullReset has priority over fastReset274    if (!fullReset && fastReset) {275      const output = await adb.clear(appPackage);276      if (_.isString(output) && output.toLowerCase().includes('failed')) {277        throw new Error(`Cannot clear the application data of '${appPackage}'. Original error: ${output}`);278      }279      // executing `shell pm clear` resets previously assigned application permissions as well280      if (autoGrantPermissions) {281        try {282          await adb.grantAllPermissions(appPackage);283        } catch (error) {284          logger.error(`Unable to grant permissions requested. Original error: ${error.message}`);285        }286      }287      logger.debug(`Performed fast reset on the installed '${appPackage}' application (stop and clear)`);288      return;289    }290  }291  if (!app) {292    throw new Error("'app' option is required for reinstall");293  }294  logger.debug(`Running full reset on '${appPackage}' (reinstall)`);295  if (isInstalled) {296    await adb.uninstallApk(appPackage);297  }298  await adb.install(app, {299    grantPermissions: autoGrantPermissions,300    timeout: androidInstallTimeout,301    allowTestPackages,302  });303};304helpers.installApk = async function (adb, opts = {}) {305  const {306    app,307    appPackage,308    fastReset,309    fullReset,310    androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT,311    autoGrantPermissions,312    allowTestPackages313  } = opts;314  if (!app || !appPackage) {315    throw new Error("'app' and 'appPackage' options are required");316  }317  if (fullReset) {318    await this.resetApp(adb, opts);319    return;320  }321  // There is no need to reset the newly installed app322  const shouldPerformFastReset = fastReset && await adb.isAppInstalled(appPackage);323  await adb.installOrUpgrade(app, appPackage, {324    grantPermissions: autoGrantPermissions,325    timeout: androidInstallTimeout,326    allowTestPackages,327  });328  if (shouldPerformFastReset) {329    logger.info(`Performing fast reset on '${appPackage}'`);330    await this.resetApp(adb, opts);331  }332};333/**334 * Installs an array of apks335 * @param {ADB} adb Instance of Appium ADB object336 * @param {Object} opts Opts defined in driver.js337 */338helpers.installOtherApks = async function (otherApps, adb, opts) {339  let {340    androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT,341    autoGrantPermissions,342    allowTestPackages343  } = opts;344  // Install all of the APK's asynchronously345  await B.all(otherApps.map((otherApp) => {346    logger.debug(`Installing app: ${otherApp}`);347    return adb.installOrUpgrade(otherApp, null, {348      grantPermissions: autoGrantPermissions,349      timeout: androidInstallTimeout,350      allowTestPackages,351    });352  }));353};354helpers.initUnicodeKeyboard = async function (adb) {355  logger.debug('Enabling Unicode keyboard support');356  logger.debug("Pushing unicode ime to device...");357  try {358    await adb.install(unicodeIMEPath, {replace: false});359  } catch (err) {360    logger.info(`Performing full reinstall of ${UNICODE_IME_PKG_ID} as a possible fix for: ${err.message}`);361    await adb.uninstallApk(UNICODE_IME_PKG_ID);362    await adb.install(unicodeIMEPath, {replace: false});363  }364  // get the default IME so we can return back to it later if we want365  let defaultIME = await adb.defaultIME();366  logger.debug(`Unsetting previous IME ${defaultIME}`);367  const appiumIME = `${UNICODE_IME_PKG_ID}/.UnicodeIME`;368  logger.debug(`Setting IME to '${appiumIME}'`);369  await adb.enableIME(appiumIME);370  await adb.setIME(appiumIME);371  return defaultIME;372};373helpers.setMockLocationApp = async function (adb, app) {374  try {375    if (await adb.getApiLevel() < 23) {376      await adb.shell(['settings', 'put', 'secure', 'mock_location', '1']);377    } else {378      await adb.shell(['appops', 'set', app, 'android:mock_location', 'allow']);379    }380  } catch (err) {381    logger.warn(`Unable to set mock location for app '${app}': ${err.message}`);382  }383};384helpers.installHelperApp = async function (adb, apkPath, packageId, appName) {385  try {386    await adb.installOrUpgrade(apkPath, packageId, {grantPermissions: true});387  } catch (err) {388    logger.warn(`Ignored error while installing Appium ${appName} helper: ` +389                `'${err.message}'. Manually uninstalling the application ` +390                `with package id '${packageId}' may help. Expect some Appium ` +391                `features may not work as expected unless this problem is ` +392                `fixed.`);393  }394};395helpers.pushSettingsApp = async function (adb, throwError = false) {396  logger.debug("Pushing settings apk to device...");397  await helpers.installHelperApp(adb, settingsApkPath, SETTINGS_HELPER_PKG_ID, 'Settings');398  // Reinstall will stop the settings helper process anyway, so399  // there is no need to continue if the application is still running400  if (await adb.processExists(SETTINGS_HELPER_PKG_ID)) {401    logger.debug(`${SETTINGS_HELPER_PKG_ID} is already running. ` +402                 `There is no need to reset its permissions.`);403    return;404  }405  // lauch io.appium.settings app due to settings failing to be set406  // if the app is not launched prior to start the session on android 7+407  // see https://github.com/appium/appium/issues/8957408  try {409    await adb.startApp({410      pkg: SETTINGS_HELPER_PKG_ID,411      activity: SETTINGS_HELPER_PKG_ACTIVITY,412      action: "android.intent.action.MAIN",413      category: "android.intent.category.LAUNCHER",414      flags: "0x10200000",415      stopApp: false,416    });417  } catch (err) {418    logger.warn(`Failed to launch settings app: ${err.message}`);419    if (throwError) {420      throw err;421    }422  }423};424helpers.pushUnlock = async function (adb) {425  logger.debug("Pushing unlock helper app to device...");426  await helpers.installHelperApp(adb, unlockApkPath, UNLOCK_HELPER_PKG_ID, 'Unlock');427};428/**429 * Extracts string.xml and converts it to string.json and pushes430 * it to /data/local/tmp/string.json on for use of bootstrap431 * If app is not present to extract string.xml it deletes remote strings.json432 * If app does not have strings.xml we push an empty json object to remote433 *434 * @param {?string} language - Language abbreviation, for example 'fr'. The default language435 * is used if this argument is not defined.436 * @param {Object} adb - The adb mofdule instance.437 * @param {Object} opts - Driver options dictionary.438 * @returns {Object} The dictionary, where string resourtces identifiers are keys439 * along with their corresponding values for the given language or an empty object440 * if no matching resources were extracted.441 */442helpers.pushStrings = async function (language, adb, opts) {443  const remoteDir = '/data/local/tmp';444  const stringsJson = 'strings.json';445  const remoteFile = `${remoteDir}/${stringsJson}`;446  // clean up remote string.json if present447  await adb.rimraf(remoteFile);448  if (_.isEmpty(opts.appPackage) || !(await fs.exists(opts.app))) {449    return {};450  }451  const stringsTmpDir = path.resolve(opts.tmpDir, opts.appPackage);452  try {453    logger.debug('Extracting strings from apk', opts.app, language, stringsTmpDir);454    const {apkStrings, localPath} = await adb.extractStringsFromApk(opts.app, language, stringsTmpDir);455    await adb.push(localPath, remoteDir);456    return apkStrings;457  } catch (err) {458    logger.warn(`Could not get strings, continuing anyway. Original error: ${err.message}`);459    await adb.shell('echo', [`'{}' > ${remoteFile}`]);460  } finally {461    await fs.rimraf(stringsTmpDir);462  }463  return {};464};465helpers.unlockWithUIAutomation = async function (driver, adb, unlockCapabilities) {466  let unlockType = unlockCapabilities.unlockType;467  if (!unlocker.isValidUnlockType(unlockType)) {468    throw new Error(`Invalid unlock type ${unlockType}`);469  }470  let unlockKey = unlockCapabilities.unlockKey;471  if (!unlocker.isValidKey(unlockType, unlockKey)) {472    throw new Error(`Missing unlockKey ${unlockKey} capability for unlockType ${unlockType}`);473  }474  const unlockMethod = {475    [PIN_UNLOCK]: unlocker.pinUnlock,476    [PASSWORD_UNLOCK]: unlocker.passwordUnlock,477    [PATTERN_UNLOCK]: unlocker.patternUnlock,478    [FINGERPRINT_UNLOCK]: unlocker.fingerprintUnlock479  }[unlockType];480  await unlockMethod(adb, driver, unlockCapabilities);481};482helpers.unlockWithHelperApp = async function (adb) {483  logger.info("Unlocking screen");484  try {485    await adb.forceStop(UNLOCK_HELPER_PKG_ID);486  } catch (e) {487    // Sometimes we can see the below error, but we can ignore it.488    // [W3C] Encountered internal error running command: Error: Error executing adbExec. Original error: 'Command 'adb -P 5037 -s emulator-5554 shell am force-stop io.appium.unlock' timed out after 20000ms'; Stderr: ''; Code: 'null'489    logger.warn(`An error in unlockWithHelperApp: ${e.message}`);490  }491  let startOpts = {492    pkg: UNLOCK_HELPER_PKG_ID,493    activity: UNLOCK_HELPER_PKG_ACTIVITY,494    action: "android.intent.action.MAIN",495    category: "android.intent.category.LAUNCHER",496    flags: "0x10200000",497    stopApp: false,498    retry: false,499    waitDuration: 1000500  };501  // Unlock succeed with a couple of retries.502  let firstRun = true;503  await retry(3, async function () {504    // To reduce a time to call adb.isScreenLocked() since `adb shell dumpsys window` is easy to hang adb commands505    if (firstRun) {506      firstRun = false;507    } else {508      try {509        if (!(await adb.isScreenLocked())) {510          return;511        }512      } catch (e) {513        logger.warn(`Error in isScreenLocked: ${e.message}`);514        logger.warn("\"adb shell dumpsys window\" command has timed out.");515        logger.warn("The reason of this timeout is the delayed adb response. Resetting adb server can improve it.");516      }517    }518    logger.info(`Launching ${UNLOCK_HELPER_PKG_ID}`);519    // The command takes too much time so we should not call the command over twice continuously.520    await adb.startApp(startOpts);521  });522};523helpers.unlock = async function (driver, adb, capabilities) {524  if (!(await adb.isScreenLocked())) {525    logger.info("Screen already unlocked, doing nothing");526    return;527  }528  logger.debug("Screen is locked, trying to unlock");529  if (_.isUndefined(capabilities.unlockType)) {530    logger.warn("Using app unlock, this is going to be deprecated!");531    await helpers.unlockWithHelperApp(adb);532  } else {533    await helpers.unlockWithUIAutomation(driver, adb, {unlockType: capabilities.unlockType, unlockKey: capabilities.unlockKey});534    await helpers.verifyUnlock(adb);535  }536};537helpers.verifyUnlock = async function (adb) {538  await retryInterval(2, 1000, async () => {539    if (await adb.isScreenLocked()) {540      throw new Error("Screen did not unlock successfully, retrying");541    }542    logger.debug("Screen unlocked successfully");543  });544};545helpers.initDevice = async function (adb, opts) {546  await adb.waitForDevice();547  // pushSettingsApp required before calling ensureDeviceLocale for API Level 24+548  await helpers.pushSettingsApp(adb);549  if (!opts.avd) {550    await helpers.setMockLocationApp(adb, SETTINGS_HELPER_PKG_ID);551  }552  await helpers.ensureDeviceLocale(adb, opts.language, opts.locale);553  await adb.startLogcat();554  let defaultIME;555  if (opts.unicodeKeyboard) {556    defaultIME = await helpers.initUnicodeKeyboard(adb);557  }558  if (_.isUndefined(opts.unlockType)) {559    await helpers.pushUnlock(adb);560  }561  return defaultIME;562};563helpers.removeNullProperties = function (obj) {564  for (let key of _.keys(obj)) {565    if (_.isNull(obj[key]) || _.isUndefined(obj[key])) {566      delete obj[key];567    }568  }569};570helpers.truncateDecimals = function (number, digits) {571  let multiplier = Math.pow(10, digits),572      adjustedNum = number * multiplier,573      truncatedNum = Math[adjustedNum < 0 ? 'ceil' : 'floor'](adjustedNum);574  return truncatedNum / multiplier;575};576helpers.isChromeBrowser = function (browser) {577  return _.includes(Object.keys(CHROME_BROWSER_PACKAGE_ACTIVITY), (browser || '').toLowerCase());578};579helpers.getChromePkg = function (browser) {580  return CHROME_BROWSER_PACKAGE_ACTIVITY[browser.toLowerCase()] ||581         CHROME_BROWSER_PACKAGE_ACTIVITY.default;582};583helpers.removeAllSessionWebSocketHandlers = async function (server, sessionId) {584  if (!server || !_.isFunction(server.getWebSocketHandlers)) {585    return;586  }587  const activeHandlers = await server.getWebSocketHandlers(sessionId);588  for (const pathname of _.keys(activeHandlers)) {589    await server.removeWebSocketHandler(pathname);590  }591};592/**593 * Takes a desired capability and tries to JSON.parse it as an array,594 * and either returns the parsed array or a singleton array.595 *596 * @param {any} cap A desired capability597 */598helpers.parseArray = function (cap) {599  let parsedCaps;600  try {601    parsedCaps = JSON.parse(cap);602  } catch (ign) { }603  if (_.isArray(parsedCaps)) {604    return parsedCaps;605  } else if (_.isString(cap)) {606    return [cap];607  }608  throw new Error(`must provide a string or JSON Array; received ${cap}`);609};610helpers.validateDesiredCaps = function (caps) {611  // make sure that the capabilities have one of `app`, `appPackage` or `browser`612  if ((!caps.browserName || !this.isChromeBrowser(caps.browserName)) && !caps.app && !caps.appPackage) {613    logger.errorAndThrow('The desired capabilities must include either an app, appPackage or browserName');614  }615  if (caps.browserName) {616    if (caps.app) {617      // warn if the capabilities have both `app` and `browser, although this is common with selenium grid618      logger.warn('The desired capabilities should generally not include both an app and a browserName');619    }620    if (caps.appPackage) {621      logger.errorAndThrow(`The desired capabilities must include either 'appPackage' or 'browserName'`);622    }623  }624  return true;625};626helpers.bootstrap = Bootstrap;627helpers.unlocker = unlocker;...

Full Screen

Full Screen

android-helpers.js

Source:android-helpers.js Github

copy

Full Screen

1import _ from 'lodash';2import path from 'path';3import { exec } from 'teen_process';4import { retry, retryInterval } from 'asyncbox';5import logger from './logger';6import { fs } from 'appium-support';7import { path as unicodeIMEPath } from 'appium-android-ime';8import { path as settingsApkPath } from 'io.appium.settings';9import { path as unlockApkPath } from 'appium-unlock';10import Bootstrap from 'appium-android-bootstrap';11import B from 'bluebird';12import ADB from 'appium-adb';13import { default as unlocker, PIN_UNLOCK, PASSWORD_UNLOCK, PATTERN_UNLOCK, FINGERPRINT_UNLOCK } from './unlock-helpers';14const PACKAGE_INSTALL_TIMEOUT = 90000; // milliseconds15const CHROME_BROWSER_PACKAGE_ACTIVITY = {16  chrome: {17    pkg: 'com.android.chrome',18    activity: 'com.google.android.apps.chrome.Main',19  },20  chromium: {21    pkg: 'org.chromium.chrome.shell',22    activity: '.ChromeShellActivity',23  },24  chromebeta: {25    pkg: 'com.chrome.beta',26    activity: 'com.google.android.apps.chrome.Main',27  },28  browser: {29    pkg: 'com.android.browser',30    activity: 'com.android.browser.BrowserActivity',31  },32  'chromium-browser': {33    pkg: 'org.chromium.chrome',34    activity: 'com.google.android.apps.chrome.Main',35  },36  'chromium-webview': {37    pkg: 'org.chromium.webview_shell',38    activity: 'org.chromium.webview_shell.WebViewBrowserActivity',39  },40  default: {41    pkg: 'com.android.chrome',42    activity: 'com.google.android.apps.chrome.Main',43  },44};45const SETTINGS_HELPER_PKG_ID = 'io.appium.settings';46const SETTINGS_HELPER_PKG_ACTIVITY = ".Settings";47const UNLOCK_HELPER_PKG_ID = 'io.appium.unlock';48const UNLOCK_HELPER_PKG_ACTIVITY = ".Unlock";49let helpers = {};50helpers.parseJavaVersion = function (stderr) {51  let lines = stderr.split("\n");52  for (let line of lines) {53    if (new RegExp(/(java|openjdk) version/).test(line)) {54      return line.split(" ")[2].replace(/"/g, '');55    }56  }57  return null;58};59helpers.getJavaVersion = async function () {60  logger.debug("Getting Java version");61  let {stderr} = await exec('java', ['-version']);62  let javaVer = helpers.parseJavaVersion(stderr);63  if (javaVer === null) {64    throw new Error("Could not get the Java version. Is Java installed?");65  }66  logger.info(`Java version is: ${javaVer}`);67  return javaVer;68};69helpers.prepareEmulator = async function (adb, opts) {70  let {avd, avdArgs, language, locale, avdLaunchTimeout,71       avdReadyTimeout} = opts;72  if (!avd) {73    throw new Error("Cannot launch AVD without AVD name");74  }75  let avdName = avd.replace('@', '');76  let runningAVD = await adb.getRunningAVD(avdName);77  if (runningAVD !== null) {78    if (avdArgs && avdArgs.toLowerCase().indexOf("-wipe-data") > -1) {79      logger.debug(`Killing '${avdName}' because it needs to be wiped at start.`);80      await adb.killEmulator(avdName);81    } else {82      logger.debug("Not launching AVD because it is already running.");83      return;84    }85  }86  avdArgs = this.prepareAVDArgs(opts, adb, avdArgs);87  await adb.launchAVD(avd, avdArgs, language, locale, avdLaunchTimeout,88                      avdReadyTimeout);89};90helpers.prepareAVDArgs = function (opts, adb, avdArgs) {91  let args = avdArgs ? [avdArgs] : [];92  if (!_.isUndefined(opts.networkSpeed)) {93    let networkSpeed = this.ensureNetworkSpeed(adb, opts.networkSpeed);94    args.push('-netspeed', networkSpeed);95  }96  if (opts.isHeadless) {97    args.push('-no-window');98  }99  return args.join(' ');100};101helpers.ensureNetworkSpeed = function (adb, networkSpeed) {102  if (_.values(adb.NETWORK_SPEED).indexOf(networkSpeed) !== -1) {103    return networkSpeed;104  }105  logger.warn(`Wrong network speed param ${networkSpeed}, using default: full. Supported values: ${_.values(adb.NETWORK_SPEED)}`);106  return adb.NETWORK_SPEED.FULL;107};108helpers.ensureDeviceLocale = async function (adb, language, country) {109  if (!_.isString(language) && !_.isString(country)) {110    logger.warn(`setDeviceLanguageCountry requires language or country.`);111    logger.warn(`Got language: '${language}' and country: '${country}'`);112    return;113  }114  await adb.setDeviceLanguageCountry(language, country);115  if (!await adb.ensureCurrentLocale(language, country)) {116    throw new Error(`Failed to set language: ${language} and country: ${country}`);117  }118};119helpers.getDeviceInfoFromCaps = async function (opts = {}) {120  // we can create a throwaway ADB instance here, so there is no dependency121  // on instantiating on earlier (at this point, we have no udid)122  // we can only use this ADB object for commands that would not be confused123  // if multiple devices are connected124  let adb = await ADB.createADB({125    javaVersion: opts.javaVersion,126    adbPort: opts.adbPort,127    remoteAdbHost: opts.remoteAdbHost,128    suppressKillServer: opts.suppressKillServer,129    clearDeviceLogsOnStart: opts.clearDeviceLogsOnStart,130  });131  let udid = opts.udid;132  let emPort = null;133  // a specific avd name was given. try to initialize with that134  if (opts.avd) {135    await helpers.prepareEmulator(adb, opts);136    udid = adb.curDeviceId;137    emPort = adb.emulatorPort;138  } else {139    // no avd given. lets try whatever's plugged in devices/emulators140    logger.info("Retrieving device list");141    let devices = await adb.getDevicesWithRetry();142    // udid was given, lets try to init with that device143    if (udid) {144      if (!_.includes(_.map(devices, 'udid'), udid)) {145        logger.errorAndThrow(`Device ${udid} was not in the list ` +146                             `of connected devices`);147      }148      emPort = adb.getPortFromEmulatorString(udid);149    } else if (opts.platformVersion) {150      opts.platformVersion = `${opts.platformVersion}`.trim();151      // a platform version was given. lets try to find a device with the same os152      logger.info(`Looking for a device with Android '${opts.platformVersion}'`);153      // in case we fail to find something, give the user a useful log that has154      // the device udids and os versions so they know what's available155      let availDevicesStr = [];156      // first try started devices/emulators157      for (let device of devices) {158        // direct adb calls to the specific device159        await adb.setDeviceId(device.udid);160        let deviceOS = await adb.getPlatformVersion();161        // build up our info string of available devices as we iterate162        availDevicesStr.push(`${device.udid} (${deviceOS})`);163        // we do a begins with check for implied wildcard matching164        // eg: 4 matches 4.1, 4.0, 4.1.3-samsung, etc165        if (deviceOS.indexOf(opts.platformVersion) === 0) {166          udid = device.udid;167          break;168        }169      }170      // we couldn't find anything! quit171      if (!udid) {172        logger.errorAndThrow(`Unable to find an active device or emulator ` +173                             `with OS ${opts.platformVersion}. The following ` +174                             `are available: ` + availDevicesStr.join(', '));175      }176      emPort = adb.getPortFromEmulatorString(udid);177    } else {178      // a udid was not given, grab the first device we see179      udid = devices[0].udid;180      emPort = adb.getPortFromEmulatorString(udid);181    }182  }183  logger.info(`Using device: ${udid}`);184  return {udid, emPort};185};186// returns a new adb instance with deviceId set187helpers.createADB = async function (javaVersion, udid, emPort, adbPort, suppressKillServer, remoteAdbHost, clearDeviceLogsOnStart) {188  let adb = await ADB.createADB({189    javaVersion,190    adbPort,191    suppressKillServer,192    remoteAdbHost,193    clearDeviceLogsOnStart,194  });195  adb.setDeviceId(udid);196  if (emPort) {197    adb.setEmulatorPort(emPort);198  }199  return adb;200};201helpers.getLaunchInfo = async function (adb, opts) {202  let {app, appPackage, appActivity, appWaitPackage, appWaitActivity} = opts;203  if (!app) {204    logger.warn("No app sent in, not parsing package/activity");205    return;206  }207  if (appPackage && appActivity) {208    return;209  }210  logger.debug("Parsing package and activity from app manifest");211  let {apkPackage, apkActivity} =212    await adb.packageAndLaunchActivityFromManifest(app);213  if (apkPackage && !appPackage) {214    appPackage = apkPackage;215  }216  if (!appWaitPackage) {217    appWaitPackage = appPackage;218  }219  if (apkActivity && !appActivity) {220    appActivity = apkActivity;221  }222  if (!appWaitActivity) {223    appWaitActivity = appActivity;224  }225  logger.debug(`Parsed package and activity are: ${apkPackage}/${apkActivity}`);226  return {appPackage, appWaitPackage, appActivity, appWaitActivity};227};228helpers.resetApp = async function (adb, opts = {}) {229  const {app, appPackage, fastReset, fullReset,230    androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT,231    autoGrantPermissions} = opts;232  if (!appPackage) {233    throw new Error("'appPackage' option is required");234  }235  const isInstalled = await adb.isAppInstalled(appPackage);236  if (isInstalled) {237    try {238      await adb.forceStop(appPackage);239    } catch (ign) {}240    // fullReset has priority over fastReset241    if (!fullReset && fastReset) {242      const output = await adb.clear(appPackage);243      if (_.isString(output) && output.toLowerCase().includes('failed')) {244        throw new Error(`Cannot clear the application data of '${appPackage}'. Original error: ${output}`);245      }246      // executing `shell pm clear` resets previously assigned application permissions as well247      if (autoGrantPermissions) {248        try {249          await adb.grantAllPermissions(appPackage);250        } catch (error) {251          logger.error(`Unable to grant permissions requested. Original error: ${error.message}`);252        }253      }254      logger.debug(`Performed fast reset on the installed '${appPackage}' application (stop and clear)`);255      return;256    }257  }258  if (!app) {259    throw new Error("'app' option is required for reinstall");260  }261  logger.debug(`Running full reset on '${appPackage}' (reinstall)`);262  if (isInstalled) {263    await adb.uninstallApk(appPackage);264  }265  await adb.install(app, {266    grantPermissions: autoGrantPermissions,267    timeout: androidInstallTimeout268  });269};270helpers.installApk = async function (adb, opts = {}) {271  const {app, appPackage, fastReset, fullReset,272    androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT,273    autoGrantPermissions} = opts;274  if (!app || !appPackage) {275    throw new Error("'app' and 'appPackage' options are required");276  }277  if (fullReset) {278    await this.resetApp(adb, opts);279    return;280  }281  // There is no need to reset the newly installed app282  const shouldPerformFastReset = fastReset && await adb.isAppInstalled(appPackage);283  await adb.installOrUpgrade(app, appPackage, {284    grantPermissions: autoGrantPermissions,285    timeout: androidInstallTimeout286  });287  if (shouldPerformFastReset) {288    logger.info(`Performing fast reset on '${appPackage}'`);289    await this.resetApp(adb, opts);290  }291};292/**293 * Installs an array of apks294 * @param {ADB} adb Instance of Appium ADB object295 * @param {Object} opts Opts defined in driver.js296 */297helpers.installOtherApks = async function (otherApps, adb, opts) {298  let {299    androidInstallTimeout = PACKAGE_INSTALL_TIMEOUT,300    autoGrantPermissions301  } = opts;302  // Install all of the APK's asynchronously303  await B.all(otherApps.map((otherApp) => {304    logger.debug(`Installing app: ${otherApp}`);305    return adb.installOrUpgrade(otherApp, null, {306      grantPermissions: autoGrantPermissions,307      timeout: androidInstallTimeout,308    });309  }));310};311helpers.initUnicodeKeyboard = async function (adb) {312  logger.debug('Enabling Unicode keyboard support');313  logger.debug("Pushing unicode ime to device...");314  // await adb.install(unicodeIMEPath, {replace: false});315  // get the default IME so we can return back to it later if we want316  let defaultIME = await adb.defaultIME();317  logger.debug(`Unsetting previous IME ${defaultIME}`);318  const appiumIME = 'io.appium.android.ime/.UnicodeIME';319  logger.debug(`Setting IME to '${appiumIME}'`);320  await adb.enableIME(appiumIME);321  await adb.setIME(appiumIME);322  return defaultIME;323};324helpers.setMockLocationApp = async function (adb, app) {325  try {326    if (await adb.getApiLevel() < 23) {327      await adb.shell(['settings', 'put', 'secure', 'mock_location', '1']);328    } else {329      await adb.shell(['appops', 'set', app, 'android:mock_location', 'allow']);330    }331  } catch (err) {332    logger.warn(`Unable to set mock location for app '${app}': ${err.message}`);333  }334};335helpers.installHelperApp = async function (adb, apkPath, packageId, appName) {336  try {337    await adb.installOrUpgrade(apkPath, packageId, {grantPermissions: true});338  } catch (err) {339    logger.warn(`Ignored error while installing Appium ${appName} helper: ` +340                `'${err.message}'. Manually uninstalling the application ` +341                `with package id '${packageId}' may help. Expect some Appium ` +342                `features may not work as expected unless this problem is ` +343                `fixed.`);344  }345};346helpers.pushSettingsApp = async function (adb, throwError = false) {347  logger.debug("Pushing settings apk to device...");348  await helpers.installHelperApp(adb, settingsApkPath, SETTINGS_HELPER_PKG_ID, 'Settings');349  // Reinstall will stop the settings helper process anyway, so350  // there is no need to continue if the application is still running351  if (await adb.processExists(SETTINGS_HELPER_PKG_ID)) {352    logger.debug(`${SETTINGS_HELPER_PKG_ID} is already running. ` +353                 `There is no need to reset its permissions.`);354    return;355  }356  // lauch io.appium.settings app due to settings failing to be set357  // if the app is not launched prior to start the session on android 7+358  // see https://github.com/appium/appium/issues/8957359  try {360    await adb.startApp({361      pkg: SETTINGS_HELPER_PKG_ID,362      activity: SETTINGS_HELPER_PKG_ACTIVITY,363      action: "android.intent.action.MAIN",364      category: "android.intent.category.LAUNCHER",365      flags: "0x10200000",366      stopApp: false,367    });368  } catch (err) {369    logger.warn(`Failed to launch settings app: ${err.message}`);370    if (throwError) {371      throw err;372    }373  }374};375helpers.pushUnlock = async function (adb) {376  logger.debug("Pushing unlock helper app to device...");377  await helpers.installHelperApp(adb, unlockApkPath, UNLOCK_HELPER_PKG_ID, 'Unlock');378};379/**380 * Extracts string.xml and converts it to string.json and pushes381 * it to /data/local/tmp/string.json on for use of bootstrap382 * If app is not present to extract string.xml it deletes remote strings.json383 * If app does not have strings.xml we push an empty json object to remote384 *385 * @param {?string} language - Language abbreviation, for example 'fr'. The default language386 * is used if this argument is not defined.387 * @param {Object} adb - The adb mofdule instance.388 * @param {Object} opts - Driver options dictionary.389 * @returns {Object} The dictionary, where string resourtces identifiers are keys390 * along with their corresponding values for the given language or an empty object391 * if no matching resources were extracted.392 */393helpers.pushStrings = async function (language, adb, opts) {394  const remoteDir = '/data/local/tmp';395  const stringsJson = 'strings.json';396  const remoteFile = `${remoteDir}/${stringsJson}`;397  // clean up remote string.json if present398  await adb.rimraf(remoteFile);399  if (_.isEmpty(opts.appPackage) || !(await fs.exists(opts.app))) {400    return {};401  }402  const stringsTmpDir = path.resolve(opts.tmpDir, opts.appPackage);403  try {404    logger.debug('Extracting strings from apk', opts.app, language, stringsTmpDir);405    const {apkStrings, localPath} = await adb.extractStringsFromApk(opts.app, language, stringsTmpDir);406    await adb.push(localPath, remoteDir);407    return apkStrings;408  } catch (err) {409    logger.warn(`Could not get strings, continuing anyway. Original error: ${err.message}`);410    await adb.shell('echo', [`'{}' > ${remoteFile}`]);411  } finally {412    await fs.rimraf(stringsTmpDir);413  }414  return {};415};416helpers.unlockWithUIAutomation = async function (driver, adb, unlockCapabilities) {417  let unlockType = unlockCapabilities.unlockType;418  if (!unlocker.isValidUnlockType(unlockType)) {419    throw new Error(`Invalid unlock type ${unlockType}`);420  }421  let unlockKey = unlockCapabilities.unlockKey;422  if (!unlocker.isValidKey(unlockType, unlockKey)) {423    throw new Error(`Missing unlockKey ${unlockKey} capability for unlockType ${unlockType}`);424  }425  const unlockMethod = {426    [PIN_UNLOCK]: unlocker.pinUnlock,427    [PASSWORD_UNLOCK]: unlocker.passwordUnlock,428    [PATTERN_UNLOCK]: unlocker.patternUnlock,429    [FINGERPRINT_UNLOCK]: unlocker.fingerprintUnlock430  }[unlockType];431  await unlockMethod(adb, driver, unlockCapabilities);432};433helpers.unlockWithHelperApp = async function (adb) {434  logger.info("Unlocking screen");435  try {436    await adb.forceStop(UNLOCK_HELPER_PKG_ID);437  } catch (e) {438    // Sometimes we can see the below error, but we can ignore it.439    // [W3C] Encountered internal error running command: Error: Error executing adbExec. Original error: 'Command 'adb -P 5037 -s emulator-5554 shell am force-stop io.appium.unlock' timed out after 20000ms'; Stderr: ''; Code: 'null'440    logger.warn(`An error in unlockWithHelperApp: ${e.message}`);441  }442  let startOpts = {443    pkg: UNLOCK_HELPER_PKG_ID,444    activity: UNLOCK_HELPER_PKG_ACTIVITY,445    action: "android.intent.action.MAIN",446    category: "android.intent.category.LAUNCHER",447    flags: "0x10200000",448    stopApp: false,449    retry: false,450    waitDuration: 1000451  };452  // Unlock succeed with a couple of retries.453  let firstRun = true;454  await retry(3, async function () {455    // To reduce a time to call adb.isScreenLocked() since `adb shell dumpsys window` is easy to hang adb commands456    if (firstRun) {457      firstRun = false;458    } else {459      try {460        if (!(await adb.isScreenLocked())) {461          return;462        }463      } catch (e) {464        logger.warn(`Error in isScreenLocked: ${e.message}`);465        logger.warn("\"adb shell dumpsys window\" command has timed out.");466        logger.warn("The reason of this timeout is the delayed adb response. Resetting adb server can improve it.");467      }468    }469    logger.info(`Launching ${UNLOCK_HELPER_PKG_ID}`);470    // The command takes too much time so we should not call the command over twice continuously.471    await adb.startApp(startOpts);472  });473};474helpers.unlock = async function (driver, adb, capabilities) {475  if (!(await adb.isScreenLocked())) {476    logger.info("Screen already unlocked, doing nothing");477    return;478  }479  logger.debug("Screen is locked, trying to unlock");480  if (_.isUndefined(capabilities.unlockType)) {481    logger.warn("Using app unlock, this is going to be deprecated!");482    await helpers.unlockWithHelperApp(adb);483  } else {484    await helpers.unlockWithUIAutomation(driver, adb, {unlockType: capabilities.unlockType, unlockKey: capabilities.unlockKey});485    await helpers.verifyUnlock(adb);486  }487};488helpers.verifyUnlock = async function (adb) {489  await retryInterval(2, 1000, async () => {490    if (await adb.isScreenLocked()) {491      throw new Error("Screen did not unlock successfully, retrying");492    }493    logger.debug("Screen unlocked successfully");494  });495};496helpers.initDevice = async function (adb, opts) {497  await adb.waitForDevice();498  if (!opts.avd) {499    // pushSettingsApp required before calling ensureDeviceLocale for API Level 24+500    // await helpers.pushSettingsApp(adb);501    await helpers.setMockLocationApp(adb, SETTINGS_HELPER_PKG_ID);502  }503  await helpers.ensureDeviceLocale(adb, opts.language, opts.locale);504  await adb.startLogcat();505  let defaultIME;506  if (opts.unicodeKeyboard) {507    defaultIME = await helpers.initUnicodeKeyboard(adb);508  }509  if (_.isUndefined(opts.unlockType)) {510    //await helpers.pushUnlock(adb);511  }512  return defaultIME;513};514helpers.removeNullProperties = function (obj) {515  for (let key of _.keys(obj)) {516    if (_.isNull(obj[key]) || _.isUndefined(obj[key])) {517      delete obj[key];518    }519  }520};521helpers.truncateDecimals = function (number, digits) {522  let multiplier = Math.pow(10, digits),523      adjustedNum = number * multiplier,524      truncatedNum = Math[adjustedNum < 0 ? 'ceil' : 'floor'](adjustedNum);525  return truncatedNum / multiplier;526};527helpers.isChromeBrowser = function (browser) {528  return _.includes(Object.keys(CHROME_BROWSER_PACKAGE_ACTIVITY), (browser || '').toLowerCase());529};530helpers.getChromePkg = function (browser) {531  return CHROME_BROWSER_PACKAGE_ACTIVITY[browser.toLowerCase()] ||532         CHROME_BROWSER_PACKAGE_ACTIVITY.default;533};534helpers.removeAllSessionWebSocketHandlers = async function (server, sessionId) {535  if (!server || !_.isFunction(server.getWebSocketHandlers)) {536    return;537  }538  const activeHandlers = await server.getWebSocketHandlers(sessionId);539  for (const pathname of _.keys(activeHandlers)) {540    await server.removeWebSocketHandler(pathname);541  }542};543/**544 * Takes a desired capability and tries to JSON.parse it as an array,545 * and either returns the parsed array or a singleton array.546 *547 * @param {any} cap A desired capability548 */549helpers.parseArray = function (cap) {550  let parsedCaps;551  try {552    parsedCaps = JSON.parse(cap);553  } catch (ign) { }554  if (_.isArray(parsedCaps)) {555    return parsedCaps;556  } else if (_.isString(cap)) {557    return [cap];558  }559  throw new Error(`must provide a string or JSON Array; received ${cap}`);560};561helpers.bootstrap = Bootstrap;562helpers.unlocker = unlocker;...

Full Screen

Full Screen

driver-specs.js

Source:driver-specs.js Github

copy

Full Screen

1import chai from 'chai';2import chaiAsPromised from 'chai-as-promised';3import log from '../../lib/logger';4import sinon from 'sinon';5import helpers from '../../lib/android-helpers';6import { withMocks } from 'appium-test-support';7import AndroidDriver from '../..';8import ADB from 'appium-adb';9import { errors } from 'appium-base-driver';10import { fs } from 'appium-support';11import { SharedPrefsBuilder } from 'shared-preferences-builder';12let driver;13let sandbox = sinon.sandbox.create();14let expect = chai.expect;15chai.should();16chai.use(chaiAsPromised);17describe('driver', () => {18  describe('constructor', () => {19    it('should call BaseDriver constructor with opts', () => {20      let driver = new AndroidDriver({foo: 'bar'});21      driver.should.exist;22      driver.opts.foo.should.equal('bar');23    });24    it('should have this.findElOrEls', () => {25      let driver = new AndroidDriver({foo: 'bar'});26      driver.findElOrEls.should.exist;27      driver.findElOrEls.should.be.a('function');28    });29  });30  describe('emulator methods', () => {31    describe('fingerprint', () => {32      it('should be rejected if isEmulator is false', () => {33        let driver = new AndroidDriver();34        sandbox.stub(driver, 'isEmulator').returns(false);35        driver.fingerprint(1111).should.eventually.be.rejectedWith("fingerprint method is only available for emulators");36        driver.isEmulator.calledOnce.should.be.true;37      });38    });39    describe('sendSMS', () => {40      it('sendSMS should be rejected if isEmulator is false', () => {41        let driver = new AndroidDriver();42        sandbox.stub(driver, 'isEmulator').returns(false);43        driver.sendSMS(4509, "Hello Appium").should.eventually.be.rejectedWith("sendSMS method is only available for emulators");44        driver.isEmulator.calledOnce.should.be.true;45      });46    });47  });48  describe('sharedPreferences', () => {49    driver = new AndroidDriver();50    let adb = new ADB();51    driver.adb = adb;52    let builder = new SharedPrefsBuilder();53    describe('should skip setting sharedPreferences', withMocks({driver}, (mocks) => {54      it('on undefined name', async () => {55        driver.opts.sharedPreferences = {};56        (await driver.setSharedPreferences()).should.be.false;57        mocks.driver.verify();58      });59    }));60    describe('should set sharedPreferences', withMocks({driver, adb, builder, fs}, (mocks) => {61      it('on defined sharedPreferences object', async () => {62        driver.opts.appPackage = 'io.appium.test';63        driver.opts.sharedPreferences = {64          name: 'com.appium.prefs',65          prefs: [{type: 'string', name: 'mystr', value:'appium rocks!'}]66        };67        mocks.driver.expects('getPrefsBuilder').once().returns(builder);68        mocks.builder.expects('build').once();69        mocks.builder.expects('toFile').once();70        mocks.adb.expects('shell').once()71          .withExactArgs(['mkdir', '-p', '/data/data/io.appium.test/shared_prefs']);72        mocks.adb.expects('push').once()73          .withExactArgs('/tmp/com.appium.prefs.xml', '/data/data/io.appium.test/shared_prefs/com.appium.prefs.xml');74        mocks.fs.expects('exists').once()75          .withExactArgs('/tmp/com.appium.prefs.xml')76          .returns(true);77        mocks.fs.expects('unlink').once()78          .withExactArgs('/tmp/com.appium.prefs.xml');79        await driver.setSharedPreferences();80        mocks.driver.verify();81        mocks.adb.verify();82        mocks.builder.verify();83        mocks.fs.verify();84      });85    }));86  });87  describe('createSession', () => {88    beforeEach(() => {89      driver = new AndroidDriver();90      sandbox.stub(driver, 'checkAppPresent');91      sandbox.stub(driver, 'checkPackagePresent');92      sandbox.stub(driver, 'startAndroidSession');93      sandbox.stub(ADB, 'createADB', async (opts) => {94        return {95          getDevicesWithRetry: async () => {96            return [97              {udid: 'emulator-1234'},98              {udid: 'rotalume-1337'}99            ];100          },101          getPortFromEmulatorString: () => {102            return 1234;103          },104          setDeviceId: () => {},105          setEmulatorPort: () => {},106          adbPort: opts.adbPort,107          networkSpeed: () => {}108        };109      });110    });111    afterEach(() => {112      sandbox.restore();113    });114    it('should verify device is an emulator', async () => {115      driver.opts.avd = "Nexus_5X_Api_23";116      driver.isEmulator().should.equal(true);117      driver.opts.avd = undefined;118      driver.opts.udid = "emulator-5554";119      driver.isEmulator().should.equal(true);120      driver.opts.udid = "01234567889";121      driver.isEmulator().should.equal(false);122    });123    it('should get java version if none is provided', async () => {124      await driver.createSession({platformName: 'Android', deviceName: 'device', app: '/path/to/some.apk'});125      driver.opts.javaVersion.should.exist;126    });127    it('should get browser package details if browserName is provided', async () => {128      sandbox.spy(helpers, 'getChromePkg');129      await driver.createSession({platformName: 'Android', deviceName: 'device', browserName: 'Chrome'});130      helpers.getChromePkg.calledOnce.should.be.true;131    });132    it('should check an app is present', async () => {133      await driver.createSession({platformName: 'Android', deviceName: 'device', app: '/path/to/some.apk'});134      driver.checkAppPresent.calledOnce.should.be.true;135    });136    it('should check a package is present', async () => {137      await driver.createSession({platformName: 'Android', deviceName: 'device', appPackage: 'some.app.package'});138      driver.checkPackagePresent.calledOnce.should.be.true;139    });140    it('should accept a package via the app capability', async () => {141      await driver.createSession({platformName: 'Android', deviceName: 'device', app: 'some.app.package'});142      driver.checkPackagePresent.calledOnce.should.be.true;143    });144    it('should add server details to caps', async () => {145      await driver.createSession({platformName: 'Android', deviceName: 'device', appPackage: 'some.app.package'});146      driver.caps.webStorageEnabled.should.exist;147    });148    it('should delete a session on failure', async () => {149      // Force an error to make sure deleteSession gets called150      sandbox.stub(helpers, 'getJavaVersion').throws();151      sandbox.stub(driver, 'deleteSession');152      try {153        await driver.createSession({platformName: 'Android', deviceName: 'device', appPackage: 'some.app.package'});154      } catch (ign) {}155      driver.deleteSession.calledOnce.should.be.true;156    });157    it('should pass along adbPort capability to ADB', async () => {158      await driver.createSession({platformName: 'Android', deviceName: 'device', appPackage: 'some.app.package', adbPort: 1111});159      driver.adb.adbPort.should.equal(1111);160    });161    it('should proxy screenshot if nativeWebScreenshot is off', async () => {162      await driver.createSession({platformName: 'Android', deviceName: 'device', browserName: 'chrome', nativeWebScreenshot: false});163      driver.getProxyAvoidList().should.have.length(8);164    });165    it('should not proxy screenshot if nativeWebScreenshot is on', async () => {166      await driver.createSession({platformName: 'Android', deviceName: 'device', browserName: 'chrome', nativeWebScreenshot: true});167      driver.getProxyAvoidList().should.have.length(9);168    });169    it('should set networkSpeed before launching app', async () => {170      sandbox.stub(driver, 'isEmulator').returns(true);171      sandbox.stub(helpers, 'ensureNetworkSpeed').returns('full');172      await driver.createSession({platformName: 'Android', deviceName: 'device', appPackage: 'some.app.package', networkSpeed: 'edge'});173      driver.isEmulator.calledOnce.should.be.true;174      helpers.ensureNetworkSpeed.calledOnce.should.be.true;175    });176  });177  describe('deleteSession', () => {178    beforeEach(async () => {179      driver = new AndroidDriver();180      driver.adb = new ADB();181      driver.bootstrap = new helpers.bootstrap(driver.adb);182      sandbox.stub(driver, 'stopChromedriverProxies');183      sandbox.stub(driver.adb, 'setIME');184      sandbox.stub(driver.adb, 'forceStop');185      sandbox.stub(driver.adb, 'goToHome');186      sandbox.stub(driver.adb, 'uninstallApk');187      sandbox.stub(driver.adb, 'stopLogcat');188      sandbox.stub(driver.bootstrap, 'shutdown');189      sandbox.spy(log, 'debug');190    });191    afterEach(() => {192      sandbox.restore();193    });194    it('should not do anything if Android Driver has already shut down', async () => {195      driver.bootstrap = null;196      await driver.deleteSession();197      log.debug.callCount.should.eql(3);198      driver.stopChromedriverProxies.called.should.be.false;199      driver.adb.stopLogcat.called.should.be.true;200    });201    it('should reset keyboard to default IME', async () => {202      driver.opts.unicodeKeyboard = true;203      driver.opts.resetKeyboard = true;204      driver.defaultIME = 'someDefaultIME';205      await driver.deleteSession();206      driver.adb.setIME.calledOnce.should.be.true;207    });208    it('should force stop non-Chrome sessions', async () => {209      await driver.deleteSession();210      driver.adb.forceStop.calledOnce.should.be.true;211    });212    it('should uninstall APK if required', async () => {213      driver.opts.fullReset = true;214      await driver.deleteSession();215      driver.adb.uninstallApk.calledOnce.should.be.true;216    });217  });218  describe('dismissChromeWelcome', () => {219    before(async () => {220      driver = new AndroidDriver();221    });222    it('should verify chromeOptions args', () => {223      driver.opts = {};224      driver.shouldDismissChromeWelcome().should.be.false;225      driver.opts = {chromeOptions: {}};226      driver.shouldDismissChromeWelcome().should.be.false;227      driver.opts = {chromeOptions: {args: []}};228      driver.shouldDismissChromeWelcome().should.be.false;229      driver.opts = {chromeOptions: {args: "--no-first-run"}};230      driver.shouldDismissChromeWelcome().should.be.false;231      driver.opts = {chromeOptions: {args: ["--disable-dinosaur-easter-egg"]}};232      driver.shouldDismissChromeWelcome().should.be.false;233      driver.opts = {chromeOptions: {args: ["--no-first-run"]}};234      driver.shouldDismissChromeWelcome().should.be.true;235    });236  });237  describe('initAUT', withMocks({helpers}, (mocks) => {238    beforeEach(async () => {239      driver = new AndroidDriver();240      driver.caps = {};241    });242    it('should throw error if run with full reset', async () => {243      driver.opts = {appPackage: "app.package", appActivity: "act", fullReset: true};244      await driver.initAUT().should.be.rejectedWith(/Full reset requires an app capability/);245    });246    it('should reset if run with fast reset', async () => {247      driver.opts = {appPackage: "app.package", appActivity: "act", fullReset: false, fastReset: true};248      driver.adb = "mock_adb";249      mocks.helpers.expects("resetApp").withExactArgs("mock_adb", undefined, "app.package", true);250      await driver.initAUT();251      mocks.helpers.verify();252    });253    it('should keep data if run without reset', async () => {254      driver.opts = {appPackage: "app.package", appActivity: "act", fullReset: false, fastReset: false};255      mocks.helpers.expects("resetApp").never();256      await driver.initAUT();257      mocks.helpers.verify();258    });259  }));260  describe('startAndroidSession', () => {261    beforeEach(async () => {262      driver = new AndroidDriver();263      driver.adb = new ADB();264      driver.bootstrap = new helpers.bootstrap(driver.adb);265      driver.settings = { update () {} };266      driver.caps = {};267      // create a fake bootstrap because we can't mock268      // driver.bootstrap.<whatever> in advance269      let fakeBootstrap = {start () {},270                           onUnexpectedShutdown: {catch () {}}271                          };272      sandbox.stub(helpers, 'initDevice');273      sandbox.stub(helpers, 'unlock');274      sandbox.stub(helpers, 'bootstrap').returns(fakeBootstrap);275      sandbox.stub(driver, 'initAUT');276      sandbox.stub(driver, 'startAUT');277      sandbox.stub(driver, 'defaultWebviewName');278      sandbox.stub(driver, 'setContext');279      sandbox.stub(driver, 'startChromeSession');280      sandbox.stub(driver, 'dismissChromeWelcome');281      sandbox.stub(driver.settings, 'update');282      sandbox.stub(driver.adb, 'getPlatformVersion');283      sandbox.stub(driver.adb, 'getScreenSize');284      sandbox.stub(driver.adb, 'getModel');285      sandbox.stub(driver.adb, 'getManufacturer');286    });287    afterEach(() => {288      sandbox.restore();289    });290    it('should set actual platform version', async () => {291      await driver.startAndroidSession();292      driver.adb.getPlatformVersion.calledOnce.should.be.true;293    });294    it('should auto launch app if it is on the device', async () => {295      driver.opts.autoLaunch = true;296      await driver.startAndroidSession();297      driver.initAUT.calledOnce.should.be.true;298    });299    it('should handle chrome sessions', async () => {300      driver.opts.browserName = 'Chrome';301      await driver.startAndroidSession();302      driver.startChromeSession.calledOnce.should.be.true;303    });304    it('should unlock the device', async () => {305      await driver.startAndroidSession();306      helpers.unlock.calledOnce.should.be.true;307    });308    it('should start AUT if auto lauching', async () => {309      driver.opts.autoLaunch = true;310      await driver.startAndroidSession();311      driver.initAUT.calledOnce.should.be.true;312    });313    it('should not start AUT if not auto lauching', async () => {314      driver.opts.autoLaunch = false;315      await driver.startAndroidSession();316      driver.initAUT.calledOnce.should.be.false;317    });318    it('should set the context if autoWebview is requested', async () => {319      driver.opts.autoWebview = true;320      await driver.startAndroidSession();321      driver.defaultWebviewName.calledOnce.should.be.true;322      driver.setContext.calledOnce.should.be.true;323    });324    it('should set the context if autoWebview is requested using timeout', async () => {325      driver.setContext.onCall(0).throws(errors.NoSuchContextError);326      driver.setContext.onCall(1).returns();327      driver.opts.autoWebview = true;328      driver.opts.autoWebviewTimeout = 5000;329      await driver.startAndroidSession();330      driver.defaultWebviewName.calledOnce.should.be.true;331      driver.setContext.calledTwice.should.be.true;332    });333    it('should respect timeout if autoWebview is requested', async function () {334      this.timeout(10000);335      driver.setContext.throws(new errors.NoSuchContextError());336      let begin = Date.now();337      driver.opts.autoWebview = true;338      driver.opts.autoWebviewTimeout = 5000;339      await driver.startAndroidSession().should.eventually.be.rejected;340      driver.defaultWebviewName.calledOnce.should.be.true;341      // we have a timeout of 5000ms, retrying on 500ms, so expect 10 times342      driver.setContext.callCount.should.equal(10);343      let end = Date.now();344      (end - begin).should.be.above(5000);345    });346    it('should not set the context if autoWebview is not requested', async () => {347      await driver.startAndroidSession();348      driver.defaultWebviewName.calledOnce.should.be.false;349      driver.setContext.calledOnce.should.be.false;350    });351    it('should set ignoreUnimportantViews cap', async () => {352      driver.opts.ignoreUnimportantViews = true;353      await driver.startAndroidSession();354      driver.settings.update.calledOnce.should.be.true;355      driver.settings.update.firstCall.args[0].ignoreUnimportantViews.should.be.true;356    });357    it('should not call dismissChromeWelcome on missing chromeOptions', async () => {358      driver.opts.browserName = 'Chrome';359      await driver.startAndroidSession();360      driver.dismissChromeWelcome.calledOnce.should.be.false;361    });362    it('should call dismissChromeWelcome', async () => {363      driver.opts.browserName = 'Chrome';364      driver.opts.chromeOptions = {365        "args" : ["--no-first-run"]366      };367      await driver.startAndroidSession();368      driver.dismissChromeWelcome.calledOnce.should.be.true;369    });370  });371  describe('validateDesiredCaps', () => {372    before(() => {373      driver = new AndroidDriver();374    });375    it('should throw an error if caps do not contain an app, package or valid browser', () => {376      expect(() => {377        driver.validateDesiredCaps({platformName: 'Android', deviceName: 'device'});378      }).to.throw(/must include/);379      expect(() => {380        driver.validateDesiredCaps({platformName: 'Android', deviceName: 'device', browserName: 'Netscape Navigator'});381      }).to.throw(/must include/);382    });383    it('should not throw an error if caps contain an app, package or valid browser', () => {384      expect(() => {385        driver.validateDesiredCaps({platformName: 'Android', deviceName: 'device', app: '/path/to/some.apk'});386      }).to.not.throw(Error);387      expect(() => {388        driver.validateDesiredCaps({platformName: 'Android', deviceName: 'device', browserName: 'Chrome'});389      }).to.not.throw(Error);390      expect(() => {391        driver.validateDesiredCaps({platformName: 'Android', deviceName: 'device', appPackage: 'some.app.package'});392      }).to.not.throw(/must include/);393    });394    it('should not be sensitive to platform name casing', () => {395      expect(() => {396        driver.validateDesiredCaps({platformName: 'AnDrOiD', deviceName: 'device', app: '/path/to/some.apk'});397      }).to.not.throw(Error);398    });399    it('should not throw an error if caps contain both an app and browser, for grid compatibility', () => {400      expect(() => {401        driver.validateDesiredCaps({platformName: 'Android', deviceName: 'device', app: '/path/to/some.apk', browserName: 'iPhone'});402      }).to.not.throw(Error);403    });404    it('should not throw an error if caps contain androidScreenshotPath capability', () => {405      expect(() => {406        driver.validateDesiredCaps({platformName: 'Android', deviceName: 'device', app: '/path/to/some.apk', androidScreenshotPath: '/path/to/screenshotdir'});407      }).to.not.throw(Error);408    });409  });410  describe('proxying', () => {411    before(() => {412      driver = new AndroidDriver();413      driver.sessionId = 'abc';414    });415    describe('#proxyActive', () => {416      it('should exist', () => {417        driver.proxyActive.should.be.an.instanceof(Function);418      });419      it('should return false', () => {420        driver.proxyActive('abc').should.be.false;421      });422      it('should throw an error if session id is wrong', () => {423        (() => { driver.proxyActive('aaa'); }).should.throw;424      });425    });426    describe('#getProxyAvoidList', () => {427      it('should exist', () => {428        driver.getProxyAvoidList.should.be.an.instanceof(Function);429      });430      it('should return jwpProxyAvoid array', () => {431        let avoidList = driver.getProxyAvoidList('abc');432        avoidList.should.be.an.instanceof(Array);433        avoidList.should.eql(driver.jwpProxyAvoid);434      });435      it('should throw an error if session id is wrong', () => {436        (() => { driver.getProxyAvoidList('aaa'); }).should.throw;437      });438    });439    describe('#canProxy', () => {440      it('should exist', () => {441        driver.canProxy.should.be.an.instanceof(Function);442      });443      it('should return false', () => {444        driver.canProxy('abc').should.be.false;445      });446      it('should throw an error if session id is wrong', () => {447        (() => { driver.canProxy('aaa'); }).should.throw;448      });449    });450  });...

Full Screen

Full Screen

Using AI Code Generation

copy

Full Screen

1var wd = require('wd');2var asserters = wd.asserters;3var chai = require('chai');4var chaiAsPromised = require('chai-as-promised');5chai.use(chaiAsPromised);6chai.should();7chaiAsPromised.transferPromiseness = wd.transferPromiseness;8var AppiumAndroidDriver = require('appium-android-driver');9var helpers = new AppiumAndroidDriver.helpers();10var desiredCaps = {

Full Screen

Using AI Code Generation

copy

Full Screen

1var wd = require('wd');2var assert = require('assert');3var androidDriver = require('appium-android-driver');4var helpers = androidDriver.helpers;5wd.addPromiseChainMethod('initDevice', helpers.initDevice);6var desired = {7};8var driver = wd.promiseChainRemote('localhost', 4723);9  .init(desired)10  .initDevice()11  .then(function () {12  });

Full Screen

Using AI Code Generation

copy

Full Screen

1var wd = require('wd');2var chai = require('chai');3var chaiAsPromised = require('chai-as-promised');4var should = chai.should();5var Q = require('q');6var initDriver = require('./helpers/initDriver');7var initDevice = require('./helpers/initDevice');8chai.use(chaiAsPromised);9var driver = wd.promiseChainRemote('

Full Screen

Automation Testing Tutorials

Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.

LambdaTest Learning Hubs:

YouTube

You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.

Run Appium Android Driver automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful