UNPKG

kuben-appium-xcuitest-driver

Version:

Appium driver for iOS using XCUITest for backend

292 lines (243 loc) 9.93 kB
import { iosCommands } from 'kuben-appium-ios-driver'; import { retryInterval } from 'asyncbox'; import { util } from 'appium-support'; import log from '../logger'; import _ from 'lodash'; import B from 'bluebird'; const IPHONE_EXTRA_WEB_COORD_SCROLL_OFFSET = -15; const IPHONE_EXTRA_WEB_COORD_NON_SCROLL_OFFSET = 10; const IPHONE_WEB_COORD_OFFSET = -10; const IPHONE_WEB_COORD_SMART_APP_BANNER_OFFSET = 84; const IPHONE_X_EXTRA_WEB_COORD_SCROLL_OFFSET = -90; const IPHONE_X_EXTRA_WEB_COORD_NON_SCROLL_OFFSET = -10; const IPHONE_X_WEB_COORD_OFFSET = 40; const IPAD_EXTRA_WEB_COORD_SCROLL_OFFSET = -10; const IPAD_EXTRA_WEB_COORD_NON_SCROLL_OFFSET = 0; const IPAD_WEB_COORD_OFFSET = 10; const IPAD_WEB_COORD_SMART_APP_BANNER_OFFSET = 95; const IPHONE_X_WIDTH = 375; const IPHONE_X_HEIGHT = 812; const ATOM_WAIT_TIMEOUT = 5 * 60000; let extensions = {}; Object.assign(extensions, iosCommands.web); extensions.getSafariIsIphone = async function getSafariIsIphone () { try { const userAgent = await this.execute('return navigator.userAgent'); return userAgent.toLowerCase().includes('iphone'); } catch (err) { log.warn(`Unable to find device type from useragent. Assuming iPhone`); log.debug(`Error: ${err.message}`); } return true; }; extensions.getSafariIsIphoneX = async function getSafariIsIphone () { try { const script = 'return {height: window.screen.availHeight, width: window.screen.availWidth};'; const {height, width} = await this.execute(script); // check for the correct height and width return (height === IPHONE_X_HEIGHT && width === IPHONE_X_WIDTH) || (height === IPHONE_X_WIDTH && width === IPHONE_X_HEIGHT); } catch (err) { log.warn(`Unable to find device type from useragent. Assuming not iPhone X`); log.debug(`Error: ${err.message}`); } return false; }; const getElementHeightMemoized = _.memoize(async function (key, driver, el) { el = util.unwrapElement(el); return (await driver.getNativeRect(el)).height; }); extensions.getExtraTranslateWebCoordsOffset = async function (coords, webviewRect) { let offset = 0; // keep track of implicit wait, and set locally to 0 const implicitWaitMs = this.implicitWaitMs; const isIphone = await this.getSafariIsIphone(); const isIphoneX = isIphone && await this.getSafariIsIphoneX(); try { this.setImplicitWait(0); // check if the full url bar is up await this.findNativeElementOrElements('accessibility id', 'ReloadButton', false); // reload button found, which means scrolling has not happened if (isIphoneX) { offset += IPHONE_X_EXTRA_WEB_COORD_NON_SCROLL_OFFSET; } else if (isIphone) { offset += IPHONE_EXTRA_WEB_COORD_NON_SCROLL_OFFSET; } else { offset += IPAD_EXTRA_WEB_COORD_NON_SCROLL_OFFSET; } } catch (err) { // no reload button, which indicates scrolling has happened // the URL bar may or may not be visible try { const el = await this.findNativeElementOrElements('accessibility id', 'URL', false); offset -= await getElementHeightMemoized('URLBar', this, el); } catch (ign) { // no URL elements found, so continue } } finally { // return implicit wait to what it was this.setImplicitWait(implicitWaitMs); } if (coords.y > webviewRect.height) { // when scrolling has happened, there is a tick more offset needed if (isIphoneX) { offset += IPHONE_X_EXTRA_WEB_COORD_SCROLL_OFFSET; } else if (isIphone) { offset += IPHONE_EXTRA_WEB_COORD_SCROLL_OFFSET; } else { offset += IPAD_EXTRA_WEB_COORD_SCROLL_OFFSET; } } // extra offset necessary offset += isIphone ? IPHONE_WEB_COORD_OFFSET : IPAD_WEB_COORD_OFFSET; offset += isIphoneX ? IPHONE_X_WEB_COORD_OFFSET : 0; log.debug(`Extra translated web coordinates offset: ${offset}`); return offset; }; extensions.getExtraNativeWebTapOffset = async function () { let offset = 0; // keep track of implicit wait, and set locally to 0 const implicitWaitMs = this.implicitWaitMs; try { this.setImplicitWait(0); // first try to get tab offset try { const el = await this.findNativeElementOrElements('-ios predicate string', `name LIKE '*, Tab' AND visible = 1`, false); offset += await getElementHeightMemoized('TabBar', this, el); } catch (ign) { // no element found, so no tabs and no need to deal with offset } // next try to see if there is an Smart App Banner try { await this.findNativeElementOrElements('accessibility id', 'Close app download offer', false); offset += await this.getSafariIsIphone() ? IPHONE_WEB_COORD_SMART_APP_BANNER_OFFSET : IPAD_WEB_COORD_SMART_APP_BANNER_OFFSET; } catch (ign) { // no smart app banner found, so continue } } finally { // return implicit wait to what it was this.setImplicitWait(implicitWaitMs); } log.debug(`Additional native web tap offset computed: ${offset}`); return offset; }; async function tapWebElementNatively (driver, atomsElement) { // try to get the text of the element, which will be accessible in the // native context try { let text = await driver.executeAtom('get_text', [atomsElement]); if (!text) { text = await driver.executeAtom('get_attribute_value', [atomsElement, 'value']); } if (text) { const el = await driver.findNativeElementOrElements('accessibility id', text, false); // use tap because on iOS 11.2 and below `nativeClick` crashes WDA const rect = await driver.proxyCommand(`/element/${el.ELEMENT}/rect`, 'GET'); const coords = { x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2), }; await driver.clickCoords(coords); return true; } } catch (err) { // any failure should fall through and trigger the more elaborate // method of clicking log.warn(`Error attempting to click: ${err.message}`); } return false; } extensions.nativeWebTap = async function (el) { const atomsElement = this.useAtomsElement(el); if (await tapWebElementNatively(this, atomsElement)) { return; } log.warn('Unable to do simple native web tap. Attempting to convert coordinates'); // `get_top_left_coordinates` returns the wrong value sometimes, // unless we pre-call both of these functions before the actual calls await this.executeAtom('get_size', [atomsElement]); await this.executeAtom('get_top_left_coordinates', [atomsElement]); const {width, height} = await this.executeAtom('get_size', [atomsElement]); let {x, y} = await this.executeAtom('get_top_left_coordinates', [atomsElement]); x += width / 2; y += height / 2; this.curWebCoords = {x, y}; await this.clickWebCoords(); }; extensions.clickCoords = async function (coords) { await this.performTouch([ { action: 'tap', options: coords, }, ]); }; extensions.translateWebCoords = async function (coords) { log.debug(`Translating coordinates (${JSON.stringify(coords)}) to web coordinates`); // absolutize web coords const implicitWaitMs = this.implicitWaitMs; let webview; try { this.setImplicitWait(0); webview = await retryInterval(5, 100, async () => { return await this.findNativeElementOrElements('-ios predicate string', `type = 'XCUIElementTypeWebView' AND visible = 1`, false); }); } finally { this.setImplicitWait(implicitWaitMs); } webview = util.unwrapElement(webview); const rect = await this.proxyCommand(`/element/${webview}/rect`, 'GET'); const wvPos = {x: rect.x, y: rect.y}; const realDims = {w: rect.width, h: rect.height}; const cmd = '(function () { return {w: document.documentElement.clientWidth, h: document.documentElement.clientHeight}; })()'; const wvDims = await this.remote.execute(cmd); // TODO: investigate where these come from. They appear to be constants in my tests const urlBarHeight = 64; wvPos.y += urlBarHeight; const realDimensionHeight = 108; realDims.h -= realDimensionHeight; // add static offset for safari in landscape mode let yOffset = this.opts.curOrientation === 'LANDSCAPE' ? this.landscapeWebCoordsOffset : 0; // add extra offset for possible extra things in the top of the page yOffset += await this.getExtraNativeWebTapOffset(); coords.y += await this.getExtraTranslateWebCoordsOffset(coords, rect); if (wvDims && realDims && wvPos) { let xRatio = realDims.w / wvDims.w; let yRatio = realDims.h / wvDims.h; let newCoords = { x: wvPos.x + Math.round(xRatio * coords.x), y: wvPos.y + yOffset + Math.round(yRatio * coords.y), }; // additional logging for coordinates, since it is sometimes broken // see https://github.com/appium/appium/issues/9159 log.debug(`Converted coordinates: ${JSON.stringify(newCoords)}`); log.debug(` rect: ${JSON.stringify(rect)}`); log.debug(` wvPos: ${JSON.stringify(wvPos)}`); log.debug(` realDims: ${JSON.stringify(realDims)}`); log.debug(` wvDims: ${JSON.stringify(wvDims)}`); log.debug(` xRatio: ${JSON.stringify(xRatio)}`); log.debug(` yRatio: ${JSON.stringify(yRatio)}`); log.debug(` yOffset: ${JSON.stringify(yOffset)}`); log.debug(`Converted web coords ${JSON.stringify(coords)} ` + `into real coords ${JSON.stringify(newCoords)}`); return newCoords; } }; extensions.checkForAlert = async function () { // eslint-disable-line require-await return false; }; extensions.waitForAtom = async function (promise) { const started = process.hrtime(); try { return this.parseExecuteResponse(await B.resolve(promise) .timeout(ATOM_WAIT_TIMEOUT)); } catch (err) { if (err instanceof B.TimeoutError) { throw new Error(`Did not get any response after ${process.hrtime(started)[0]}s`); } throw err; } }; export default extensions;