UNPKG

@kronoslive/codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

1,530 lines (1,408 loc) 40.2 kB
let webdriverio; let wdioV4; const fs = require('fs'); const axios = require('axios'); const Webdriver = require('./WebDriver'); const AssertionFailedError = require('../assert/error'); const { truth } = require('../assert/truth'); const recorder = require('../recorder'); const Locator = require('../locator'); const ConnectionRefused = require('./errors/ConnectionRefused'); const mobileRoot = '//*'; const webRoot = 'body'; /** * Appium helper extends [Webriver](http://codecept.io/helpers/WebDriver/) helper. * It supports all browser methods and also includes special methods for mobile apps testing. * You can use this helper to test Web on desktop and mobile devices and mobile apps. * * ## Appium Installation * * Appium is an open source test automation framework for use with native, hybrid and mobile web apps that implements the WebDriver protocol. * It allows you to run Selenium tests on mobile devices and also test native, hybrid and mobile web apps. * * Download and install [Appium](http://appium.io/) * * ```sh * npm install -g appium * ``` * * Launch the daemon: `appium` * * ## Helper configuration * * This helper should be configured in codecept.json or codecept.conf.js * * * `app`: Application path. Local path or remote URL to an .ipa or .apk file, or a .zip containing one of these. Alias to desiredCapabilities.appPackage * * `host`: (default: 'localhost') Appium host * * `port`: (default: '4723') Appium port * * `platform`: (Android or IOS), which mobile OS to use; alias to desiredCapabilities.platformName * * `restart`: restart browser or app between tests (default: true), if set to false cookies will be cleaned but browser window will be kept and for apps nothing will be changed. * * `desiredCapabilities`: [], Appium capabilities, see below * * `platformName` - Which mobile OS platform to use * * `appPackage` - Java package of the Android app you want to run * * `appActivity` - Activity name for the Android activity you want to launch from your package. * * `deviceName`: The kind of mobile device or emulator to use * * `platformVersion`: Mobile OS version * * `app` - The absolute local path or remote http URL to an .ipa or .apk file, or a .zip containing one of these. Appium will attempt to install this app binary on the appropriate device first. * * `browserName`: Name of mobile web browser to automate. Should be an empty string if automating an app instead. * * Example Android App: * * ```js * { * helpers: { * Appium: { * platform: "Android", * desiredCapabilities: { * appPackage: "com.example.android.myApp", * appActivity: "MainActivity", * deviceName: "OnePlus3", * platformVersion: "6.0.1" * } * } * } * } * ``` * * Example iOS Mobile Web with local Appium: * * ```js * { * helpers: { * Appium: { * platform: "iOS", * url: "https://the-internet.herokuapp.com/", * desiredCapabilities: { * deviceName: "iPhone X", * platformVersion: "12.0", * browserName: "safari" * } * } * } * } * ``` * * Example iOS Mobile Web on BrowserStack: * * ```js * { * helpers: { * Appium: { * host: "hub-cloud.browserstack.com", * port: 4444, * user: process.env.BROWSERSTACK_USER, * key: process.env.BROWSERSTACK_KEY, * platform: "iOS", * url: "https://the-internet.herokuapp.com/", * desiredCapabilities: { * realMobile: "true", * device: "iPhone 8", * os_version: "12", * browserName: "safari" * } * } * } * } * ``` * * Additional configuration params can be used from <https://github.com/appium/appium/blob/master/docs/en/writing-running-appium/caps.md> * * ## Access From Helpers * * Receive a Appium client from a custom helper by accessing `browser` property: * * ```js * let browser = this.helpers['Appium'].browser * ``` * * ## Methods */ class Appium extends Webdriver { /** * Appium Special Methods for Mobile only */ constructor(config) { super(config); this.isRunning = false; webdriverio = require('webdriverio'); (!webdriverio.VERSION || webdriverio.VERSION.indexOf('4') !== 0) ? wdioV4 = false : wdioV4 = true; } _validateConfig(config) { if (!(config.app || config.platform) && !config.browser) { throw new Error(` Appium requires either platform and app or a browser to be set. Check your codeceptjs config file to ensure these are set properly { "helpers": { "Appium": { "app": "/path/to/app/package" "platform": "MOBILE_OS", } } } `); } // set defaults const defaults = { // webdriverio defaults protocol: 'http', hostname: '0.0.0.0', // webdriverio specs port: 4723, path: '/wd/hub', // config waitForTimeout: 1000, // ms logLevel: 'error', capabilities: {}, deprecationWarnings: false, restart: true, manualStart: false, timeouts: { script: 0, // ms }, }; // override defaults with config config = Object.assign(defaults, config); config.baseUrl = config.url || config.baseUrl; if (config.desiredCapabilities && Object.keys(config.desiredCapabilities).length) { config.capabilities = config.desiredCapabilities; } config.capabilities.deviceName = config.device || config.capabilities.deviceName; config.capabilities.browserName = config.browser || config.capabilities.browserName; config.capabilities.app = config.app || config.capabilities.app; config.capabilities.platformName = config.platform || config.capabilities.platformName; config.waitForTimeout /= 1000; // convert to seconds // [CodeceptJS compatible] transform host to hostname config.hostname = config.host || config.hostname; if (!config.app && config.capabilities.browserName) { this.isWeb = true; this.root = webRoot; } else { this.isWeb = false; this.root = mobileRoot; } this.platform = null; if (config.capabilities.platformName) { this.platform = config.capabilities.platformName.toLowerCase(); } return config; } static _config() { return [{ name: 'app', message: 'Application package. Path to file or url', default: 'http://localhost', }, { name: 'platform', message: 'Mobile Platform', type: 'list', choices: ['iOS', 'Android'], default: 'Android', }, { name: 'device', message: 'Device to run tests on', default: 'emulator', }]; } async _startBrowser() { try { if (this.options.multiremote) { this.browser = await webdriverio.multiremote(this.options.multiremote); } else { this.browser = await webdriverio.remote(this.options); } } catch (err) { if (err.toString().indexOf('ECONNREFUSED')) { throw new ConnectionRefused(err); } throw err; } this.$$ = this.browser.$$.bind(this.browser); this.isRunning = true; if (this.options.timeouts && this.isWeb) { await this.defineTimeout(this.options.timeouts); } if (this.options.windowSize === 'maximize' && !this.platform) { const res = await this.browser.execute('return [screen.width, screen.height]'); return this.browser.windowHandleSize({ width: res.value[0], height: res.value[1], }); } if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0 && !this.platform) { const dimensions = this.options.windowSize.split('x'); await this.browser.windowHandleSize({ width: dimensions[0], height: dimensions[1], }); } } async _after() { if (!this.isRunning) return; if (this.options.restart) { this.isRunning = false; return this.browser.deleteSession(); } if (this.isWeb && !this.platform) { return super._after(); } } async _withinBegin(context) { if (this.isWeb) { return super._withinBegin(context); } if (context === 'webview') { return this.switchToWeb(); } if (typeof context === 'object') { if (context.web) return this.switchToWeb(context.web); if (context.webview) return this.switchToWeb(context.webview); } return this._switchToContext(context); } _withinEnd() { if (this.isWeb) { return super._withinEnd(); } return this.switchToNative(); } _buildAppiumEndpoint() { const { protocol, port, hostname, path, } = this.browser.options; // Build path to Appium REST API endpoint return `${protocol}://${hostname}:${port}${path}`; } /** * Execute code only on iOS * * ```js * I.runOnIOS(() => { * I.click('//UIAApplication[1]/UIAWindow[1]/UIAButton[1]'); * I.see('Hi, IOS', '~welcome'); * }); * ``` * * Additional filter can be applied by checking for capabilities. * For instance, this code will be executed only on iPhone 5s: * * * ```js * I.runOnIOS({deviceName: 'iPhone 5s'},() => { * // ... * }); * ``` * * Also capabilities can be checked by a function. * * ```js * I.runOnAndroid((caps) => { * // caps is current config of desiredCapabiliites * return caps.platformVersion >= 6 * },() => { * // ... * }); * ``` * * @param {*} caps * @param {*} fn */ async runOnIOS(caps, fn) { if (this.platform !== 'ios') return; recorder.session.start('iOS-only actions'); this._runWithCaps(caps, fn); recorder.add('restore from iOS session', () => recorder.session.restore()); return recorder.promise(); } /** * Execute code only on Android * * ```js * I.runOnAndroid(() => { * I.click('io.selendroid.testapp:id/buttonTest'); * }); * ``` * * Additional filter can be applied by checking for capabilities. * For instance, this code will be executed only on Android 6.0: * * * ```js * I.runOnAndroid({platformVersion: '6.0'},() => { * // ... * }); * ``` * * Also capabilities can be checked by a function. * In this case, code will be executed only on Android >= 6. * * ```js * I.runOnAndroid((caps) => { * // caps is current config of desiredCapabiliites * return caps.platformVersion >= 6 * },() => { * // ... * }); * ``` * * @param {*} caps * @param {*} fn */ async runOnAndroid(caps, fn) { if (this.platform !== 'android') return; recorder.session.start('Android-only actions'); this._runWithCaps(caps, fn); recorder.add('restore from Android session', () => recorder.session.restore()); return recorder.promise(); } /** * Execute code only in Web mode. * * ```js * I.runInWeb(() => { * I.waitForElement('#data'); * I.seeInCurrentUrl('/data'); * }); * ``` * * @param {*} fn */ /* eslint-disable */ async runInWeb(fn) { if (!this.isWeb) return; recorder.session.start('Web-only actions'); recorder.add('restore from Web session', () => recorder.session.restore(), true); return recorder.promise(); } /* eslint-enable */ async _runWithCaps(caps, fn) { if (typeof caps === 'object') { for (const key in caps) { // skip if capabilities do not match if (this.config.desiredCapabilities[key] !== caps[key]) { return; } } } if (typeof caps === 'function') { if (!fn) { fn = caps; } else { // skip if capabilities are checked inside a function const enabled = caps(this.config.desiredCapabilities); if (!enabled) return; } } fn(); } /** * Check if an app is installed. * * ```js * I.seeAppIsInstalled("com.example.android.apis"); * ``` * * @param {string} bundleId String ID of bundled app * * Appium: support only Android */ async seeAppIsInstalled(bundleId) { onlyForApps.call(this, 'Android'); const res = await this.browser.isAppInstalled(bundleId); return truth(`app ${bundleId}`, 'to be installed').assert(res); } /** * Check if an app is not installed. * * ```js * I.seeAppIsNotInstalled("com.example.android.apis"); * ``` * * @param {string} bundleId String ID of bundled app * * Appium: support only Android */ async seeAppIsNotInstalled(bundleId) { onlyForApps.call(this, 'Android'); const res = await this.browser.isAppInstalled(bundleId); return truth(`app ${bundleId}`, 'to be installed').negate(res); } /** * Install an app on device. * * ```js * I.installApp('/path/to/file.apk'); * ``` * @param {string} path path to apk file * * Appium: support only Android */ async installApp(path) { onlyForApps.call(this, 'Android'); return this.browser.installApp(path); } /** * Remove an app from the device. * * ```js * I.removeApp('appName', 'com.example.android.apis'); * ``` * @param {string} appId * @param {string} bundleId String ID of bundle * * Appium: support only Android */ async removeApp(appId, bundleId) { onlyForApps.call(this, 'Android'); if (wdioV4) { return this.browser.removeApp(bundleId); } return axios({ method: 'post', url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/appium/device/remove_app`, data: { appId, bundleId }, }); } /** * Check current activity on an Android device. * * ```js * I.seeCurrentActivityIs(".HomeScreenActivity") * ``` * @param {string} currentActivity * * Appium: support only Android */ async seeCurrentActivityIs(currentActivity) { onlyForApps.call(this, 'Android'); const res = await this.browser.getCurrentActivity(); return truth('current activity', `to be ${currentActivity}`).assert(res === currentActivity); } /** * Check whether the device is locked. * * ```js * I.seeDeviceIsLocked(); * ``` * * Appium: support only Android */ async seeDeviceIsLocked() { onlyForApps.call(this, 'Android'); const res = await this.browser.isLocked(); return truth('device', 'to be locked').assert(res); } /** * Check whether the device is not locked. * * ```js * I.seeDeviceIsUnlocked(); * ``` * * Appium: support only Android */ async seeDeviceIsUnlocked() { onlyForApps.call(this, 'Android'); const res = await this.browser.isLocked(); return truth('device', 'to be locked').negate(res); } /** * Check the device orientation * * ```js * I.seeOrientationIs('PORTRAIT'); * I.seeOrientationIs('LANDSCAPE') * ``` * * @param {'LANDSCAPE'|'PORTRAIT'} orientation LANDSCAPE or PORTRAIT * * Appium: support Android and iOS */ async seeOrientationIs(orientation) { onlyForApps.call(this); let currentOrientation; if (wdioV4) { const res = await this.browser.orientation(); currentOrientation = res; } const res = await axios({ method: 'get', url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/orientation`, }); currentOrientation = res.data.value; return truth('orientation', `to be ${orientation}`).assert(currentOrientation === orientation); } /** * Set a device orientation. Will fail, if app will not set orientation * * ```js * I.setOrientation('PORTRAIT'); * I.setOrientation('LANDSCAPE') * ``` * * @param {'LANDSCAPE'|'PORTRAIT'} orientation LANDSCAPE or PORTRAIT * * Appium: support Android and iOS */ async setOrientation(orientation) { onlyForApps.call(this); if (wdioV4) { return this.browser.setOrientation(orientation); } return axios({ method: 'post', url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/orientation`, data: { orientation }, }); } /** * Get list of all available contexts * * ``` * let contexts = await I.grabAllContexts(); * ``` * * Appium: support Android and iOS */ async grabAllContexts() { onlyForApps.call(this); return this.browser.getContexts(); } /** * Retrieve current context * * ```js * let context = await I.grabContext(); * ``` * * Appium: support Android and iOS */ async grabContext() { onlyForApps.call(this); return this.browser.getContext(); } /** * Get current device activity. * * ```js * let activity = await I.grabCurrentActivity(); * ``` * * Appium: support only Android */ async grabCurrentActivity() { onlyForApps.call(this, 'Android'); return this.browser.getCurrentActivity(); } /** * Get information about the current network connection (Data/WIFI/Airplane). * The actual server value will be a number. However WebdriverIO additional * properties to the response object to allow easier assertions. * * ```js * let con = await I.grabNetworkConnection(); * ``` * * Appium: support only Android */ async grabNetworkConnection() { onlyForApps.call(this, 'Android'); const res = await this.browser.getNetworkConnection(); return { value: res, inAirplaneMode: res.inAirplaneMode, hasWifi: res.hasWifi, hasData: res.hasData, }; } /** * Get current orientation. * * ```js * let orientation = await I.grabOrientation(); * ``` * * Appium: support Android and iOS */ async grabOrientation() { onlyForApps.call(this); const res = await this.browser.orientation(); this.debugSection('Orientation', res); return res; } /** * Get all the currently specified settings. * * ```js * let settings = await I.grabSettings(); * ``` * * Appium: support Android and iOS */ async grabSettings() { onlyForApps.call(this); const res = await this.browser.getSettings(); this.debugSection('Settings', JSON.stringify(res)); return res; } /** * Switch to the specified context. * * @param {*} context the context to switch to */ async _switchToContext(context) { return this.browser.switchContext(context); } /** * Switches to web context. * If no context is provided switches to the first detected web context * * ```js * // switch to first web context * I.switchToWeb(); * * // or set the context explicitly * I.switchToWeb('WEBVIEW_io.selendroid.testapp'); * ``` * * @param {string} [context] */ async switchToWeb(context) { this.isWeb = true; this.defaultContext = 'body'; if (context) return this._switchToContext(context); const contexts = await this.grabAllContexts(); this.debugSection('Contexts', contexts.toString()); for (const idx in contexts) { if (contexts[idx].match(/^WEBVIEW/)) return this._switchToContext(contexts[idx]); } throw new Error('No WEBVIEW could be guessed, please specify one in params'); } /** * Switches to native context. * By default switches to NATIVE_APP context unless other specified. * * ```js * I.switchToNative(); * * // or set context explicitly * I.switchToNative('SOME_OTHER_CONTEXT'); * ``` * @param {*} context */ async switchToNative(context = null) { this.isWeb = false; this.defaultContext = '//*'; if (context) return this._switchToContext(context); return this._switchToContext('NATIVE_APP'); } /** * Start an arbitrary Android activity during a session. * * ```js * I.startActivity('io.selendroid.testapp', '.RegisterUserActivity'); * ``` * * Appium: support only Android */ async startActivity(appPackage, appActivity) { onlyForApps.call(this, 'Android'); return this.browser.startActivity(appPackage, appActivity); } /** * Set network connection mode. * * * airplane mode * * wifi mode * * data data * * ```js * I.setNetworkConnection(0) // airplane mode off, wifi off, data off * I.setNetworkConnection(1) // airplane mode on, wifi off, data off * I.setNetworkConnection(2) // airplane mode off, wifi on, data off * I.setNetworkConnection(4) // airplane mode off, wifi off, data on * I.setNetworkConnection(6) // airplane mode off, wifi on, data on * ``` * See corresponding [webdriverio reference](http://webdriver.io/api/mobile/setNetworkConnection.html). * * Appium: support only Android */ async setNetworkConnection(value) { onlyForApps.call(this, 'Android'); return this.browser.setNetworkConnection(value); } /** * Update the current setting on the device * * ```js * I.setSettings({cyberdelia: 'open'}); * ``` * * @param {object} settings object * * Appium: support Android and iOS */ async setSettings(settings) { onlyForApps.call(this); return this.browser.settings(settings); } /** * Hide the keyboard. * * ```js * // taps outside to hide keyboard per default * I.hideDeviceKeyboard(); * I.hideDeviceKeyboard('tapOutside'); * * // or by pressing key * I.hideDeviceKeyboard('pressKey', 'Done'); * ``` * * @param {'tapOutside' | 'pressKey'} strategy desired strategy to close keyboard (‘tapOutside’ or ‘pressKey’) * * Appium: support Android and iOS */ async hideDeviceKeyboard(strategy, key) { onlyForApps.call(this); strategy = strategy || 'tapOutside'; return this.browser.hideKeyboard(strategy, key); } /** * Send a key event to the device. * List of keys: https://developer.android.com/reference/android/view/KeyEvent.html * * ```js * I.sendDeviceKeyEvent(3); * ``` * * @param {number} keyValue Device specific key value * * Appium: support only Android */ async sendDeviceKeyEvent(keyValue) { onlyForApps.call(this, 'Android'); if (wdioV4) { return this.browser.sendKeyEvent(keyValue); } return this.browser.pressKeyCode(keyValue); } /** * Open the notifications panel on the device. * * ```js * I.openNotifications(); * ``` * * Appium: support only Android */ async openNotifications() { onlyForApps.call(this, 'Android'); return this.browser.openNotifications(); } /** * The Touch Action API provides the basis of all gestures that can be * automated in Appium. At its core is the ability to chain together ad hoc * individual actions, which will then be applied to an element in the * application on the device. * [See complete documentation](http://webdriver.io/api/mobile/touchAction.html) * * ```js * I.makeTouchAction("~buttonStartWebviewCD", 'tap'); * ``` * * Appium: support Android and iOS */ async makeTouchAction(locator, action) { onlyForApps.call(this); const element = await this.browser.$(parseLocator.call(this, locator)); return this.browser.touchAction({ action, element, }); } /** * Taps on element. * * ```js * I.tap("~buttonStartWebviewCD"); * ``` * * Shortcut for `makeTouchAction` * * @param {*} locator */ async tap(locator) { return this.makeTouchAction(locator, 'tap'); } /** * Perform a swipe on the screen or an element. * * ```js * let locator = "#io.selendroid.testapp:id/LinearLayout1"; * I.swipe(locator, 800, 1200, 1000); * ``` * * [See complete reference](http://webdriver.io/api/mobile/swipe.html) * * @param {CodeceptJS.LocatorOrString} locator * @param {number} xoffset * @param {number} yoffset * @param {number} [speed=1000] (optional), 1000 by default * * Appium: support Android and iOS */ /* eslint-disable */ async swipe(locator, xoffset, yoffset, speed = 1000) { onlyForApps.call(this); const res = await this.browser.$(parseLocator.call(this, locator)); // if (!res.length) throw new ElementNotFound(locator, 'was not found in UI'); return this.performSwipe(await res.getLocation(), { x: await res.getLocation().x + xoffset, y: await res.getLocation().y + yoffset }); } /* eslint-enable */ /** * Perform a swipe on the screen. * * ```js * I.performswipe(100,200); * ``` * * @param {number} from * @param {number} to * * Appium: support Android and iOS */ async performSwipe(from, to) { await this.browser.touchPerform([{ action: 'press', options: from, }, { action: 'wait', options: { ms: 1000 }, }, { action: 'moveTo', options: to, }, { action: 'release', }]); await this.browser.pause(1000); } /** * Perform a swipe down on an element. * * ```js * let locator = "#io.selendroid.testapp:id/LinearLayout1"; * I.swipeDown(locator); // simple swipe * I.swipeDown(locator, 500); // set speed * I.swipeDown(locator, 1200, 1000); // set offset and speed * ``` * * @param {CodeceptJS.LocatorOrString} locator * @param {number} [yoffset] (optional) * @param {number} [speed=1000] (optional), 1000 by default * * Appium: support Android and iOS */ async swipeDown(locator, yoffset = 1000, speed) { onlyForApps.call(this); if (!speed) { speed = yoffset; yoffset = 100; } return this.swipe(locator, 0, yoffset, speed); } /** * * Perform a swipe left on an element. * * ```js * let locator = "#io.selendroid.testapp:id/LinearLayout1"; * I.swipeLeft(locator); // simple swipe * I.swipeLeft(locator, 500); // set speed * I.swipeLeft(locator, 1200, 1000); // set offset and speed * ``` * * @param {CodeceptJS.LocatorOrString} locator * @param {number} [xoffset] (optional) * @param {number} [speed=1000] (optional), 1000 by default * * Appium: support Android and iOS */ async swipeLeft(locator, xoffset = 1000, speed) { onlyForApps.call(this); if (!speed) { speed = xoffset; xoffset = 100; } return this.swipe(parseLocator.call(this, locator), -xoffset, 0, speed); } /** * Perform a swipe right on an element. * * ```js * let locator = "#io.selendroid.testapp:id/LinearLayout1"; * I.swipeRight(locator); // simple swipe * I.swipeRight(locator, 500); // set speed * I.swipeRight(locator, 1200, 1000); // set offset and speed * ``` * * @param {CodeceptJS.LocatorOrString} locator * @param {number} [xoffset] (optional) * @param {number} [speed=1000] (optional), 1000 by default * * Appium: support Android and iOS */ async swipeRight(locator, xoffset = 1000, speed) { onlyForApps.call(this); if (!speed) { speed = xoffset; xoffset = 100; } return this.swipe(parseLocator.call(this, locator), xoffset, 0, speed); } /** * Perform a swipe up on an element. * * ```js * let locator = "#io.selendroid.testapp:id/LinearLayout1"; * I.swipeUp(locator); // simple swipe * I.swipeUp(locator, 500); // set speed * I.swipeUp(locator, 1200, 1000); // set offset and speed * ``` * * @param {CodeceptJS.LocatorOrString} locator * @param {number} [yoffset] (optional) * @param {number} [speed=1000] (optional), 1000 by default * * Appium: support Android and iOS */ async swipeUp(locator, yoffset = 1000, speed) { onlyForApps.call(this); if (!speed) { speed = yoffset; yoffset = 100; } return this.swipe(parseLocator.call(this, locator), 0, -yoffset, speed); } /** * Perform a swipe in selected direction on an element to searchable element. * * ```js * I.swipeTo( * "android.widget.CheckBox", // searchable element * "//android.widget.ScrollView/android.widget.LinearLayout", // scroll element * "up", // direction * 30, * 100, * 500); * ``` * * @param {string} searchableLocator * @param {string} scrollLocator * @param {string} direction * @param {number} timeout * @param {number} offset * @param {number} speed * * Appium: support Android and iOS */ async swipeTo(searchableLocator, scrollLocator, direction, timeout, offset, speed) { onlyForApps.call(this); direction = direction || 'down'; switch (direction) { case 'down': direction = 'swipeDown'; break; case 'up': direction = 'swipeUp'; break; case 'left': direction = 'swipeLeft'; break; case 'right': direction = 'swipeRight'; break; } timeout = timeout || this.options.waitForTimeout; const errorMsg = `element ("${searchableLocator}") still not visible after ${timeout}seconds`; const browser = this.browser; let err = false; let currentSource; return browser.waitUntil(() => { if (err) { return new Error(`Scroll to the end and element ${searchableLocator} was not found`); } return browser.$$(parseLocator.call(this, searchableLocator)) .then(els => els.length && els[0].isDisplayed()) .then((res) => { if (res) { return true; } return this[direction](scrollLocator, offset, speed).getSource().then((source) => { if (source === currentSource) { err = true; } else { currentSource = source; return false; } }); }); }, timeout * 1000, errorMsg) .catch((e) => { if (e.message.indexOf('timeout') && e.type !== 'NoSuchElement') { throw new AssertionFailedError({ customMessage: `Scroll to the end and element ${searchableLocator} was not found` }, ''); } else { throw e; } }); } /** * Performs a specific touch action. * The action object need to contain the action name, x/y coordinates * * ```js * I.touchPerform([{ * action: 'press', * options: { * x: 100, * y: 200 * } * }, {action: 'release'}]) * * I.touchPerform([{ * action: 'tap', * options: { * element: '1', // json web element was queried before * x: 10, // x offset * y: 20, // y offset * count: 1 // number of touches * } * }]); * ``` * * Appium: support Android and iOS */ async touchPerform(actions) { onlyForApps.call(this); return this.browser.touchPerform(actions); } /** * Pulls a file from the device. * * ```js * I.pullFile('/storage/emulated/0/DCIM/logo.png', 'my/path'); * // save file to output dir * I.pullFile('/storage/emulated/0/DCIM/logo.png', output_dir); * ``` * * Appium: support Android and iOS */ async pullFile(path, dest) { onlyForApps.call(this); return this.browser.pullFile(path).then(res => fs.writeFile(dest, Buffer.from(res, 'base64'), (err) => { if (err) { return false; } return true; })); } /** * Perform a shake action on the device. * * ```js * I.shakeDevice(); * ``` * * Appium: support only iOS */ async shakeDevice() { onlyForApps.call(this, 'iOS'); return this.browser.shake(); } /** * Perform a rotation gesture centered on the specified element. * * ```js * I.rotate(120, 120) * ``` * * See corresponding [webdriverio reference](http://webdriver.io/api/mobile/rotate.html). * * Appium: support only iOS */ async rotate(x, y, duration, radius, rotation, touchCount) { onlyForApps.call(this, 'iOS'); return this.browser.rotate(x, y, duration, radius, rotation, touchCount); } /** * Set immediate value in app. * * See corresponding [webdriverio reference](http://webdriver.io/api/mobile/setImmediateValue.html). * * Appium: support only iOS */ async setImmediateValue(id, value) { onlyForApps.call(this, 'iOS'); return this.browser.setImmediateValue(id, value); } /** * Simulate Touch ID with either valid (match == true) or invalid (match == false) fingerprint. * * ```js * I.touchId(); // simulates valid fingerprint * I.touchId(true); // simulates valid fingerprint * I.touchId(false); // simulates invalid fingerprint * ``` * * Appium: support only iOS * TODO: not tested */ async simulateTouchId(match) { onlyForApps.call(this, 'iOS'); match = match || true; return this.browser.touchId(match); } /** * Close the given application. * * ```js * I.closeApp(); * ``` * * Appium: support only iOS */ async closeApp() { onlyForApps.call(this, 'iOS'); return this.browser.closeApp(); } /** * {{> appendField }} * */ async appendField(field, value) { if (this.isWeb) return super.appendField(field, value); return super.appendField(parseLocator.call(this, field), value); } /** * {{> checkOption }} * */ async checkOption(field) { if (this.isWeb) return super.checkOption(field); return super.checkOption(parseLocator.call(this, field)); } /** * {{> click }} * */ async click(locator, context) { if (this.isWeb) return super.click(locator, context); return super.click(parseLocator.call(this, locator), parseLocator.call(this, context)); } /** * {{> dontSeeCheckboxIsChecked }} * */ async dontSeeCheckboxIsChecked(field) { if (this.isWeb) return super.dontSeeCheckboxIsChecked(field); return super.dontSeeCheckboxIsChecked(parseLocator.call(this, field)); } /** * {{> dontSeeElement }} */ async dontSeeElement(locator) { if (this.isWeb) return super.dontSeeElement(locator); return super.dontSeeElement(parseLocator.call(this, locator)); } /** * {{> dontSeeInField }} * */ async dontSeeInField(field, value) { if (this.isWeb) return super.dontSeeInField(field, value); return super.dontSeeInField(parseLocator.call(this, field), value); } /** * {{> dontSee }} */ async dontSee(text, context = null) { if (this.isWeb) return super.dontSee(text, context); return super.dontSee(text, parseLocator.call(this, context)); } /** * {{> fillField }} * */ async fillField(field, value) { value = value.toString(); if (this.isWeb) return super.fillField(field, value); return super.fillField(parseLocator.call(this, field), value); } /** * {{> grabTextFromAll }} * */ async grabTextFromAll(locator) { if (this.isWeb) return super.grabTextFromAll(locator); return super.grabTextFromAll(parseLocator.call(this, locator)); } /** * {{> grabTextFrom }} * */ async grabTextFrom(locator) { if (this.isWeb) return super.grabTextFrom(locator); return super.grabTextFrom(parseLocator.call(this, locator)); } /** * {{> grabValueFromAll }} * */ async grabValueFromAll(locator) { if (this.isWeb) return super.grabValueFromAll(locator); return super.grabValueFromAll(parseLocator.call(this, locator)); } /** * {{> grabValueFrom }} * */ async grabValueFrom(locator) { if (this.isWeb) return super.grabValueFrom(locator); return super.grabValueFrom(parseLocator.call(this, locator)); } /** * {{> scrollIntoView }} * * Supported only for web testing */ async scrollIntoView(locator, scrollIntoViewOptions) { if (this.isWeb) return super.scrollIntoView(locator, scrollIntoViewOptions); } /** * {{> seeCheckboxIsChecked }} * */ async seeCheckboxIsChecked(field) { if (this.isWeb) return super.seeCheckboxIsChecked(field); return super.seeCheckboxIsChecked(parseLocator.call(this, field)); } /** * {{> seeElement }} * */ async seeElement(locator) { if (this.isWeb) return super.seeElement(locator); return super.seeElement(parseLocator.call(this, locator)); } /** * {{> seeInField }} * */ async seeInField(field, value) { if (this.isWeb) return super.seeInField(field, value); return super.seeInField(parseLocator.call(this, field), value); } /** * {{> see }} * */ async see(text, context) { if (this.isWeb) return super.see(text, context); return super.see(text, parseLocator.call(this, context)); } /** * {{> selectOption }} * * Supported only for web testing */ async selectOption(select, option) { if (this.isWeb) return super.selectOption(select, option); throw new Error('Should be used only in Web context. In native context use \'click\' method instead'); } /** * {{> waitForElement }} * */ async waitForElement(locator, sec = null) { if (this.isWeb) return super.waitForElement(locator, sec); return super.waitForElement(parseLocator.call(this, locator), sec); } /** * {{> waitForVisible }} * */ async waitForVisible(locator, sec = null) { if (this.isWeb) return super.waitForVisible(locator, sec); return super.waitForVisible(parseLocator.call(this, locator), sec); } /** * {{> waitForInvisible }} * */ async waitForInvisible(locator, sec = null) { if (this.isWeb) return super.waitForInvisible(locator, sec); return super.waitForInvisible(parseLocator.call(this, locator), sec); } /** * {{> waitForText }} * */ async waitForText(text, sec = null, context = null) { if (this.isWeb) return super.waitForText(text, sec, context); return super.waitForText(text, sec, parseLocator.call(this, context)); } } function parseLocator(locator) { if (!locator) return null; if (typeof locator === 'object') { if (locator.web && this.isWeb) { return parseLocator.call(this, locator.web); } if (locator.android && this.platform === 'android') { if (typeof locator.android === 'string') { return parseLocator.call(this, locator.android); } // The locator is an Android DataMatcher or ViewMatcher locator so return as is return locator.android; } if (locator.ios && this.platform === 'ios') { return parseLocator.call(this, locator.ios); } } if (typeof locator === 'string') { if (locator[0] === '~') return locator; if (locator.substr(0, 2) === '//') return locator; if (locator[0] === '#' && !this.isWeb) { // hook before webdriverio supports native # locators return parseLocator.call(this, { id: locator.slice(1) }); } if (this.platform === 'android' && !this.isWeb) { const isNativeLocator = /^\-?android=?/.exec(locator); return isNativeLocator ? locator : `android=new UiSelector().text("${locator}")`; } } locator = new Locator(locator, 'xpath'); if (locator.type === 'css' && !this.isWeb) throw new Error('Unable to use css locators in apps. Locator strategies for this request: xpath, id, class name or accessibility id'); if (locator.type === 'name' && !this.isWeb) throw new Error("Can't locate element by name in Native context. Use either ID, class name or accessibility id"); if (locator.type === 'id' && !this.isWeb && this.platform === 'android') return `//*[@resource-id='${locator.value}']`; return locator.simplify(); } // in the end of a file function onlyForApps(expectedPlatform) { const stack = new Error().stack || ''; const re = /Appium.(\w+)/g; const caller = stack.split('\n')[2].trim(); const m = re.exec(caller); if (!m) { throw new Error(`Invalid caller ${caller}`); } const callerName = m[1] || m[2]; if (!expectedPlatform) { if (!this.platform) { throw new Error(`${callerName} method can be used only with apps`); } } else if (this.platform !== expectedPlatform.toLowerCase()) { throw new Error(`${callerName} method can be used only with ${expectedPlatform} apps`); } } module.exports = Appium;