From 554796ddb9af43aca60c363262288be74d77eb59 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 2 Jun 2023 09:36:30 +0200 Subject: [PATCH 1/3] fix: Align initial atom wait timeout with alerts check interval --- lib/commands/execute.js | 1 - lib/commands/web.js | 702 ++++++++++++++++++++-------------------- 2 files changed, 352 insertions(+), 351 deletions(-) diff --git a/lib/commands/execute.js b/lib/commands/execute.js index 2c19eaa19..f4cfcac29 100644 --- a/lib/commands/execute.js +++ b/lib/commands/execute.js @@ -6,7 +6,6 @@ import {util} from 'appium/support'; /** * Checks if script expects a particular parameter (either optional or required). * @template {keyof XCUITestDriver.executeMethodMap} Script - * @this {XCUITestDriver} * @param {Script} script - Script name * @param {string} param - Parameter name * @returns {boolean} diff --git a/lib/commands/web.js b/lib/commands/web.js index 09381bc2e..74136f59b 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -31,7 +31,10 @@ const NOTCHED_DEVICE_SIZES = [ const {W3C_WEB_ELEMENT_IDENTIFIER} = util; const ATOM_WAIT_TIMEOUT_MS = 2 * 60000; -const ATOM_INITIAL_WAIT_MS = 1000; +// This value must be greater than the alerts check interval in WDA: +// https://github.com/appium/WebDriverAgent/blob/8bc3135f021b529d916846477544f4b8ca890f59/WebDriverAgentLib/Utilities/FBAlertsMonitor.m#L17 +const ATOM_INITIAL_WAIT_MS = 2100; +const OBSTRUCTING_ALERT_PRESENCE_CHECK_INTERVAL_MS = 500; const ON_OBSTRUCTING_ALERT_EVENT = 'alert'; const ON_APP_CRASH_EVENT = 'app_crash'; @@ -114,42 +117,42 @@ const commands = { * @group Mobile Web Only */ async setFrame(frame) { - if (!this.isWebContext()) { - throw new errors.NotImplementedError(); - } + if (!this.isWebContext()) { + throw new errors.NotImplementedError(); + } - if (_.isNull(frame)) { - this.curWebFrames = []; - this.log.debug('Leaving web frame and going back to default content'); - return; - } + if (_.isNull(frame)) { + this.curWebFrames = []; + this.log.debug('Leaving web frame and going back to default content'); + return; + } - if (helpers.hasElementId(frame)) { - const atomsElement = this.getAtomsElement(frame); - const value = await this.executeAtom('get_frame_window', [atomsElement]); - this.log.debug(`Entering new web frame: '${value.WINDOW}'`); - this.curWebFrames.unshift(value.WINDOW); - } else { - const atom = _.isNumber(frame) ? 'frame_by_index' : 'frame_by_id_or_name'; - const value = await this.executeAtom(atom, [frame]); - if (_.isNull(value) || _.isUndefined(value.WINDOW)) { - throw new errors.NoSuchFrameError(); - } - this.log.debug(`Entering new web frame: '${value.WINDOW}'`); - this.curWebFrames.unshift(value.WINDOW); - } + if (helpers.hasElementId(frame)) { + const atomsElement = this.getAtomsElement(frame); + const value = await this.executeAtom('get_frame_window', [atomsElement]); + this.log.debug(`Entering new web frame: '${value.WINDOW}'`); + this.curWebFrames.unshift(value.WINDOW); + } else { + const atom = _.isNumber(frame) ? 'frame_by_index' : 'frame_by_id_or_name'; + const value = await this.executeAtom(atom, [frame]); + if (_.isNull(value) || _.isUndefined(value.WINDOW)) { + throw new errors.NoSuchFrameError(); + } + this.log.debug(`Entering new web frame: '${value.WINDOW}'`); + this.curWebFrames.unshift(value.WINDOW); + } }, /** * @this {XCUITestDriver} * @group Mobile Web Only */ async getCssProperty(propertyName, el) { - if (!this.isWebContext()) { - throw new errors.NotImplementedError(); - } + if (!this.isWebContext()) { + throw new errors.NotImplementedError(); + } - const atomsElement = this.getAtomsElement(el); - return await this.executeAtom('get_value_of_css_property', [atomsElement, propertyName]); + const atomsElement = this.getAtomsElement(el); + return await this.executeAtom('get_value_of_css_property', [atomsElement, propertyName]); }, /** * Submit the form an element is in @@ -159,34 +162,34 @@ const commands = { * @this {XCUITestDriver} */ async submit(el) { - if (!this.isWebContext()) { - throw new errors.NotImplementedError(); - } + if (!this.isWebContext()) { + throw new errors.NotImplementedError(); + } - const atomsElement = this.getAtomsElement(el); - await this.executeAtom('submit', [atomsElement]); + const atomsElement = this.getAtomsElement(el); + await this.executeAtom('submit', [atomsElement]); }, /** * @this {XCUITestDriver} * @group Mobile Web Only */ async refresh() { - if (!this.isWebContext()) { - throw new errors.NotImplementedError(); - } + if (!this.isWebContext()) { + throw new errors.NotImplementedError(); + } - await this.executeAtom('refresh', []); + await this.executeAtom('refresh', []); }, /** * @this {XCUITestDriver} * @group Mobile Web Only */ async getUrl() { - if (!this.isWebContext()) { - throw new errors.NotImplementedError(); - } + if (!this.isWebContext()) { + throw new errors.NotImplementedError(); + } - return await this.remote.execute('window.location.href'); + return await this.remote.execute('window.location.href'); }, /** * @this {XCUITestDriver} @@ -232,28 +235,28 @@ const commands = { * @group Mobile Web Only */ async setCookie(cookie) { - if (!this.isWebContext()) { - throw new errors.NotImplementedError(); - } + if (!this.isWebContext()) { + throw new errors.NotImplementedError(); + } - cookie = _.clone(cookie); + cookie = _.clone(cookie); - // if `path` field is not specified, Safari will not update cookies as expected; eg issue #1708 - if (!cookie.path) { - cookie.path = '/'; - } + // if `path` field is not specified, Safari will not update cookies as expected; eg issue #1708 + if (!cookie.path) { + cookie.path = '/'; + } - const jsCookie = cookieUtils.createJSCookie(cookie.name, cookie.value, { + const jsCookie = cookieUtils.createJSCookie(cookie.name, cookie.value, { expires: _.isNumber(cookie.expiry) ? new Date(cookie.expiry * 1000).toUTCString() : cookie.expiry, - path: cookie.path, - domain: cookie.domain, - httpOnly: cookie.httpOnly, + path: cookie.path, + domain: cookie.domain, + httpOnly: cookie.httpOnly, secure: cookie.secure, - }); - let script = `document.cookie = ${JSON.stringify(jsCookie)}`; - await this.executeAtom('execute_script', [script, []]); + }); + let script = `document.cookie = ${JSON.stringify(jsCookie)}`; + await this.executeAtom('execute_script', [script, []]); }, /** * @this {XCUITestDriver} @@ -299,18 +302,18 @@ const helpers = { * @this {XCUITestDriver} */ cacheWebElement(el) { - if (!_.isPlainObject(el)) { - return el; - } - const elId = util.unwrapElement(el); - if (!isValidElementIdentifier(elId)) { - return el; - } - // In newer debugger releases element identifiers look like `:wdc:1628151649325` - // We assume it is safe to use these to identify cached elements - const cacheId = _.includes(elId, ':') ? elId : util.uuidV4(); - this.webElementsCache.set(cacheId, elId); - return util.wrapElement(cacheId); + if (!_.isPlainObject(el)) { + return el; + } + const elId = util.unwrapElement(el); + if (!isValidElementIdentifier(elId)) { + return el; + } + // In newer debugger releases element identifiers look like `:wdc:1628151649325` + // We assume it is safe to use these to identify cached elements + const cacheId = _.includes(elId, ':') ? elId : util.uuidV4(); + this.webElementsCache.set(cacheId, elId); + return util.wrapElement(cacheId); }, /** * @this {XCUITestDriver} @@ -318,16 +321,16 @@ const helpers = { cacheWebElements(response) { const toCached = (v) => (_.isArray(v) || _.isPlainObject(v) ? this.cacheWebElements(v) : v); - if (_.isArray(response)) { - return response.map(toCached); - } else if (_.isPlainObject(response)) { - const result = {...response, ...this.cacheWebElement(response)}; - return _.toPairs(result).reduce((acc, [key, value]) => { - acc[key] = toCached(value); - return acc; - }, {}); - } - return response; + if (_.isArray(response)) { + return response.map(toCached); + } else if (_.isPlainObject(response)) { + const result = {...response, ...this.cacheWebElement(response)}; + return _.toPairs(result).reduce((acc, [key, value]) => { + acc[key] = toCached(value); + return acc; + }, {}); + } + return response; }, /** * @param {string} atom @@ -337,20 +340,20 @@ const helpers = { * @this {XCUITestDriver} */ async executeAtom(atom, args, alwaysDefaultFrame = false) { - let frames = alwaysDefaultFrame === true ? [] : this.curWebFrames; - let promise = this.remote.executeAtom(atom, args, frames); - return await this.waitForAtom(promise); + let frames = alwaysDefaultFrame === true ? [] : this.curWebFrames; + let promise = this.remote.executeAtom(atom, args, frames); + return await this.waitForAtom(promise); }, /** * @this {XCUITestDriver} */ async executeAtomAsync(atom, args, responseUrl) { - // save the resolve and reject methods of the promise to be waited for - let promise = new B((resolve, reject) => { - this.asyncPromise = {resolve, reject}; - }); - await this.remote.executeAtomAsync(atom, args, this.curWebFrames, responseUrl); - return await this.waitForAtom(promise); + // save the resolve and reject methods of the promise to be waited for + let promise = new B((resolve, reject) => { + this.asyncPromise = {resolve, reject}; + }); + await this.remote.executeAtomAsync(atom, args, this.curWebFrames, responseUrl); + return await this.waitForAtom(promise); }, /** * @template {string} S @@ -359,33 +362,33 @@ const helpers = { * @this {XCUITestDriver} */ getAtomsElement(elOrId) { - const elId = util.unwrapElement(elOrId); - if (!this.webElementsCache?.has(elId)) { - throw new errors.StaleElementReferenceError(); - } - return {ELEMENT: this.webElementsCache.get(elId)}; + const elId = util.unwrapElement(elOrId); + if (!this.webElementsCache?.has(elId)) { + throw new errors.StaleElementReferenceError(); + } + return {ELEMENT: this.webElementsCache.get(elId)}; }, /** * @param {readonly any[]} [args] * @this {XCUITestDriver} */ convertElementsForAtoms(args = []) { - return args.map((arg) => { - if (helpers.hasElementId(arg)) { - try { - return this.getAtomsElement(arg); - } catch (err) { - if (!isErrorType(err, errors.StaleElementReferenceError)) { - throw err; + return args.map((arg) => { + if (helpers.hasElementId(arg)) { + try { + return this.getAtomsElement(arg); + } catch (err) { + if (!isErrorType(err, errors.StaleElementReferenceError)) { + throw err; + } } + return arg; } - return arg; - } - return _.isArray(arg) ? this.convertElementsForAtoms(arg) : arg; - }); + return _.isArray(arg) ? this.convertElementsForAtoms(arg) : arg; + }); }, getElementId(element) { - return element.ELEMENT || element[W3C_WEB_ELEMENT_IDENTIFIER]; + return element.ELEMENT || element[W3C_WEB_ELEMENT_IDENTIFIER]; }, /** * @param {any} element @@ -413,7 +416,7 @@ const extensions = { contextElement, ]); return !_.isNull(element); -}; + }; try { await this.implicitWaitForCondition(doFind); } catch (err) { @@ -444,17 +447,17 @@ const extensions = { * @this {XCUITestDriver} */ async getSafariIsIphone() { - if (_.isBoolean(this._isSafariIphone)) { - return this._isSafariIphone; - } - try { + if (_.isBoolean(this._isSafariIphone)) { + return this._isSafariIphone; + } + try { const userAgent = /** @type {string} */ (await this.execute('return navigator.userAgent')); - this._isSafariIphone = userAgent.toLowerCase().includes('iphone'); - } catch (err) { - this.log.warn(`Unable to find device type from useragent. Assuming iPhone`); - this.log.debug(`Error: ${err.message}`); - } - return this._isSafariIphone ?? true; + this._isSafariIphone = userAgent.toLowerCase().includes('iphone'); + } catch (err) { + this.log.warn(`Unable to find device type from useragent. Assuming iPhone`); + this.log.debug(`Error: ${err.message}`); + } + return this._isSafariIphone ?? true; }, /** * @this {XCUITestDriver} @@ -465,143 +468,143 @@ const extensions = { const {width, height} = /** @type {import('@appium/types').Size} */ ( await this.execute(script) ); - const [normHeight, normWidth] = height > width ? [height, width] : [width, height]; - return { - width: normWidth, - height: normHeight, - }; + const [normHeight, normWidth] = height > width ? [height, width] : [width, height]; + return { + width: normWidth, + height: normHeight, + }; }, /** * @this {XCUITestDriver} */ async getSafariIsNotched() { - if (_.isBoolean(this._isSafariNotched)) { - return this._isSafariNotched; - } + if (_.isBoolean(this._isSafariNotched)) { + return this._isSafariNotched; + } - try { - const {width, height} = await this.getSafariDeviceSize(); - for (const device of NOTCHED_DEVICE_SIZES) { - if (device.w === width && device.h === height) { - this._isSafariNotched = true; + try { + const {width, height} = await this.getSafariDeviceSize(); + for (const device of NOTCHED_DEVICE_SIZES) { + if (device.w === width && device.h === height) { + this._isSafariNotched = true; + } } - } - } catch (err) { + } catch (err) { this.log.warn( `Unable to find device type from dimensions. Assuming the device is not notched` ); - this.log.debug(`Error: ${err.message}`); - } - return this._isSafariNotched ?? false; + this.log.debug(`Error: ${err.message}`); + } + return this._isSafariNotched ?? false; }, /** * @this {XCUITestDriver} */ async getExtraTranslateWebCoordsOffset(wvPos, realDims) { - let topOffset = 0; - let bottomOffset = 0; - - const isIphone = await this.getSafariIsIphone(); - - // No need to check whether the Smart App Banner or Tab Bar is visible or not - // if already defined by nativeWebTapTabBarVisibility or nativeWebTapSmartAppBannerVisibility in settings. - const { - nativeWebTapTabBarVisibility, - nativeWebTapSmartAppBannerVisibility, - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - safariTabBarPosition = util.compareVersions(this.opts.platformVersion, '>=', '15.0') && - isIphone - ? TAB_BAR_POSITION_BOTTOM - : TAB_BAR_POSITION_TOP, - } = this.settings.getSettings(); - let tabBarVisibility = _.lowerCase(String(nativeWebTapTabBarVisibility)); - let bannerVisibility = _.lowerCase(String(nativeWebTapSmartAppBannerVisibility)); - const tabBarPosition = _.lowerCase(String(safariTabBarPosition)); - - if (!VISIBILITIES.includes(tabBarVisibility)) { - tabBarVisibility = DETECT; - } - if (!VISIBILITIES.includes(bannerVisibility)) { - bannerVisibility = DETECT; - } + let topOffset = 0; + let bottomOffset = 0; + + const isIphone = await this.getSafariIsIphone(); + + // No need to check whether the Smart App Banner or Tab Bar is visible or not + // if already defined by nativeWebTapTabBarVisibility or nativeWebTapSmartAppBannerVisibility in settings. + const { + nativeWebTapTabBarVisibility, + nativeWebTapSmartAppBannerVisibility, + // @ts-expect-error - do not assign arbitrary properties to `this.opts` + safariTabBarPosition = util.compareVersions(this.opts.platformVersion, '>=', '15.0') && + isIphone + ? TAB_BAR_POSITION_BOTTOM + : TAB_BAR_POSITION_TOP, + } = this.settings.getSettings(); + let tabBarVisibility = _.lowerCase(String(nativeWebTapTabBarVisibility)); + let bannerVisibility = _.lowerCase(String(nativeWebTapSmartAppBannerVisibility)); + const tabBarPosition = _.lowerCase(String(safariTabBarPosition)); + + if (!VISIBILITIES.includes(tabBarVisibility)) { + tabBarVisibility = DETECT; + } + if (!VISIBILITIES.includes(bannerVisibility)) { + bannerVisibility = DETECT; + } - if (!TAB_BAR_POSSITIONS.includes(tabBarPosition)) { - throw new errors.InvalidArgumentError( + if (!TAB_BAR_POSSITIONS.includes(tabBarPosition)) { + throw new errors.InvalidArgumentError( `${safariTabBarPosition} is invalid as Safari tab bar position. Available positions are ${TAB_BAR_POSSITIONS}.` ); - } + } const isNotched = isIphone && (await this.getSafariIsNotched()); - const orientation = realDims.h > realDims.w ? 'PORTRAIT' : 'LANDSCAPE'; + const orientation = realDims.h > realDims.w ? 'PORTRAIT' : 'LANDSCAPE'; - const notchOffset = isNotched - ? // @ts-expect-error - do not assign arbitrary properties to `this.opts` - util.compareVersions(this.opts.platformVersion, '=', '13.0') - ? IPHONE_X_NOTCH_OFFSET_IOS_13 - : IPHONE_X_NOTCH_OFFSET_IOS - : 0; + const notchOffset = isNotched + ? // @ts-expect-error - do not assign arbitrary properties to `this.opts` + util.compareVersions(this.opts.platformVersion, '=', '13.0') + ? IPHONE_X_NOTCH_OFFSET_IOS_13 + : IPHONE_X_NOTCH_OFFSET_IOS + : 0; - const isScrolled = await this.execute('return document.documentElement.scrollTop > 0'); - if (isScrolled) { - topOffset = IPHONE_SCROLLED_TOP_BAR_HEIGHT + notchOffset; + const isScrolled = await this.execute('return document.documentElement.scrollTop > 0'); + if (isScrolled) { + topOffset = IPHONE_SCROLLED_TOP_BAR_HEIGHT + notchOffset; - if (isNotched) { - topOffset -= IPHONE_X_SCROLLED_OFFSET; - } - - // If the iPhone is landscape then there is no top bar - if (orientation === 'LANDSCAPE' && isIphone) { - topOffset = 0; - } - } else { - topOffset = tabBarPosition === TAB_BAR_POSITION_BOTTOM ? 0 : IPHONE_TOP_BAR_HEIGHT; - topOffset += notchOffset; - this.log.debug(`tabBarPosition and topOffset: ${tabBarPosition}, ${topOffset}`); - - if (isIphone) { - if (orientation === 'PORTRAIT') { - // The bottom bar is only visible when portrait - bottomOffset = IPHONE_BOTTOM_BAR_OFFSET; - } else { - topOffset = IPHONE_LANDSCAPE_TOP_BAR_HEIGHT; + if (isNotched) { + topOffset -= IPHONE_X_SCROLLED_OFFSET; } - } - if (orientation === 'LANDSCAPE' || !isIphone) { - if (tabBarVisibility === VISIBLE) { - topOffset += TAB_BAR_OFFSET; - } else if (tabBarVisibility === DETECT) { - // Tabs only appear if the device is landscape or if it's an iPad so we only check visibility in this case - // Assume that each tab bar is a WebView - const contextsAndViews = await this.getContextsAndViews(); - const tabs = contextsAndViews.filter((ctx) => ctx.id.startsWith('WEBVIEW_')); + // If the iPhone is landscape then there is no top bar + if (orientation === 'LANDSCAPE' && isIphone) { + topOffset = 0; + } + } else { + topOffset = tabBarPosition === TAB_BAR_POSITION_BOTTOM ? 0 : IPHONE_TOP_BAR_HEIGHT; + topOffset += notchOffset; + this.log.debug(`tabBarPosition and topOffset: ${tabBarPosition}, ${topOffset}`); + + if (isIphone) { + if (orientation === 'PORTRAIT') { + // The bottom bar is only visible when portrait + bottomOffset = IPHONE_BOTTOM_BAR_OFFSET; + } else { + topOffset = IPHONE_LANDSCAPE_TOP_BAR_HEIGHT; + } + } - if (tabs.length > 1) { - this.log.debug(`Found ${tabs.length} tabs. Assuming the tab bar is visible`); + if (orientation === 'LANDSCAPE' || !isIphone) { + if (tabBarVisibility === VISIBLE) { topOffset += TAB_BAR_OFFSET; + } else if (tabBarVisibility === DETECT) { + // Tabs only appear if the device is landscape or if it's an iPad so we only check visibility in this case + // Assume that each tab bar is a WebView + const contextsAndViews = await this.getContextsAndViews(); + const tabs = contextsAndViews.filter((ctx) => ctx.id.startsWith('WEBVIEW_')); + + if (tabs.length > 1) { + this.log.debug(`Found ${tabs.length} tabs. Assuming the tab bar is visible`); + topOffset += TAB_BAR_OFFSET; + } } } } - } - topOffset += await this.getExtraNativeWebTapOffset(isIphone, bannerVisibility); + topOffset += await this.getExtraNativeWebTapOffset(isIphone, bannerVisibility); - wvPos.y += topOffset; + wvPos.y += topOffset; realDims.h -= topOffset + bottomOffset; }, /** * @this {XCUITestDriver} */ async getExtraNativeWebTapOffset(isIphone, bannerVisibility) { - let offset = 0; + let offset = 0; - if (bannerVisibility === VISIBLE) { + if (bannerVisibility === VISIBLE) { offset += isIphone ? IPHONE_WEB_COORD_SMART_APP_BANNER_OFFSET : IPAD_WEB_COORD_SMART_APP_BANNER_OFFSET; - } else if (bannerVisibility === DETECT) { - // try to see if there is an Smart App Banner + } else if (bannerVisibility === DETECT) { + // try to see if there is an Smart App Banner const banners = /** @type {import('@appium/types').Element[]} */ ( await this.findNativeElementOrElements('accessibility id', 'Close app download offer', true) ); @@ -609,70 +612,70 @@ const extensions = { offset += isIphone ? IPHONE_WEB_COORD_SMART_APP_BANNER_OFFSET : IPAD_WEB_COORD_SMART_APP_BANNER_OFFSET; + } } - } - this.log.debug(`Additional native web tap offset computed: ${offset}`); - return offset; + this.log.debug(`Additional native web tap offset computed: ${offset}`); + return offset; }, /** * @this {XCUITestDriver} */ async nativeWebTap(el) { - const atomsElement = this.getAtomsElement(el); + const atomsElement = this.getAtomsElement(el); - // if strict native tap, do not try to do it with WDA directly + // if strict native tap, do not try to do it with WDA directly if ( !(await this.settings.getSettings()).nativeWebTapStrict && (await tapWebElementNatively(this, atomsElement)) ) { - return; - } - this.log.warn('Unable to do simple native web tap. Attempting to convert coordinates'); + return; + } + this.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 B.Promise.all([ - this.executeAtom('get_size', [atomsElement]), - this.executeAtom('get_top_left_coordinates', [atomsElement]), - ]); + // `get_top_left_coordinates` returns the wrong value sometimes, + // unless we pre-call both of these functions before the actual calls + await B.Promise.all([ + this.executeAtom('get_size', [atomsElement]), + this.executeAtom('get_top_left_coordinates', [atomsElement]), + ]); const [size, coordinates] = /** @type {[import('@appium/types').Size, import('@appium/types').Position]} */ ( await B.Promise.all([ - this.executeAtom('get_size', [atomsElement]), - this.executeAtom('get_top_left_coordinates', [atomsElement]), + this.executeAtom('get_size', [atomsElement]), + this.executeAtom('get_top_left_coordinates', [atomsElement]), ]) ); - const {width, height} = size; - let {x, y} = coordinates; - x += width / 2; - y += height / 2; + const {width, height} = size; + let {x, y} = coordinates; + x += width / 2; + y += height / 2; - this.curWebCoords = {x, y}; - await this.clickWebCoords(); + this.curWebCoords = {x, y}; + await this.clickWebCoords(); }, /** * @this {XCUITestDriver} */ async clickCoords(coords) { - await this.performTouch([ - { - action: 'tap', - options: coords, - }, - ]); + await this.performTouch([ + { + action: 'tap', + options: coords, + }, + ]); }, /** * @this {XCUITestDriver} */ async translateWebCoords(coords) { - this.log.debug(`Translating coordinates (${JSON.stringify(coords)}) to web coordinates`); + this.log.debug(`Translating coordinates (${JSON.stringify(coords)}) to web coordinates`); - // absolutize web coords + // absolutize web coords /** @type {import('@appium/types').Element|undefined|string} */ - let webview; - try { + let webview; + try { webview = /** @type {import('@appium/types').Element|undefined} */ ( await retryInterval( 5, @@ -681,61 +684,60 @@ const extensions = { await this.findNativeElementOrElements('class name', 'XCUIElementTypeWebView', false) ) ); - } catch (ign) {} + } catch (ign) {} - if (!webview) { - throw new Error(`No WebView found. Unable to translate web coordinates for native web tap.`); - } + if (!webview) { + throw new Error(`No WebView found. Unable to translate web coordinates for native web tap.`); + } - webview = util.unwrapElement(webview); + webview = util.unwrapElement(webview); const rect = /** @type {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 wvPos = {x: rect.x, y: rect.y}; + const realDims = {w: rect.width, h: rect.height}; - const cmd = '(function () { return {w: window.innerWidth, h: window.innerHeight}; })()'; - const wvDims = await this.remote.execute(cmd); + const cmd = '(function () { return {w: window.innerWidth, h: window.innerHeight}; })()'; + const wvDims = await this.remote.execute(cmd); - // keep track of implicit wait, and set locally to 0 - // https://github.com/appium/appium/issues/14988 - const implicitWaitMs = this.implicitWaitMs; + // keep track of implicit wait, and set locally to 0 + // https://github.com/appium/appium/issues/14988 + const implicitWaitMs = this.implicitWaitMs; this.setImplicitWait(0); - try { - await this.getExtraTranslateWebCoordsOffset(wvPos, realDims); - } finally { + try { + await this.getExtraTranslateWebCoordsOffset(wvPos, realDims); + } finally { this.setImplicitWait(implicitWaitMs); - } - - 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 + Math.round(yRatio * coords.y), - }; + } - // additional logging for coordinates, since it is sometimes broken - // see https://github.com/appium/appium/issues/9159 - this.log.debug(`Converted coordinates: ${JSON.stringify(newCoords)}`); - this.log.debug(` rect: ${JSON.stringify(rect)}`); - this.log.debug(` wvPos: ${JSON.stringify(wvPos)}`); - this.log.debug(` realDims: ${JSON.stringify(realDims)}`); - this.log.debug(` wvDims: ${JSON.stringify(wvDims)}`); - this.log.debug(` xRatio: ${JSON.stringify(xRatio)}`); - this.log.debug(` yRatio: ${JSON.stringify(yRatio)}`); + 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 + Math.round(yRatio * coords.y), + }; + + // additional logging for coordinates, since it is sometimes broken + // see https://github.com/appium/appium/issues/9159 + this.log.debug(`Converted coordinates: ${JSON.stringify(newCoords)}`); + this.log.debug(` rect: ${JSON.stringify(rect)}`); + this.log.debug(` wvPos: ${JSON.stringify(wvPos)}`); + this.log.debug(` realDims: ${JSON.stringify(realDims)}`); + this.log.debug(` wvDims: ${JSON.stringify(wvDims)}`); + this.log.debug(` xRatio: ${JSON.stringify(xRatio)}`); + this.log.debug(` yRatio: ${JSON.stringify(yRatio)}`); this.log.debug( - `Converted web coords ${JSON.stringify(coords)} ` + - `into real coords ${JSON.stringify(newCoords)}` + `Converted web coords ${JSON.stringify(coords)} into real coords ${JSON.stringify(newCoords)}` ); - return newCoords; - } + return newCoords; + } }, /** * @this {XCUITestDriver} */ async checkForAlert() { - return _.isString(await this.getAlertText()); + return _.isString(await this.getAlertText()); }, /** @@ -743,81 +745,81 @@ const extensions = { * @this {XCUITestDriver} */ async waitForAtom(promise) { - const timer = new timing.Timer().start(); + const timer = new timing.Timer().start(); - // need to check for alert while the atom is being executed. - // so notify ourselves when it happens - const timedAtomPromise = B.resolve(promise).timeout(ATOM_WAIT_TIMEOUT_MS); - const handlePromiseError = async (p) => { - try { - return await p; - } catch (err) { + // need to check for alert while the atom is being executed. + // so notify ourselves when it happens + const timedAtomPromise = B.resolve(promise).timeout(ATOM_WAIT_TIMEOUT_MS); + const handlePromiseError = async (p) => { + try { + return await p; + } catch (err) { const originalError = err instanceof B.AggregateError ? err[0] : err; - this.log.debug(`Error received while executing atom: ${originalError.message}`); - if (originalError instanceof B.TimeoutError) { + this.log.debug(`Error received while executing atom: ${originalError.message}`); + if (originalError instanceof B.TimeoutError) { throw new errors.TimeoutError( `Did not get any response for atom execution after ` + `${timer.getDuration().asMilliSeconds.toFixed(0)}ms` ); + } + throw originalError; } - throw originalError; - } - }; - // if the atom promise is fulfilled within ATOM_INITIAL_WAIT_MS - // then we don't need to check for an alert presence - await handlePromiseError(B.any([B.delay(ATOM_INITIAL_WAIT_MS), timedAtomPromise])); - if (timedAtomPromise.isFulfilled()) { - return await timedAtomPromise; - } + }; + // if the atom promise is fulfilled within ATOM_INITIAL_WAIT_MS + // then we don't need to check for an alert presence + await handlePromiseError(B.any([B.delay(ATOM_INITIAL_WAIT_MS), timedAtomPromise])); + if (timedAtomPromise.isFulfilled()) { + return await timedAtomPromise; + } - // ...otherwise make sure there is no unexpected alert covering the element - this._waitingAtoms.count++; + // ...otherwise make sure there is no unexpected alert covering the element + this._waitingAtoms.count++; - let onAlertCallback; - let onAppCrashCallback; - try { - // only restart the monitor if it is not running already - if (this._waitingAtoms.alertMonitor.isResolved()) { + let onAlertCallback; + let onAppCrashCallback; + try { + // only restart the monitor if it is not running already + if (this._waitingAtoms.alertMonitor.isResolved()) { this._waitingAtoms.alertMonitor = B.resolve( (async () => { - while (this._waitingAtoms.count > 0) { - try { - if (await this.checkForAlert()) { - this._waitingAtoms.alertNotifier.emit(ON_OBSTRUCTING_ALERT_EVENT); + while (this._waitingAtoms.count > 0) { + try { + if (await this.checkForAlert()) { + this._waitingAtoms.alertNotifier.emit(ON_OBSTRUCTING_ALERT_EVENT); + } + } catch (err) { + if (isErrorType(err, errors.InvalidElementStateError)) { + this._waitingAtoms.alertNotifier.emit(ON_APP_CRASH_EVENT, err); + } + } + await B.delay(OBSTRUCTING_ALERT_PRESENCE_CHECK_INTERVAL_MS); } - } catch (err) { - if (isErrorType(err, errors.InvalidElementStateError)) { - this._waitingAtoms.alertNotifier.emit(ON_APP_CRASH_EVENT, err); - } - } - await B.delay(ATOM_INITIAL_WAIT_MS / 2); - } })() ); - } + } - return await new B((resolve, reject) => { - onAlertCallback = () => reject(new errors.UnexpectedAlertOpenError()); - onAppCrashCallback = reject; - this._waitingAtoms.alertNotifier.once(ON_OBSTRUCTING_ALERT_EVENT, onAlertCallback); - this._waitingAtoms.alertNotifier.once(ON_APP_CRASH_EVENT, onAppCrashCallback); - handlePromiseError(timedAtomPromise) - // eslint-disable-next-line promise/prefer-await-to-then - .then(resolve) - .catch(reject); - }); - } finally { - if (onAlertCallback) { + return await new B((resolve, reject) => { + onAlertCallback = () => reject(new errors.UnexpectedAlertOpenError()); + onAppCrashCallback = reject; + this._waitingAtoms.alertNotifier.once(ON_OBSTRUCTING_ALERT_EVENT, onAlertCallback); + this._waitingAtoms.alertNotifier.once(ON_APP_CRASH_EVENT, onAppCrashCallback); + handlePromiseError(timedAtomPromise) + // eslint-disable-next-line promise/prefer-await-to-then + .then(resolve) + .catch(reject); + }); + } finally { + if (onAlertCallback) { this._waitingAtoms.alertNotifier.removeListener( ON_OBSTRUCTING_ALERT_EVENT, onAlertCallback ); + } + if (onAppCrashCallback) { + this._waitingAtoms.alertNotifier.removeListener(ON_APP_CRASH_EVENT, onAppCrashCallback); + } + this._waitingAtoms.count--; } - if (onAppCrashCallback) { - this._waitingAtoms.alertNotifier.removeListener(ON_APP_CRASH_EVENT, onAppCrashCallback); - } - this._waitingAtoms.count--; - } }, /** @@ -825,12 +827,12 @@ const extensions = { * @this {XCUITestDriver} */ async mobileWebNav(navType) { - this.remote.allowNavigationWithoutReload = true; - try { - await this.executeAtom('execute_script', [`history.${navType}();`, null]); - } finally { - this.remote.allowNavigationWithoutReload = false; - } + this.remote.allowNavigationWithoutReload = true; + try { + await this.executeAtom('execute_script', [`history.${navType}();`, null]); + } finally { + this.remote.allowNavigationWithoutReload = false; + } }, /** @@ -863,18 +865,18 @@ const extensions = { * @returns {Promise} * @throws {Error} if run on a real device or if the preferences argument is invalid * @this {XCUITestDriver} - */ + */ async mobileUpdateSafariPreferences(preferences) { - if (!this.isSimulator()) { - throw new Error('This extension is only available for Simulator'); - } - if (!_.isPlainObject(preferences)) { - throw new errors.InvalidArgumentError('"preferences" argument must be a valid object'); - } + if (!this.isSimulator()) { + throw new Error('This extension is only available for Simulator'); + } + if (!_.isPlainObject(preferences)) { + throw new errors.InvalidArgumentError('"preferences" argument must be a valid object'); + } - this.log.debug(`About to update Safari preferences: ${JSON.stringify(preferences)}`); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` - await this.opts.device.updateSafariSettings(preferences); + this.log.debug(`About to update Safari preferences: ${JSON.stringify(preferences)}`); + // @ts-expect-error - do not assign arbitrary properties to `this.opts` + await this.opts.device.updateSafariSettings(preferences); }, }; From a50ef21d883f50339bc26a4ba31cdc029fd1ed47 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 2 Jun 2023 09:40:44 +0200 Subject: [PATCH 2/3] Indent --- lib/commands/web.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/web.js b/lib/commands/web.js index 74136f59b..e2b93f639 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -875,7 +875,7 @@ const extensions = { } this.log.debug(`About to update Safari preferences: ${JSON.stringify(preferences)}`); - // @ts-expect-error - do not assign arbitrary properties to `this.opts` + // @ts-expect-error - do not assign arbitrary properties to `this.opts` await this.opts.device.updateSafariSettings(preferences); }, }; From 5316b1e1f493067e1267afd7c1c5987835c86c13 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Fri, 2 Jun 2023 09:43:07 +0200 Subject: [PATCH 3/3] fix indent --- lib/commands/web.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/commands/web.js b/lib/commands/web.js index e2b93f639..80a288620 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -220,9 +220,9 @@ const commands = { try { cookie.value = decodeURI(cookie.value); } catch (error) { - this.log.debug( - `Cookie ${cookie.name} was not decoded successfully. Cookie value: ${cookie.value}` - ); + this.log.debug( + `Cookie ${cookie.name} was not decoded successfully. Cookie value: ${cookie.value}` + ); this.log.warn(error); // Keep the original value }