
Appium Web App Testing — Python
Appium Web App Testing — Python
Besides native apps, we can use Appium to automate web browsers on mobile devices, just as if we were using Selenium for desktop browsers.

Let’s learn how to do that.

What is Mobile Web Automation?

Now that we’re familiar with configuring and launching a native mobile app with Appium, let’s proceed to learn how to use Appium to test something other than a native app. Mobile devices are equipped with their own web browsers, allowing users to access a wide range of websites and web applications. Often, a significant portion of our device usage involves browsing the web. Consequently, when testing a web app, it’s imperative to ensure compatibility across both mobile and desktop browsers, prioritizing testing on mobile browsers.
Appium enables web browser automation on Android (Chrome) and iOS (Safari).
Fortunately, Appium facilitates the automation of standard browsers on both Android and iOS platforms, specifically Chrome and Safari, respectively. It offers distinct mechanisms to support Safari and Chrome on each platform. Nonetheless, the outcome remains consistent: automating a mobile browser with Appium mirrors the process of automating a desktop web browser using a Selenium driver.
To automate a mobile browser, use ‘browserName' capability instead of ‘app’. It‘s value should be either ‘Safari' or ‘Chrome'.
To instruct Appium to automate a web browser instead of a native app, we utilize the browserName capability, akin to Selenium, rather than the app capability used for launching native apps. The browserName capability should be assigned either Safari or Chrome as its value. Currently, automation of other mobile browsers is not facilitated through this capability.
📝 Note: If you’re interested in automating the Chromium or Gecko driver on a desktop (Linux, Mac, or Windows) check out the Appium Chromium driver and Gecko driver. Don’t worry, we aren’t about to use those, or at least not yet. This post focuses on mobile Safari and Chrome that can be passed as the browserName value in the capabilities and does not require installation of any additional drivers, when we have the UIAutomator2 and XCUITest drivers already installed (as we’d done in the last section of the Appium Setup post).

⏩ Shortcut: Replace the browserName capability with appPackage/appActivity (Android) or app with a bundle ID (iOS) for a more latent load, since a mobile browser is actually a hybrid mobile app in itself. Learn more on how and why this is possible in my prior post on Automating System Apps with Appium. The below sample shows how we’d launch Chrome on a Samsung device. Take a note of applicable appPackage and appActivity of your mobile browser and replace the browserName respectively, if you choose this route.
{
"platformName": "Android",
"appium:options": {
"deviceName": "SM-G375H",
"udid": "RBZM317318Y",
"autoAcceptsAlerts": true,
"automationName": "UIAutomator2",
"appPackage": "com.android.chrome",
"appActivity": "com.google.android.apps.chrome.Main"
}
}
With a mobile web session, we have access to all web-based Selenium WebDriver commands and capabilities (e.g., the CSS locator strategy).
Upon initiating a session using browserName, we gain access to all the standard WebDriver commands typically unavailable in a native session. This includes commands such as retrieving the webpage title or navigating to a specific URL. Additionally, we can utilize web-based locator strategies like CSS selectors or tag names, which aren’t typically applicable in a native session.

We still have access to all Appium’s device commands, etc., while automating the browser.
Certainly, we’ll also benefit from numerous mobile-related functionalities offered by Appium. While automating a webpage, native element identification isn’t possible. However, we can perform various other tasks such as adjusting device orientation, launching additional apps or activities, and more.

Web Automation with Mobile Safari

The ‘remote debugger’ enables automation. Safari permits external applications to connect to a designated port it opens, facilitating the transmission of commands or retrieval of information regarding web pages. Simulators are straightforward to connect to, while real devices necessitate a remote debugger proxy server.
How does Appium enable webpage automation in mobile Safari? Essentially, it utilizes the remote debugger mechanism. Safari features its own version of the remote debugger, permitting external applications to connect via a specialized port that Safari opens on the host. Through this port, various commands can be sent, prompting the browser to execute different actions. This capability extends to mobile Safari as well, which also has its own remote debugger port for external connections. When discussing the iOS simulator, this port is readily accessible on the host machine, simplifying the connection process. However, with Safari on real devices, the port operates within the device’s network. Consequently, to establish a connection, we first need to run a proxy server on our local machine to forward traffic to the device, allowing Appium to connect to this port.
To ensure that the remote debugger is activated, enable ‘Remote Automation’ in Safari’s advanced settings.
To ensure the availability of this port, a specific setting must be activated on the phone, as illustrated in the image below, within the advanced section of the Safari settings. This applies to both simulators and real devices. However, it is enabled by default for simulators, eliminating the need for any additional steps.

