Skip to content

Commit 2eb426d

Browse files
refactor: modify context switching to be faster on mobile (#239)
1 parent 6b67ff3 commit 2eb426d

File tree

2 files changed

+290
-123
lines changed

2 files changed

+290
-123
lines changed

utam-core/src/main/java/utam/core/selenium/appium/MobileDriverAdapter.java

Lines changed: 139 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,23 @@
77
*/
88
package utam.core.selenium.appium;
99

10+
import static utam.core.framework.UtamLogger.error;
11+
import static utam.core.framework.UtamLogger.info;
1012
import static utam.core.framework.UtamLogger.warning;
1113

14+
import com.fasterxml.jackson.core.JsonProcessingException;
15+
import com.fasterxml.jackson.databind.JsonNode;
16+
import com.fasterxml.jackson.databind.ObjectMapper;
1217
import io.appium.java_client.AppiumDriver;
1318
import io.appium.java_client.remote.SupportsContextSwitching;
19+
import java.util.List;
20+
import java.util.ListIterator;
21+
import java.util.Map;
1422
import java.util.Set;
1523
import java.util.function.Supplier;
1624
import org.openqa.selenium.By;
25+
import org.openqa.selenium.NoSuchWindowException;
26+
import org.openqa.selenium.Platform;
1727
import org.openqa.selenium.WebDriverException;
1828
import org.openqa.selenium.WebElement;
1929
import utam.core.driver.Driver;
@@ -34,6 +44,14 @@
3444
public class MobileDriverAdapter extends DriverAdapter implements Driver {
3545

3646
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";
3755
static final String NATIVE_CONTEXT_HANDLE = "NATIVE_APP";
3856
static final String ERR_BRIDGE_TITLE_NULL = "Bridge application title is null, please configure";
3957

@@ -75,47 +93,135 @@ final AppiumDriver switchToWebView(String title) {
7593
// from the return of getContextHandles. This is Android unique.
7694
setPageContextToNative();
7795
}
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();
91151
}
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);
92158
if (newDriver != null) {
93-
String newTitle = newDriver.getTitle();
94-
if (!newTitle.isEmpty() && newTitle.equalsIgnoreCase(title)) {
95-
return newDriver;
96-
}
159+
return newDriver;
97160
}
98161
}
99162
}
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;
115221
}
116222
}
117223
}
118-
return null;
224+
return newDriver;
119225
}
120226

121227
@Override

0 commit comments

Comments
 (0)