From 05432f9ee75eea54037e0c6ce02d8d782125c0c5 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 27 Feb 2024 01:28:29 -0800 Subject: [PATCH 01/12] retry with reconnection --- lib/commands/web.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/commands/web.js b/lib/commands/web.js index 9a3fc7f85..fcdcc1fba 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -351,9 +351,26 @@ const helpers = { */ 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); + + for (const retryCount in _.range(2)) { + try { + let promise = this.remote.executeAtom(atom, args, frames); + return await this.waitForAtom(promise); + } catch (err) { + // timeout + if (err) { + await this.stopRemote(); + this.remote = null; + await this.setContext(`WEBVIEW_${this.curContext}`); + continue + } + + throw err; + } + }; }, + + /** * @this {XCUITestDriver} */ From 398744ab81defec4d12135396bd1214939f0e9b2 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 28 Feb 2024 01:18:43 -0800 Subject: [PATCH 02/12] recovery with reconnection --- lib/commands/web.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/lib/commands/web.js b/lib/commands/web.js index fcdcc1fba..27e95eea0 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -352,20 +352,23 @@ const helpers = { async executeAtom(atom, args, alwaysDefaultFrame = false) { let frames = alwaysDefaultFrame === true ? [] : this.curWebFrames; - for (const retryCount in _.range(2)) { + for (const _retryCount of _.range(2)) { try { let promise = this.remote.executeAtom(atom, args, frames); return await this.waitForAtom(promise); } catch (err) { - // timeout - if (err) { - await this.stopRemote(); - this.remote = null; - await this.setContext(`WEBVIEW_${this.curContext}`); - continue + if (!(err instanceof errors.TimeoutError)) { + throw err; } - throw err; + this.log.warn(`Retry to send the same request after re-connecting to the remote debugger.`); + const currentContext = this.curContext; + + await this.stopRemote(); + this.remote = null; + + await this.connectToRemoteDebugger(); + await this.setContext(`WEBVIEW_${currentContext}`); } }; }, From 26702b8892e2a2f8e5765d4b86a1fcc7dd6138a4 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 28 Feb 2024 01:48:14 -0800 Subject: [PATCH 03/12] add webviewAtomWaitTimeout --- docs/reference/capabilities.md | 1 + lib/commands/web.js | 8 +++++++- lib/desired-caps.js | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/reference/capabilities.md b/docs/reference/capabilities.md index 6e68c7899..ae765d302 100644 --- a/docs/reference/capabilities.md +++ b/docs/reference/capabilities.md @@ -126,6 +126,7 @@ about capabilities, refer to the [Appium documentation](https://appium.io/docs/e |`appium:safariWebInspectorMaxFrameLength`| The maximum size in bytes of a single data frame for the Web Inspector. Too high values could introduce slowness and/or memory leaks. Too low values could introduce possible buffer overflow exceptions. Defaults to 20MB (`20*1024*1024`) |`1024`, `100*1024*1024` | |`appium:additionalWebviewBundleIds`|Array (or JSON array) of possible bundle identifiers for webviews. This is sometimes necessary if the Web Inspector is found to be returning a modified bundle identifier for the app. If the value includes `*`, XCUITest driver will return all available webview contexts on the device. Defaults to `[]`|`["io.appium.modifiedId', 'ABCDEF"]`, `["*"]`| |`appium:webviewConnectTimeout`|The time to wait, in `ms`, for the initial presence of webviews in MobileSafari or hybrid apps. Defaults to `0`|`5000`| +|`appium:webviewAtomWaitTimeout`|The time to wait, in `ms`, for each atom execution timeout of webviews in MobileSafari or hybrid apps. Defaults to `0`|`120000`| |`appium:safariIgnoreWebHostnames`| Provide a list of hostnames (comma-separated) that the Safari automation tools should ignore. This is to provide a workaround to prevent a webkit bug where the web context is unintentionally changed to a 3rd party website and the test gets stuck. The common culprits are search engines (yahoo, bing, google) and `about:blank` |`'www.yahoo.com, www.bing.com, www.google.com, about:blank'`| |`appium:nativeWebTap`| Enable native, non-javascript-based taps being in web context mode. Defaults to `false`. Warning: sometimes the preciseness of native taps could be broken, because there is no reliable way to map web element coordinates to native ones. | `true` | |`appium:nativeWebTapStrict`| Enabling this capability would skip the additional logic that tries to match web view elements to native ones by using their textual descriptions. Depending on the actual web view content this algorithm might sometimes be not very reliable and will slow down each click as we anyway fallback to the usual coordinates transformation flow if it fails. It is advised to enable strict tap if you use [mobile: calibrateWebToRealCoordinatesTranslation](./execute-methods.md#mobile-calibratewebtorealcoordinatestranslation) extension. Only applicable if `nativeWebTap` is enabled. `false` by default | `true` | diff --git a/lib/commands/web.js b/lib/commands/web.js index 27e95eea0..c1340312e 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -352,6 +352,8 @@ const helpers = { async executeAtom(atom, args, alwaysDefaultFrame = false) { let frames = alwaysDefaultFrame === true ? [] : this.curWebFrames; + // The web inspector could behave as no response over the timeout. + // Then, reconnecting the remote debugger could help to recover the condition. for (const _retryCount of _.range(2)) { try { let promise = this.remote.executeAtom(atom, args, frames); @@ -793,9 +795,13 @@ const extensions = { async waitForAtom(promise) { const timer = new timing.Timer().start(); + let atomWaitTimeoutMs = ATOM_WAIT_TIMEOUT_MS; + if (_.isNumber(this.opts.webviewAtomWaitTimeout)) { + atomWaitTimeoutMs = this.opts.webviewAtomWaitTimeout + } // 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 timedAtomPromise = B.resolve(promise).timeout(atomWaitTimeoutMs); const handlePromiseError = async (p) => { try { return await p; diff --git a/lib/desired-caps.js b/lib/desired-caps.js index 8032ded87..817910d5a 100644 --- a/lib/desired-caps.js +++ b/lib/desired-caps.js @@ -306,6 +306,9 @@ const desiredCapConstraints = /** @type {const} */ ({ webviewConnectTimeout: { isNumber: true, }, + webviewAtomWaitTimeout: { + isNumber: true, + }, iosSimulatorLogsPredicate: { isString: true, }, From 9bac2a5a2a1e7ce918ab285e4f349a19b2d48a77 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 28 Feb 2024 01:52:46 -0800 Subject: [PATCH 04/12] fix lint --- lib/commands/web.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/commands/web.js b/lib/commands/web.js index c1340312e..9b37e8279 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -354,6 +354,7 @@ const helpers = { // The web inspector could behave as no response over the timeout. // Then, reconnecting the remote debugger could help to recover the condition. + // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const _retryCount of _.range(2)) { try { let promise = this.remote.executeAtom(atom, args, frames); @@ -363,7 +364,7 @@ const helpers = { throw err; } - this.log.warn(`Retry to send the same request after re-connecting to the remote debugger.`); + this.log.warn(`retrying to send the same request after re-connecting to the Web Inspector.`); const currentContext = this.curContext; await this.stopRemote(); @@ -797,7 +798,7 @@ const extensions = { let atomWaitTimeoutMs = ATOM_WAIT_TIMEOUT_MS; if (_.isNumber(this.opts.webviewAtomWaitTimeout)) { - atomWaitTimeoutMs = this.opts.webviewAtomWaitTimeout + atomWaitTimeoutMs = this.opts.webviewAtomWaitTimeout; } // need to check for alert while the atom is being executed. // so notify ourselves when it happens From 77d985e06e9376d146f717badab4d768715324f9 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 28 Feb 2024 01:57:28 -0800 Subject: [PATCH 05/12] >0 --- 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 9b37e8279..cf235f033 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -797,7 +797,7 @@ const extensions = { const timer = new timing.Timer().start(); let atomWaitTimeoutMs = ATOM_WAIT_TIMEOUT_MS; - if (_.isNumber(this.opts.webviewAtomWaitTimeout)) { + if (_.isNumber(this.opts.webviewAtomWaitTimeout) && this.opts.webviewAtomWaitTimeout > 0) { atomWaitTimeoutMs = this.opts.webviewAtomWaitTimeout; } // need to check for alert while the atom is being executed. From 050c5fa0a60a0caad57a28aaf5d2386a5d60ebb7 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Wed, 28 Feb 2024 02:03:22 -0800 Subject: [PATCH 06/12] correct docs --- docs/reference/capabilities.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/capabilities.md b/docs/reference/capabilities.md index ae765d302..0be13d127 100644 --- a/docs/reference/capabilities.md +++ b/docs/reference/capabilities.md @@ -126,7 +126,7 @@ about capabilities, refer to the [Appium documentation](https://appium.io/docs/e |`appium:safariWebInspectorMaxFrameLength`| The maximum size in bytes of a single data frame for the Web Inspector. Too high values could introduce slowness and/or memory leaks. Too low values could introduce possible buffer overflow exceptions. Defaults to 20MB (`20*1024*1024`) |`1024`, `100*1024*1024` | |`appium:additionalWebviewBundleIds`|Array (or JSON array) of possible bundle identifiers for webviews. This is sometimes necessary if the Web Inspector is found to be returning a modified bundle identifier for the app. If the value includes `*`, XCUITest driver will return all available webview contexts on the device. Defaults to `[]`|`["io.appium.modifiedId', 'ABCDEF"]`, `["*"]`| |`appium:webviewConnectTimeout`|The time to wait, in `ms`, for the initial presence of webviews in MobileSafari or hybrid apps. Defaults to `0`|`5000`| -|`appium:webviewAtomWaitTimeout`|The time to wait, in `ms`, for each atom execution timeout of webviews in MobileSafari or hybrid apps. Defaults to `0`|`120000`| +|`appium:webviewAtomWaitTimeout`|The time to wait, in `ms`, for each atom execution timeout of webviews in MobileSafari or hybrid apps. Defaults to `120000`. If the value was zero or less, the timeout keeps the default value. |`20000`| |`appium:safariIgnoreWebHostnames`| Provide a list of hostnames (comma-separated) that the Safari automation tools should ignore. This is to provide a workaround to prevent a webkit bug where the web context is unintentionally changed to a 3rd party website and the test gets stuck. The common culprits are search engines (yahoo, bing, google) and `about:blank` |`'www.yahoo.com, www.bing.com, www.google.com, about:blank'`| |`appium:nativeWebTap`| Enable native, non-javascript-based taps being in web context mode. Defaults to `false`. Warning: sometimes the preciseness of native taps could be broken, because there is no reliable way to map web element coordinates to native ones. | `true` | |`appium:nativeWebTapStrict`| Enabling this capability would skip the additional logic that tries to match web view elements to native ones by using their textual descriptions. Depending on the actual web view content this algorithm might sometimes be not very reliable and will slow down each click as we anyway fallback to the usual coordinates transformation flow if it fails. It is advised to enable strict tap if you use [mobile: calibrateWebToRealCoordinatesTranslation](./execute-methods.md#mobile-calibratewebtorealcoordinatestranslation) extension. Only applicable if `nativeWebTap` is enabled. `false` by default | `true` | From 9e300a00c3dfbffd98ea26b123f2bf1d11bcd995 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Thu, 11 Jul 2024 00:48:05 -0700 Subject: [PATCH 07/12] Update web.js --- 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 63233b511..ae5f24c46 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -352,7 +352,7 @@ const helpers = { async executeAtom(atom, args, alwaysDefaultFrame = false) { let frames = alwaysDefaultFrame === true ? [] : this.curWebFrames; - // The web inspector could behave as no response over the timeout. + // The web inspector could behave as no response over the timeout, especially with iOS/iPadOS 17 family. // Then, reconnecting the remote debugger could help to recover the condition. // eslint-disable-next-line @typescript-eslint/no-unused-vars for (const _retryCount of _.range(2)) { From 9bb26163cb6ce273226228757b673a5c67d451f7 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sun, 14 Jul 2024 17:13:14 -0700 Subject: [PATCH 08/12] add ios 17 --- lib/commands/web.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/commands/web.js b/lib/commands/web.js index ae5f24c46..6b6dfe740 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -352,6 +352,11 @@ const helpers = { async executeAtom(atom, args, alwaysDefaultFrame = false) { let frames = alwaysDefaultFrame === true ? [] : this.curWebFrames; + if (!util.compareVersions(/** @type {string} */ (this.opts.platformVersion), '=', '17')) { + let promise = this.remote.executeAtom(atom, args, frames); + return await this.waitForAtom(promise); + } + // The web inspector could behave as no response over the timeout, especially with iOS/iPadOS 17 family. // Then, reconnecting the remote debugger could help to recover the condition. // eslint-disable-next-line @typescript-eslint/no-unused-vars From 97858d49dad074fcd31dad0e051f5af79af5607b Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sun, 14 Jul 2024 22:03:51 -0700 Subject: [PATCH 09/12] tweak the logic --- lib/commands/web.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/commands/web.js b/lib/commands/web.js index 6b6dfe740..b9d3ecd1d 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -52,6 +52,8 @@ const TAB_BAR_POSITION_TOP = 'top'; const TAB_BAR_POSITION_BOTTOM = 'bottom'; const TAB_BAR_POSSITIONS = [TAB_BAR_POSITION_TOP, TAB_BAR_POSITION_BOTTOM]; +const RECONNECT_COUNT_ATOMS_IOS17 = 2 + /** * @this {XCUITestDriver} * @param {any} atomsElement @@ -352,15 +354,17 @@ const helpers = { async executeAtom(atom, args, alwaysDefaultFrame = false) { let frames = alwaysDefaultFrame === true ? [] : this.curWebFrames; - if (!util.compareVersions(/** @type {string} */ (this.opts.platformVersion), '=', '17')) { + if ( + util.compareVersions(/** @type {string} */ (this.opts.platformVersion), '<', '17') || + util.compareVersions(/** @type {string} */ (this.opts.platformVersion), '>', '17') + ) { let promise = this.remote.executeAtom(atom, args, frames); return await this.waitForAtom(promise); } // The web inspector could behave as no response over the timeout, especially with iOS/iPadOS 17 family. // Then, reconnecting the remote debugger could help to recover the condition. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const _retryCount of _.range(2)) { + for (const _retryCount of _.range(RECONNECT_COUNT_ATOMS_IOS17)) { try { let promise = this.remote.executeAtom(atom, args, frames); return await this.waitForAtom(promise); @@ -369,7 +373,7 @@ const helpers = { throw err; } - this.log.warn(`retrying to send the same request after re-connecting to the Web Inspector.`); + this.log.warn(`Retrying to send the same request after re-connecting to the Web Inspector.`); const currentContext = this.curContext; await this.stopRemote(); From 920b28f1761cdff1ce2eb116f09dc4516150fab0 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sun, 14 Jul 2024 22:04:17 -0700 Subject: [PATCH 10/12] add FIXME --- lib/commands/web.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/commands/web.js b/lib/commands/web.js index b9d3ecd1d..15c517579 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -364,6 +364,7 @@ const helpers = { // The web inspector could behave as no response over the timeout, especially with iOS/iPadOS 17 family. // Then, reconnecting the remote debugger could help to recover the condition. + // FIXME: Remove this reconnect logic after dropping iOS 17. for (const _retryCount of _.range(RECONNECT_COUNT_ATOMS_IOS17)) { try { let promise = this.remote.executeAtom(atom, args, frames); From 04167f1658ea76c7d2e2a5fbd70fa23fa8c4d06f Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Sun, 14 Jul 2024 22:06:43 -0700 Subject: [PATCH 11/12] simplify a bit --- lib/commands/web.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/commands/web.js b/lib/commands/web.js index 15c517579..218802ece 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -358,8 +358,7 @@ const helpers = { util.compareVersions(/** @type {string} */ (this.opts.platformVersion), '<', '17') || util.compareVersions(/** @type {string} */ (this.opts.platformVersion), '>', '17') ) { - let promise = this.remote.executeAtom(atom, args, frames); - return await this.waitForAtom(promise); + return await this.waitForAtom(this.remote.executeAtom(atom, args, frames)); } // The web inspector could behave as no response over the timeout, especially with iOS/iPadOS 17 family. @@ -367,8 +366,7 @@ const helpers = { // FIXME: Remove this reconnect logic after dropping iOS 17. for (const _retryCount of _.range(RECONNECT_COUNT_ATOMS_IOS17)) { try { - let promise = this.remote.executeAtom(atom, args, frames); - return await this.waitForAtom(promise); + return await this.waitForAtom(this.remote.executeAtom(atom, args, frames)); } catch (err) { if (!(err instanceof errors.TimeoutError)) { throw err; From 50cda102bfce939ebc81758eb6ca0d4efa6dcbdc Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Mon, 15 Jul 2024 09:09:04 -0700 Subject: [PATCH 12/12] Update web.js --- 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 218802ece..91622c210 100644 --- a/lib/commands/web.js +++ b/lib/commands/web.js @@ -52,7 +52,7 @@ const TAB_BAR_POSITION_TOP = 'top'; const TAB_BAR_POSITION_BOTTOM = 'bottom'; const TAB_BAR_POSSITIONS = [TAB_BAR_POSITION_TOP, TAB_BAR_POSITION_BOTTOM]; -const RECONNECT_COUNT_ATOMS_IOS17 = 2 +const RECONNECT_COUNT_ATOMS_IOS17 = 2; /** * @this {XCUITestDriver} @@ -364,7 +364,7 @@ const helpers = { // The web inspector could behave as no response over the timeout, especially with iOS/iPadOS 17 family. // Then, reconnecting the remote debugger could help to recover the condition. // FIXME: Remove this reconnect logic after dropping iOS 17. - for (const _retryCount of _.range(RECONNECT_COUNT_ATOMS_IOS17)) { + for (const retryCount of _.range(RECONNECT_COUNT_ATOMS_IOS17)) { try { return await this.waitForAtom(this.remote.executeAtom(atom, args, frames)); } catch (err) { @@ -372,7 +372,7 @@ const helpers = { throw err; } - this.log.warn(`Retrying to send the same request after re-connecting to the Web Inspector.`); + this.log.warn(`Retrying to send the same request after re-connecting to the Web Inspector. (${retryCount})`); const currentContext = this.curContext; await this.stopRemote();