Appium converts WebDriver commands from us into Remote Debug Protocol commands for Safari’s remote debugger.
This is how Appium enables regular WebDriver commands in this scenario: when we send a WebDriver command, Appium initially examines its nature. If it pertains to mobile Safari, Appium transforms the command into the suitable type of remote debugger command and dispatches it to the remote debugger port where Safari awaits such commands. This entire process occurs behind the scenes, shielding us from the need to be aware or concerned about Appium translating our command into a anything unexpected. However, understanding this process is beneficial, particularly because it clarifies certain constraints in Appium’s Safari support.
The remote debugger doesn’t grant us full control over the browser, only allowing us to accomplish tasks that are within the ballpark of JavaScript possibilities.
As we’re limited to the remote debugger protocol for Safari, there are certain functionalities that aren’t fully supported. For instance, when attempting to click an element, we essentially trigger a JavaScript event on the corresponding element, which differs somewhat from the technical process of a user physically tapping the screen. Additionally, we can’t utilize the remote debugger protocol’s JavaScript functions to automate alerts, prompting Appium to devise alternative solutions to overcome this limitation. Overall, it’s important to note that Appium’s Safari support differs from the capabilities of the official SafariDriver from Apple on desktop, as the latter is capable of integrating with the Safari browser at a deeper level.

Mobile Safari — Practical Walkthrough
It’s time to transition from theory to practice. For our testing, we’re using an app called The Internet and it was originally developed by Dave Haeffner, someone who’s contributed a lot to the Selenium community. The Internet is located at https://the-internet.herokuapp.com:

