|
7 | 7 | */ |
8 | 8 | package utam.core.selenium.appium; |
9 | 9 |
|
| 10 | +import static utam.core.framework.UtamLogger.error; |
| 11 | +import static utam.core.framework.UtamLogger.info; |
10 | 12 | import static utam.core.framework.UtamLogger.warning; |
11 | 13 |
|
| 14 | +import com.fasterxml.jackson.core.JsonProcessingException; |
| 15 | +import com.fasterxml.jackson.databind.JsonNode; |
| 16 | +import com.fasterxml.jackson.databind.ObjectMapper; |
12 | 17 | import io.appium.java_client.AppiumDriver; |
13 | 18 | import io.appium.java_client.remote.SupportsContextSwitching; |
| 19 | +import java.util.List; |
| 20 | +import java.util.ListIterator; |
| 21 | +import java.util.Map; |
14 | 22 | import java.util.Set; |
15 | 23 | import java.util.function.Supplier; |
16 | 24 | import org.openqa.selenium.By; |
| 25 | +import org.openqa.selenium.NoSuchWindowException; |
| 26 | +import org.openqa.selenium.Platform; |
17 | 27 | import org.openqa.selenium.WebDriverException; |
18 | 28 | import org.openqa.selenium.WebElement; |
19 | 29 | import utam.core.driver.Driver; |
|
34 | 44 | public class MobileDriverAdapter extends DriverAdapter implements Driver { |
35 | 45 |
|
36 | 46 | static final String WEBVIEW_CONTEXT_HANDLE_PREFIX = "WEBVIEW"; |
| 47 | + static final String WEBVIEW_CONTEXT_KEY_ANDROID = "webviewName"; |
| 48 | + static final String WEBVIEW_CONTEXT_KEY_IOS = "id"; |
| 49 | + static final String WEBVIEW_PAGES_KEY = "pages"; |
| 50 | + static final String WEBVIEW_PAGE_KEY = "id"; |
| 51 | + static final String WEBVIEW_PAGE_DESCRIPTION_KEY = "description"; |
| 52 | + static final String WEBVIEW_PAGE_DESCRIPTION_VISIBILITY_KEY = "visible"; |
| 53 | + static final String WEBVIEW_TITLE_KEY_ANDROID = "title"; |
| 54 | + static final String WEBVIEW_TITLE_KEY_IOS = "title"; |
37 | 55 | static final String NATIVE_CONTEXT_HANDLE = "NATIVE_APP"; |
38 | 56 | static final String ERR_BRIDGE_TITLE_NULL = "Bridge application title is null, please configure"; |
39 | 57 |
|
@@ -75,47 +93,135 @@ final AppiumDriver switchToWebView(String title) { |
75 | 93 | // from the return of getContextHandles. This is Android unique. |
76 | 94 | setPageContextToNative(); |
77 | 95 | } |
78 | | - Set<String> contextHandles = ((SupportsContextSwitching) appiumDriver).getContextHandles(); |
79 | | - for (String contextHandle : contextHandles) { |
80 | | - if (!contextHandle.equals(NATIVE_CONTEXT_HANDLE)) { |
81 | | - AppiumDriver newDriver; |
82 | | - try { |
83 | | - newDriver = |
84 | | - (AppiumDriver) ((SupportsContextSwitching) appiumDriver).context(contextHandle); |
85 | | - } catch (WebDriverException e) { |
86 | | - warning( |
87 | | - String.format( |
88 | | - "Context switch to webview '%s' failed. Error: %s", |
89 | | - contextHandle, e.getMessage())); |
90 | | - continue; |
| 96 | + // https://webdriver.io/docs/api/mobile/getContexts/ |
| 97 | + Map<String, Object> args = Map.of("returnDetailedContexts", true); |
| 98 | + Object result = appiumDriver.executeScript("mobile: getContexts", args); |
| 99 | + List<Map<String, Object>> contexts = (List<Map<String, Object>>) result; |
| 100 | + int contextsSize = contexts.size(); |
| 101 | + ListIterator<Map<String, Object>> li = contexts.listIterator(contextsSize); |
| 102 | + AppiumDriver newDriver = null; |
| 103 | + // Iterate in reverse since the WebView on screen is often the last context in the list. |
| 104 | + while (li.hasPrevious()) { |
| 105 | + Map<String, Object> context = li.previous(); |
| 106 | + if (appiumDriver.getCapabilities().getPlatformName().equals(Platform.ANDROID) |
| 107 | + && context.containsKey(WEBVIEW_CONTEXT_KEY_ANDROID)) { |
| 108 | + newDriver = checkAndroidWebViewContext(appiumDriver, title, context); |
| 109 | + } else if (appiumDriver.getCapabilities().getPlatformName().equals(Platform.IOS) |
| 110 | + && context.containsKey(WEBVIEW_CONTEXT_KEY_IOS)) { |
| 111 | + newDriver = checkIOSWebViewContext(appiumDriver, title, context); |
| 112 | + } |
| 113 | + if (newDriver != null) { |
| 114 | + return newDriver; |
| 115 | + } |
| 116 | + } |
| 117 | + return null; |
| 118 | + } |
| 119 | + |
| 120 | + /** |
| 121 | + * Check if the Android WebView context is the one with the desired title. |
| 122 | + * |
| 123 | + * @see <a |
| 124 | + * href="https://github.com/appium/appium-uiautomator2-driver/?tab=readme-ov-file#mobile-getcontexts">Appium |
| 125 | + * UIAutomator2 Driver</a> |
| 126 | + * @see <a href="https://chromedevtools.github.io/devtools-protocol/">Chrome DevTools Protocol</a> |
| 127 | + * @see <a |
| 128 | + * href="https://github.com/appium/appium-android-driver/blob/master/lib/commands/types.ts">Appium |
| 129 | + * Android Driver Types</a> |
| 130 | + * @param appiumDriver the Appium driver instance |
| 131 | + * @param title the desired WebView title to match |
| 132 | + * @param context the context map containing WebView information |
| 133 | + * @return the AppiumDriver if context switch successful, null otherwise |
| 134 | + */ |
| 135 | + private static AppiumDriver checkAndroidWebViewContext( |
| 136 | + AppiumDriver appiumDriver, String title, Map<String, Object> context) { |
| 137 | + String webviewName = (String) context.get(WEBVIEW_CONTEXT_KEY_ANDROID); |
| 138 | + if (!context.containsKey(WEBVIEW_PAGES_KEY)) { |
| 139 | + return null; |
| 140 | + } |
| 141 | + List<Map<String, Object>> pages = (List<Map<String, Object>>) context.get(WEBVIEW_PAGES_KEY); |
| 142 | + for (Map<String, Object> page : pages) { |
| 143 | + String newTitle = (String) page.get(WEBVIEW_TITLE_KEY_ANDROID); |
| 144 | + // 'description' doesn't cast to Map, only String |
| 145 | + String serializedDescription = (String) page.get(WEBVIEW_PAGE_DESCRIPTION_KEY); |
| 146 | + boolean visible = false; |
| 147 | + try { |
| 148 | + JsonNode descriptionNode = new ObjectMapper().readTree(serializedDescription); |
| 149 | + if (descriptionNode.has(WEBVIEW_PAGE_DESCRIPTION_VISIBILITY_KEY)) { |
| 150 | + visible = descriptionNode.get(WEBVIEW_PAGE_DESCRIPTION_VISIBILITY_KEY).asBoolean(); |
91 | 151 | } |
| 152 | + } catch (JsonProcessingException e) { |
| 153 | + error(e); |
| 154 | + } |
| 155 | + if (newTitle.equalsIgnoreCase(title) && visible) { |
| 156 | + String id = (String) page.get(WEBVIEW_PAGE_KEY); |
| 157 | + AppiumDriver newDriver = switchToContext(appiumDriver, webviewName, id); |
92 | 158 | if (newDriver != null) { |
93 | | - String newTitle = newDriver.getTitle(); |
94 | | - if (!newTitle.isEmpty() && newTitle.equalsIgnoreCase(title)) { |
95 | | - return newDriver; |
96 | | - } |
| 159 | + return newDriver; |
97 | 160 | } |
98 | 161 | } |
99 | 162 | } |
100 | | - // For the Appium chromedriver limitation to handle multiple WebViews, |
101 | | - // If switch to context fail to find the target WebView, then switch to |
102 | | - // use window |
103 | | - if ((mobilePlatform == MobilePlatformType.ANDROID |
104 | | - || mobilePlatform == MobilePlatformType.ANDROID_PHONE |
105 | | - || mobilePlatform == MobilePlatformType.ANDROID_TABLET) |
106 | | - && !isNativeContext()) { |
107 | | - Set<String> windowHandles = appiumDriver.getWindowHandles(); |
108 | | - for (String windowHandle : windowHandles) { |
109 | | - if (!windowHandle.equals(NATIVE_CONTEXT_HANDLE)) { |
110 | | - AppiumDriver newDriver = (AppiumDriver) appiumDriver.switchTo().window(windowHandle); |
111 | | - String currentTitle = newDriver.getTitle(); |
112 | | - if (!currentTitle.isEmpty() && currentTitle.equalsIgnoreCase(title)) { |
113 | | - return newDriver; |
114 | | - } |
| 163 | + return null; |
| 164 | + } |
| 165 | + |
| 166 | + /** |
| 167 | + * Check if the iOS WebView context is the one with the desired title. |
| 168 | + * |
| 169 | + * @see <a |
| 170 | + * href="https://github.com/appium/appium-xcuitest-driver/blob/master/docs/reference/execute-methods.md#mobile-getcontexts">Appium |
| 171 | + * XCUITest Driver - Execute Methods</a> |
| 172 | + * @see <a |
| 173 | + * href="https://github.com/appium/appium-xcuitest-driver/blob/master/lib/commands/types.ts">Appium |
| 174 | + * XCUITest Driver - Types</a> |
| 175 | + * @param appiumDriver the Appium driver instance |
| 176 | + * @param title the desired WebView title to match |
| 177 | + * @param context the context map containing WebView information |
| 178 | + * @return the AppiumDriver if context switch successful, null otherwise |
| 179 | + */ |
| 180 | + private static AppiumDriver checkIOSWebViewContext( |
| 181 | + AppiumDriver appiumDriver, String title, Map<String, Object> context) { |
| 182 | + String id = (String) context.get(WEBVIEW_CONTEXT_KEY_IOS); |
| 183 | + if (id.equals(NATIVE_CONTEXT_HANDLE)) { |
| 184 | + return null; |
| 185 | + } |
| 186 | + String newTitle = (String) context.get(WEBVIEW_TITLE_KEY_IOS); |
| 187 | + if (newTitle.equalsIgnoreCase(title)) { |
| 188 | + return switchToContext(appiumDriver, id, null); |
| 189 | + } |
| 190 | + return null; |
| 191 | + } |
| 192 | + |
| 193 | + private static AppiumDriver switchToContext( |
| 194 | + AppiumDriver appiumDriver, String context, String window) { |
| 195 | + SupportsContextSwitching contextSwitcher = (SupportsContextSwitching) appiumDriver; |
| 196 | + AppiumDriver newDriver = null; |
| 197 | + try { |
| 198 | + newDriver = (AppiumDriver) contextSwitcher.context(context); |
| 199 | + } catch (WebDriverException e) { |
| 200 | + error(e); |
| 201 | + error("Failed to switch to context: " + context); |
| 202 | + } |
| 203 | + // Window switching is on Android only. |
| 204 | + if (newDriver != null |
| 205 | + && window != null |
| 206 | + && appiumDriver.getCapabilities().getPlatformName().equals(Platform.ANDROID)) { |
| 207 | + // On emulators if it's the first window and Android < 13, it will fail if tried without |
| 208 | + // the |
| 209 | + // 'CDwindow-' prefix. |
| 210 | + try { |
| 211 | + newDriver.switchTo().window(window); |
| 212 | + } catch (NoSuchWindowException e1) { |
| 213 | + warning("Failed to switch to window: " + window); |
| 214 | + String cdWindow = "CDwindow-" + window; |
| 215 | + try { |
| 216 | + newDriver.switchTo().window(cdWindow); |
| 217 | + info("Successfully switched to window: " + cdWindow + ", retrying with prefix"); |
| 218 | + } catch (NoSuchWindowException e2) { |
| 219 | + error("Failed to switch to window: " + cdWindow); |
| 220 | + newDriver = null; |
115 | 221 | } |
116 | 222 | } |
117 | 223 | } |
118 | | - return null; |
| 224 | + return newDriver; |
119 | 225 | } |
120 | 226 |
|
121 | 227 | @Override |
|
0 commit comments