Hybrid apps embed one or more WebView components inside a native shell. When your Appium test needs to interact with web content inside a WebView, it must switch from the native context into the web context. This context switch is the defining technique for hybrid app testing — get it right and testing hybrid screens is almost identical to Selenium. Get it wrong and every findElement call returns NoSuchElementException even when you can clearly see the element on screen.
Why context switching is required
Appium sessions start in NATIVE_APP context. In this context, the automation engine (UIAutomator2 or XCUITest) reads the native accessibility tree. A WebView is just an opaque rectangle in the native tree — the engine sees the WebView container element but cannot inspect its HTML contents.
To inspect and interact with the HTML inside a WebView, you must switch to the WEBVIEW context. When you do, Appium routes your commands through the browser's remote debugging protocol (Chrome DevTools Protocol on Android, WebKit Remote Debugging on iOS). Inside that context, standard Selenium-style CSS, XPath, and ID selectors work exactly as they do on the web.
Listing available contexts
After navigating to a screen that contains a WebView:
Set<String> contexts = driver.getContextHandles();
System.out.println(contexts);
// Prints: [NATIVE_APP, WEBVIEW_com.example.myapp]Context names follow this pattern:
NATIVE_APP— always presentWEBVIEW_<package>on AndroidWEBVIEW_<pid>on iOS (process ID of the WebView)
There may be multiple WebView contexts if the app has several WebViews loaded simultaneously.
Switching contexts
// Switch to the first WebView context
Set<String> contexts = driver.getContextHandles();
String webViewContext = contexts.stream()
.filter(ctx -> ctx.startsWith("WEBVIEW"))
.findFirst()
.orElseThrow(() -> new RuntimeException("No WebView context found"));
driver.context(webViewContext);
// Now use standard web locators
driver.findElement(By.cssSelector("input[name='email']")).sendKeys("user@test.com");
driver.findElement(By.id("submit-button")).click();
// Switch back to native
driver.context("NATIVE_APP");Enabling WebView debugging on Android
WebView debugging must be enabled in the app for Appium to connect to it. In a production app, this is typically enabled only for debug builds:
// In the app source (requires developer to add this)
if (BuildConfig.DEBUG) {
WebView.setWebContentsDebuggingEnabled(true);
}If the app does not have WebView debugging enabled, the WEBVIEW context will appear in getContextHandles() but will be empty or inaccessible. Contact your developers to ensure debug builds have this enabled.
On Android, you can also verify WebView debugging is working by opening Chrome on your desktop, navigating to chrome://inspect/#devices, and checking that the WebView appears in the inspectable pages list.
iOS WebView requirements
On iOS, WebView debugging works automatically on Simulators and on real devices that are connected to a Mac via USB and trusted. The app must use WKWebView (not the deprecated UIWebView). Appium's XCUITest driver handles the rest.
A complete hybrid test example
Consider a banking app where the main screens are native but the statements page is a WebView:
public class BankingHybridTest extends BaseTest {
@Test
public void viewAccountStatement() {
// Step 1: Navigate through native screens to the statements page
driver.findElement(AppiumBy.accessibilityId("accounts_tab")).click();
driver.findElement(AppiumBy.accessibilityId("statements_button")).click();
// Step 2: Wait for the WebView to load
new WebDriverWait(driver, Duration.ofSeconds(15))
.until(d -> d.getContextHandles().stream().anyMatch(ctx -> ctx.startsWith("WEBVIEW")));
// Step 3: Switch to the WebView context
String webCtx = driver.getContextHandles().stream()
.filter(ctx -> ctx.startsWith("WEBVIEW"))
.findFirst().get();
driver.context(webCtx);
// Step 4: Interact with web content using standard selectors
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.presenceOfElementLocated(By.cssSelector("table.statement-table")));
List<WebElement> rows = driver.findElements(By.cssSelector("table.statement-table tbody tr"));
Assert.assertFalse(rows.isEmpty(), "Statement should have transactions");
String firstAmount = rows.get(0).findElement(By.cssSelector(".amount")).getText();
Assert.assertTrue(firstAmount.startsWith("$"), "Amount should be in dollars");
// Step 5: Switch back to native for native UI interaction
driver.context("NATIVE_APP");
driver.findElement(AppiumBy.accessibilityId("back_button")).click();
}
}Context helper in your BaseTest
Add a reusable context switch to BaseTest:
protected void switchToWebView() {
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(d -> d.getContextHandles().size() > 1);
String webViewContext = driver.getContextHandles().stream()
.filter(ctx -> ctx.startsWith("WEBVIEW"))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No WEBVIEW context available"));
driver.context(webViewContext);
}
protected void switchToNative() {
driver.context("NATIVE_APP");
}Iframes inside WebViews
If the WebView contains iframes, the standard Selenium iframe handling applies:
// Switch to iframe
driver.switchTo().frame(driver.findElement(By.cssSelector("iframe#payment-frame")));
// Interact with iframe content
driver.findElement(By.id("card-number")).sendKeys("4111111111111111");
// Return to main document
driver.switchTo().defaultContent();Common problems
Empty WEBVIEW context. WebView debugging is not enabled in the app build. Ask developers to enable WebView.setWebContentsDebuggingEnabled(true).
WEBVIEW context appears but elements not found. The page inside the WebView has not finished loading. Wait for a specific element or use document.readyState:
new WebDriverWait(driver, Duration.ofSeconds(15))
.until(d -> ((JavascriptExecutor)d)
.executeScript("return document.readyState").equals("complete"));Multiple WEBVIEW contexts. The app has multiple WebViews loaded. List all contexts and choose the correct one by navigating through them or matching by URL:
for (String ctx : driver.getContextHandles()) {
if (ctx.startsWith("WEBVIEW")) {
driver.context(ctx);
String currentUrl = driver.getCurrentUrl();
if (currentUrl.contains("statements")) break;
}
}