We’ll execute the following test steps:
# TODO: Navigate to the app URL.
# TODO: Click Form Authentication.
# TODO: Enter credentials and click Login.
# TODO: Click Logout.
# TODO: Verify presense of 'logged out' text.
The Python code for a desktop browser execution will look something like this:
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
try:
wait = WebDriverWait(driver, 10)
driver.get('https://the-internet.herokuapp.com')
form_auth_link = wait.until(EC.presence_of_element_located(
(By.LINK_TEXT, 'Form Authentication')))
form_auth_link.click()
username = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '#username')))
username.send_keys('tomsmith')
password = driver.find_element(By.CSS_SELECTOR, '#password')
password.send_keys('SuperSecretPassword!')
driver.find_element(By.CSS_SELECTOR, 'button[type=submit]').click()
wait.until(EC.presence_of_element_located(
(By.LINK_TEXT, 'Logout'))).click()
flash = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '#flash')))
assert 'logged out' in flash.text
finally:
driver.quit()
Let’s adjust the code to fit our iOS testing scenario in a new web_ios.py file:
from appium import webdriver
from appium.options.common import AppiumOptions
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
APPIUM = "http://localhost:4723"
CAPS = {
"platformName": "iOS",
"browserName": "Safari",
"appium:options": {
"platformVersion": "17.4",
"deviceName": "iPhone 15 Pro Max",
"automationName": "XCUITest",
"showXcodeLog": True # optional - check the xcodebuild's log details
}
}
OPTIONS = AppiumOptions().load_capabilities(CAPS)
driver = webdriver.Remote(
command_executor=APPIUM,
options=OPTIONS
)
try:
wait = WebDriverWait(driver, 3)
driver.get('https://the-internet.herokuapp.com')
form_auth_link = wait.until(EC.presence_of_element_located(
(By.LINK_TEXT, 'Form Authentication')))
form_auth_link.click()
username = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '#username')))
username.send_keys('tomsmith')
password = driver.find_element(By.CSS_SELECTOR, '#password')
password.send_keys('SuperSecretPassword!')
driver.find_element(By.CSS_SELECTOR, 'button[type=submit]').click()
wait.until(EC.presence_of_element_located(
(By.LINK_TEXT, 'Logout'))).click()
wait.until(EC.url_to_be('https://the-internet.herokuapp.com/login'))
flash = wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '#flash')))
assert 'logged out' in flash.text
finally:
driver.quit()
The main modifications are:
- delete webdriver.Chrome()
- add iOS-specific capabilities, including “browserName”: ”Safari”
- instantiate Appium driver with webdriver.Remote()
- add an explicit wait with the url_to_be() condition to verify the assertion
Different WebDriver Race Conditions on Desktop (Selenium) and Mobile (Appium)
In case you wonder why need this extra wait, there’s an explanation for it. We need some way to make sure that we don’t read the text value of the flash element until the new page has transitioned. We don’t have to worry about this with Selenium, since Selenium drivers know that when we click an element, a new page might load, and they’re hooked deep into the browser so they can detect such events. With Appium’s remote debugger based support for Safari, we don’t have that luxury. Instead, we need to guard this assertion with another type of wait — a wait for the page itself to be what we expect. That’s why we add an expected condition right before we retrieve the flash element:
wait.until(EC.url_to_be('https://the-internet.herokuapp.com/login'))
What we’re doing here is making sure that the URL is what we expect before we try to find the new element. This is a wait that won’t hurt anything in the Selenium case, but it certainly helps us in this Appium case here. Without it, we’d get an error during the test script:
Traceback (most recent call last):
File "web_ios.py", line XX, in
assert "logged out" in flash.text
AssertionError
It’s an AssertionError, telling us that the string “logged out” was not in the flash.text property as we expected. If “logged out” was not part of the flash element text property, let’s find out what that property is.
We can do a sanity check and put a print statement print(flash.text) just before the line that’s causing the problem. When we adjust the code to print out the property, run the the script again, we are able to see what's happening. We get the same error, but this time we see that a line with an ‘x’ gets printed out above it. We can ignore this line, it’s just a small close button.
You logged into a secure area!
×
Traceback (most recent call last):
File "web_ios.py", line XX, in
assert "logged out" in text
AssertionError
What’s important here is the line above the ‘x’ that states “You are logged into a secure area!”. This tells us that Appium is looking for the text of the flash element, but it’s so fast that it’s actually finding the text of the flash element before the new text has loaded with the “logged out” text. It’s a race condition between the app and the test script. Typically we’d expect the app to win, but our test is winning instead, which is leading to a problem. One of my earlier posts addresses locator strategies that can help with synchronizing the app and the client’s test code.
This is an example of tracking down a race condition and using an explicit wait to help get rid of it, increasing our test stability all around. It’s an illustration that in general we can just take Selenium tests and then run them without change on Appium, but we might run into situations where we have an opportunity to increase our test stability for both web and mobile by running it on different platforms.
Now that we’ve taken a detour into potential synchronization differences between the desktop and mobile browsers, let’s finally run our script on the Safari browser on iOS. Make sure to have the Appium server running and a simulator up as well before heading over to the terminal and running the script:
python3 web_ios.py

Awesome, our script works! Now, let’s consider launching the Safari app with it’s bundle ID. The reason behind this approach is the intent to take a shortcut to the desired app state. It’s a faster way to launch an app. As discussed in my prior post, the bundle ID is a unique identifier given to each app by its developer. In the case of Apple apps, their bundle IDs start with com.apple. The lists that show the bundle IDs of each app for each version of iOS are available online.

In a new Python file web_ios_bundle_id.py, we’ll modify the code as follows:
- remove “browserName”: “Safari” from W3C capabilities
- add “app”: “com.apple.mobilesafari” to Appium capabilities
So our iOS capabilities now look like this:
CAPS = {
"platformName": "iOS",
"appium:options": {
"platformVersion": "17.4",
"deviceName": "iPhone 15 Pro Max",
"automationName": "XCUITest",
"app": "com.apple.mobilesafari",
"showXcodeLog": True # optional - check the xcodebuild's log details
}
}
However, if we run the code as is, we’ll get the following error:

