lib/appium/helpers/wdHelper.js (277 lines of code) (raw):

/* * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ 'use strict'; const APPIUM_SERVER_HOST = 'localhost'; const APPIUM_SERVER_PORT = 4723; const WEBVIEW_WAIT_TIMEOUT = 5000; const IMPLICIT_WAIT_TIMEOUT = 10000; const ASYNC_SCRIPT_TIMEOUT = 60000; const fs = require('fs'); const path = require('path'); const wd = global.WD || require('wd'); const { utilities } = require('../../utils'); module.exports.getDriver = function (platform) { let normalizedPlatform; let automationName; let driverConfig = {}; let serverConfig = {}; let driver; switch (platform.toLowerCase()) { case utilities.ANDROID: normalizedPlatform = 'Android'; automationName = 'Appium'; break; case utilities.IOS: normalizedPlatform = 'iOS'; automationName = 'XCUITest'; break; default: throw new Error(`Unknown Platform: ${platform}`); } global.WD.configureHttp({ timeout: utilities.WD_TIMEOUT, retryDelay: utilities.WD_RETRY_DELAY, retries: utilities.WD_RETRIES }); if (global.USE_SAUCE) { serverConfig = { host: global.SAUCE_SERVER_HOST, port: global.SAUCE_SERVER_PORT }; driverConfig = global.SAUCE_CAPS; driver = global.WD.promiseChainRemote(serverConfig.host, serverConfig.port, global.SAUCE_USER, global.SAUCE_KEY); } else { serverConfig = { host: APPIUM_SERVER_HOST, port: APPIUM_SERVER_PORT }; driverConfig = { browserName: '', platformName: normalizedPlatform, platformVersion: global.PLATFORM_VERSION || '', deviceName: global.DEVICE_NAME || '', app: global.PACKAGE_PATH, autoAcceptAlerts: true, automationName }; if (platform.toLowerCase() === utilities.IOS) { driverConfig.wdaLaunchTimeout = 150000; } if (global.UDID) { driverConfig.udid = global.UDID; } driver = global.WD.promiseChainRemote(serverConfig); } module.exports.configureLogging(driver); const spamDots = setInterval(function () { process.stdout.write('.'); }, 1000); return driver .init(driverConfig) .setImplicitWaitTimeout(IMPLICIT_WAIT_TIMEOUT) .then(function () { clearInterval(spamDots); process.stdout.write('\n'); }, function (error) { clearInterval(spamDots); process.stdout.write('\n'); throw (error); }); }; module.exports.getWD = function () { return wd; }; module.exports.getWebviewContext = function (driver, retries) { if (typeof retries === 'undefined') { retries = 2; } return driver .sleep(WEBVIEW_WAIT_TIMEOUT) .contexts() .then(function (contexts) { // take the last webview context contexts.reverse(); for (let i = 0; i < contexts.length; i++) { if (contexts[i].indexOf('WEBVIEW') >= 0) return contexts[i]; } // no webview context, the app is still loading return driver .sleep(1000) .then(function () { if (retries > 0) { console.log('No webview context. Retries remaining: ' + retries); return module.exports.getWebviewContext(driver, retries - 1); } throw new Error('Couldn\'t get webview context.'); }); }); }; module.exports.waitForDeviceReady = function (driver) { return driver .setAsyncScriptTimeout(ASYNC_SCRIPT_TIMEOUT) .executeAsync(function (cb) { document.addEventListener('deviceready', function () { cb(); }, false); }, []); }; module.exports.injectLibraries = function (driver) { const q = fs.readFileSync(path.join(__dirname, 'lib', 'q.min.js'), 'utf8'); return driver .execute(q) .execute(function () { navigator._appiumPromises = {}; }, []); }; module.exports.configureLogging = function (driver) { if (!global.VERBOSE) return; driver.on('status', function (info) { console.log(info); }); driver.on('command', function (meth, path, data) { console.log(' > ' + meth, path, data || ''); }); driver.on('http', function (meth, path, data) { console.log(' > ' + meth, path, data || ''); }); }; module.exports.tapElementByXPath = function (xpath, driver) { return driver .waitForElementByXPath(xpath, 30000) .getLocation() .then(function (loc) { if (loc.x <= 0) loc.x = 1; if (loc.y <= 0) loc.y = 1; loc.x = Math.floor(loc.x + 1); loc.y = Math.floor(loc.y + 1); const wd = module.exports.getWD(); const tapElement = new wd.TouchAction(); tapElement.tap(loc); return driver.performTouchAction(tapElement); }); }; module.exports.pollForEvents = function (driver, platform, skipBuster, windowOffset, retries) { const isAndroid = platform === utilities.ANDROID; const isBrowser = platform === utilities.BROWSER; const isIOS = platform === utilities.IOS; if (retries === undefined || retries === null) retries = 2; if (!windowOffset) windowOffset = 0; // polling for new events return driver .sleep(0) .then(function () { if (skipBuster) return driver; return driver.bustAlert(platform); }) .then(function () { if (isIOS || isBrowser) return driver; // for some reason inappbrowser tests tend to leave an active window on android // so for the polling to work correctly we need to // switch back to the window where the cache is located return driver .windowHandles() .then(function (windowHandles) { if (windowOffset >= windowHandles.length) { throw new Error('Cannot find a window with the event cache.'); } return driver.window(windowHandles[windowOffset]); }); }) .execute(function () { // wrong window if (typeof window._jasmineParamedicProxyCache === 'undefined') return null; // get the results and clean up the cache const result = window._jasmineParamedicProxyCache; window._jasmineParamedicProxyCache = []; return JSON.stringify(result); }, []) .then(function (result) { if (result) { result = JSON.parse(result); } // found if (Object.prototype.toString.call(result) === '[object Array]') return result; // not found if (isBrowser && retries > 0) { // the odds are that we're hitting "bad gateway" error on Sauce, refreshing the page should fix it return driver .get('http://localhost:8000/cdvtests/index.html') .then(function () { return module.exports.pollForEvents(driver, platform, skipBuster, windowOffset, retries - 1); }); } if (!isAndroid) { throw new Error('Cannot get the event cache: it doesn\'t exist in the app. Got this instead: ' + result); } // no luck finding the event cache in this window, let's try next return module.exports.pollForEvents(driver, platform, skipBuster, windowOffset + 1); }); }; module.exports.bustAlert = function (driver, platform) { let previousContext; return driver .currentContext() .then(function (context) { if (context !== 'NATIVE_APP') { previousContext = context; } return driver; }) .context('NATIVE_APP') .then(function () { // iOS if (platform === 'ios') { return driver.acceptAlert() .then(function alertDismissed () { }, function noAlert () { }); } // Android return driver .elementByXPath('//android.widget.Button[translate(@text, "alow", "ALOW")="ALLOW"]') .click() .fail(function noAlert () { }); }) .then(function () { return previousContext ? driver.context(previousContext) : driver; }); }; module.exports.addFillerImage = function (driver) { const bitmap = fs.readFileSync(path.join(__dirname, '..', 'cordova_logo_thumb.jpg')); // @todo 'new Buffer()' was deprecated since v6.0.0. Use 'Buffer.alloc()' or 'Buffer.from()' instead const base64str = new Buffer(bitmap).toString('base64'); // eslint-disable-line return driver.executeAsync(function (b64str, cb) { try { window.imageSaver.saveBase64Image({ data: b64str }, function (fpath) { cb(fpath); }, function (err) { cb('ERROR: ' + err); // eslint-disable-line }); } catch (err) { cb('ERROR: ' + err.message); // eslint-disable-line } }, [base64str]); }; module.exports.deleteFillerImage = function (driver, testImagePath) { if (!testImagePath) return driver; return driver.executeAsync(function (testImagePath, cb) { if (window.imageSaver) { window.imageSaver.removeImage({ data: testImagePath }, function () { cb(); }, function (err) { cb('ERROR: ' + err); // eslint-disable-line }); } else { cb(); } }, [testImagePath]); }; wd.addPromiseChainMethod('getWebviewContext', function (retries) { return module.exports.getWebviewContext(this, retries); }); wd.addPromiseChainMethod('waitForDeviceReady', function () { return module.exports.waitForDeviceReady(this); }); wd.addPromiseChainMethod('injectLibraries', function () { return module.exports.injectLibraries(this); }); wd.addPromiseChainMethod('tapElementByXPath', function (xpath) { return module.exports.tapElementByXPath(xpath, this); }); wd.addPromiseChainMethod('pollForEvents', function (platform, skipBuster) { return module.exports.pollForEvents(this, platform, skipBuster); }); wd.addPromiseChainMethod('addFillerImage', function () { return module.exports.addFillerImage(this); }); wd.addPromiseChainMethod('deleteFillerImage', function (testImagePath) { return module.exports.deleteFillerImage(this, testImagePath); }); wd.addPromiseChainMethod('bustAlert', function (platform) { return module.exports.bustAlert(this, platform); });