So it’s essential to remember that if we launch Safari by its bundle ID, we also have to adjust the locator strategies to those acceptable in the native mobile sessions:
from appium import webdriver
from appium.options.common import AppiumOptions
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
APPIUM = "http://localhost:4723"
CAPS = {
"platformName": "iOS",
"appium:options": {
"platformVersion": "17.4",
"deviceName": "iPhone 15 Pro Max",
"automationName": "XCUITest",
"app": "com.apple.mobilesafari",
"showXcodeLog": True # optional - check the xcodebuild's log details
}
}
OPTIONS = AppiumOptions().load_capabilities(CAPS)
driver = webdriver.Remote(
command_executor=APPIUM,
options=OPTIONS
)
try:
wait = WebDriverWait(driver, 3)
driver.get('https://the-internet.herokuapp.com')
form_auth_link = wait.until(EC.presence_of_element_located(
(AppiumBy.XPATH, "//XCUIElementTypeStaticText[@name='Form Authentication']")))
form_auth_link.click()
username = wait.until(EC.presence_of_element_located(
(AppiumBy.XPATH, "//XCUIElementTypeTextField[@name='Username']")))
username.send_keys("tomsmith")
password = driver.find_element(By.XPATH, "//XCUIElementTypeSecureTextField[@name='Password']")
password.send_keys('SuperSecretPassword!')
driver.find_element(By.XPATH, "//XCUIElementTypeButton[contains(@name, 'Login')]").click()
wait.until(EC.presence_of_element_located(
(AppiumBy.XPATH, "//XCUIElementTypeStaticText[@name='Logout']")))
.click()
# Skipping assertion, since the native context does not support the url_to_be() method.
# Tried wait.until(EC.invisibility_of_element_located((AppiumBy.XPATH, "//XCUIElementTypeStaticText[@name='You logged into a secure area!']"))), but it didn't work.
# Page source at this step only has the flash text with the '...logged into...', no sign of '...logged out...'
finally:
driver.quit()
After replacing the locator strategies, the code runs the same way, with one exception. The native context doesn’t support the url_to_be() method, and the page source contains nothing close the “logged out” verbiage for the app in the logged out state. I tried sleeps and waits with no success. If you find a way to resolve this assertion, drop a comment.

Web Automation with Mobile Chrome

ChromeDriver works on Android.
We’re fortunate to have a remarkable advantage on Android, known as ChromeDriver. It’s a software developed by Google, serving as an independent WebDriver server specifically for Chrome. Thankfully, its design enables seamless compatibility with Chrome on Android, not just the desktop version.
When Chrome automation is needed, Appium operates ChromeDriver under the hood, covertly transmitting all our automation commands to it.
Hence, Appium facilitates Chrome automation by locating ChromeDriver, initiating it as a WebDriver server, and discreetly relaying all our test commands directly to ChromeDriver whenever we’re actively automating a webpage.
In this mode, Appium simply functions as a ‘proxy,’ serving as a thin intermediary between us and ChromeDriver.
When this is taking place, we refer to Appium being in a ‘proxy’ mode, indicating that it remains neutral towards our command requests or responses. It merely serves as an intermediary between our script and ChromeDriver. The commands sent by our script are transmitted unaltered to ChromeDriver, and likewise, the responses from ChromeDriver are directly returned to our script without any modifications. It’s akin to Appium momentarily stepping back and delegating all web automation responsibilities to ChromeDriver, saying, “ChromeDriver, handle everything related to web automation.”
ChromeDriver is specifically designed for automating Chrome, making it more powerful compared to creating support through the remote debugger, as we did with Safari.
Due to ChromeDriver being developed by the same teams responsible for Chrome, it possesses significantly more capabilities than the basic remote debugger support available for Safari.
ChromeDriver Download — Trials & Tribulations

However, not everything is perfect when it comes to Chrome. ChromeDriver is a double-edged sword! It’s incredible that it’s available and enables free web automation for Android. Yet, being an autonomous software with its own prerequisites, it introduces some complexity.
Each version of Appium UiAutomator driver is bundled with one version of ChromeDriver.
Each version of Appium, particularly the UiAutomator2 driver, comes with a specific version of ChromeDriver. Usually, this corresponds to the latest version available at the time of our UiAutomator2 driver release. This arrangement is advantageous because it eliminates the need for us to manually download ChromeDriver.
Each version of ChromeDriver only automates certain versions of Chrome.
In regards to disadvantages, ChromeDriver comes with some of its own requirements when it comes to automating Chrome. Each version of ChromeDriver can only automate particular versions of Chrome, typically those released concurrently with that ChromeDriver version. If we try to automate a version of Chrome that isn’t supported with the ChromeDriver that comes with Appium, we’ll encounter an error message resembling the one depicted below:

As evident, this error message is quite informative. It indicates that there was no compatible ChromeDriver available to automate the version of Chrome detected on our device.
Appium always prints in its logs the versions of ChromeDriver it knows about and what versions of Chrome they require.
The error provides a link for additional information. Moreover, if we were to scroll further up in the Appium logs at this point, we’d find even more useful details. Whenever we use Appium to automate a webpage, it will give us some information regarding available ChromeDriver versions and their corresponding minimum required Chrome versions.
To resolve errors, one tactic is to download newer versions of Chrome, which is advisable anyway.
To address any issues, we could simply update Chrome to the specified version. However, this often involves too much manual intervention. Thus, Appium actually offers a feature designed to manage this problem. Let’s explore it in detail.
ChromeDriver Autodownload Feature

Appium fetches a version of ChromeDriver tailored to match the version of Chrome detected on our device.
In essence, Appium will readily download the ChromeDriver version suitable for the Chrome version detected on the device.
Appium manages a complete collection of ChromeDrivers for our devices by downloading them onto our system.
Furthermore, when automating numerous devices with the same Appium server, Appium downloads and retains an entire set of ChromeDrivers, along with a mapping file instructing Appium on which ChromeDrivers to utilize with specific Chrome versions.
Because this feature entails downloading and running code from the internet, it technically poses a security risk. Thus, the Appium server administrator must explicitly opt into it.
Appium downloads ChromeDrivers from Google’s official repository. However, there’s always the possibility that the URL might change or be compromised by a malicious entity. Recognizing this potential security concern, the Appium team acknowledges the risk associated with allowing the download and execution of files from the internet. Hence, this feature is not enabled by default. Instead, it must be explicitly enabled by the administrator responsible for initiating the Appium server.
To activate potentially insecure functionalities, use the --allow-insecure CLI argument when starting Appium. In this case, use appium --allow-insecure=chromedriver_autodownload.
We can accomplish this by including a distinct flag when launching the Appium server, denoted as --allow-insecure. This flag allows us to designate any number of features we wish to activate that aren’t enabled by default due to security considerations. To enable this specific ChromeDriver auto-download feature, we must identify its feature name. These names are available in the Appium documentation. For our scenario, the specific feature name to use is chromedriver_autodownload. So to configure everything accordingly, simply launch the Appium server with this command line parameter.
ChromeDriver Download Capabilities

chromedriverExecutableDir — Path designating the location where Appium stores ChromeDrivers it downloads.
By default, when Appium downloads ChromeDrivers, it places them in the same directory where the UiAutomator2 driver is installed. While this setup works, updating the driver may result in the deletion of all downloaded ChromeDrivers. If we want to retain these ChromeDrivers between driver updates, we can utilize the chromedriverExecutableDir capability. Simply provide the path to a directory where Appium can store all its ChromeDrivers. It’s important to ensure that the user running the Appium server has write permissions for this directory.
chromedriverChromeMappingFile — Path to JSON file where Appium stores its mapping between ChromeDriver binaries and Chrome versions.
Similarly, there exists a capability named chromedriverChromeMappingFile, which enables us to specify the location of the JSON file generated by Appium. This file stores a mapping that associates ChromeDriver binaries with the respective Chrome versions they are compatible with.

Mobile Chrome — Practical Walkthrough
It’s time to move onto some kinetic learning. Let’s create a new Python file web_android.py and recycle the code from the iOS example. We need to adjust is the capabilities section, in order for us to target an Android device and the Chrome browser. The one thing left is to change the browserName capability, and set the value to Chrome.
CAPS = {
"platformName": "Android",
"browserName": "Chrome",
"appium:options": {
"platformVersion": "14.0", # optional
"deviceName": "Android Emulator",
"automationName": "UiAutomator2",
}
}
Now we should be ready to go in terms of the script. But remember that the ChromeDriver Autodownload feature isn’t turned on automatically. Thus, in the terminal start the server using the --allow-insecure=chromedriver_autodownload parameter.

The server is running now with this feature enabled, so we can run the script:
python3 web_android.py
After we wait a short while for the usual startup routine, Chrome pops up. We see it loading the page and walking through the flow. No errors got produced, so it’s a passing test.

We’ve addressed the necessary steps for interfacing with Chrome through Appium. The primary challenge lies in ChromeDriver compatibility with different Chrome versions. However, we can rely on Appium to handle this complexity seamlessly through its ChromeDriver Autodownload feature. In the event you’re unable to activate this feature, alternative solutions exist, such as instructing Appium to utilize a different ChromeDriver version or updating Chrome directly, as previously discussed. You can find detailed information on these options in the Appium documentation regarding ChromeDriver management.
Extra Practice Exercise
If you feel adventurous, try re-running the code with the following capabilities, which omit the browserName but include appPackage and appActivity:
CAPS = {
"platformName": "Android",
"appium:options": {
"platformVersion": "14.0", # optional
"deviceName": "Android Emulator",
"automationName": "UiAutomator2",
"appPackage": "com.android.chrome",
"appActivity": "com.google.android.apps.chrome.Main"
}
}
You’d probably want to rewrite the selectors to fit those that work in a native Android session, just as we’ve done for iOS when we used the bundle ID. Appium Inspector or UIAutomatorViewer might be helpful in inspecting those elements and extracting their respective selectors.

- GitHub Repo: https://github.com/lana-20/web-testing-appium
- YouTube Playlist: https://www.youtube.com/playlist?list=PLbWhm4uo5sutjEGjPY-XkItoMEszH3j2u
- The Internet App (AUT): https://the-internet.herokuapp.com
- Elemental Selenium Tips (test scenarios for The Internet app): https://elementalselenium.com/tips
- About WebDriver for Safari: https://developer.apple.com/documentation/webkit/about_webdriver_for_safari
- Enable WebDriver on iOS and iPadOS: https://developer.apple.com/documentation/safari-developer-tools/ios-enabling-webdriver
- Apple Web Inspector: https://developer.apple.com/documentation/safari-developer-tools/web-inspector
- Remote Automation enabled on simulators: https://developer.apple.com/forums/thread/656190
- How to enable chromedriver_autodownload feature in appium?: https://stackoverflow.com/questions/62723984/how-to-enable-chromedriver-autodownload-feature-in-appium
- Automatic Discovery of Compatible Chromedriver: https://github.com/appium/appium-uiautomator2-driver?tab=readme-ov-file#automatic-discovery-of-compatible-chromedriver,
- Update chromedriver download logic for v115+: https://github.com/appium/appium/issues/18897
- Legacy Appium ChromeDriver docs: https://appium.readthedocs.io/en/latest/en/writing-running-appium/web/chromedriver/ & https://dpgraham.github.io/appium-docs/advanced/chromedriver/

Stay tuned to learn more about mobile software testing.
Happy testing and debugging!
I welcome any comments and contributions to the subject. Connect with me on LinkedIn, X , GitHub, or IG.
If you find this post useful, please consider buying me a coffee.