How to Automate Deep Link Testing on Emulators, Simulators and Real Devices

photo-blog-mobile-phone and laptop

This blog post was originally authored by Wim Selles on Appium Pro Newsletter Edition 84 and has since been edited to be used with the Sauce Labs Demo App.

Recently I wanted to explore the possibilities of using deep links with Appium on real devices and I stumbled upon Edition 7 of Appium Pro, called Speeding Up Your Tests With Deep Links. In that edition, we learned how to use shortcuts to make execution speed less important by using deep links.

In order to demonstrate and use deep links together with Appium the following command was used:

driver.get('<app-identifier>://<deep-link>')

During my research I found out that this works perfectly for Android emulators and iOS simulators, but it didn't work on real devices. On iOS, Siri was opened, which is the native behavior for iOS (see this issue for more details). In this edition, I want to show you a different method of triggering deep links that will work for emulators, simulators and real devices, which you can use with our Sauce Labs Demo App (more information about how deep linking has been configured in our app can be found here). Let's start with the easy part, which will be Android.

Note: The code examples are in JavaScript and can be used with WebdriverIO V5. I think the steps and code are self-explanatory, which makes it easy to translate it to your favorite language / framework.

Android and deep linking

Android has a specific mobile command to use deep linking, which can be found here. We can use the deep link command in the following way:

driver.execute( 'mobile:deepLink', { url: "<deep-link-url>", package: "<package-name>" } );

For The Sauce Labs Demo App we need the following deep link format:

swaglabs:///<screen-name>/<ids>

Based on this format it will wake up the app, skip authentication behind the scenes and jump the user to the swag overview screen instantly. When the url, the Android package name of The Sauce Labs Demo App, and the command are combined, we get this for Android:

driver.execute( 'mobile:deepLink', { url: "swaglabs://swag-overview/0,2", package: "com.swaglabsmobileapp" } );

This command will work for Android emulators and real devices, even when the app is already opened. Pretty easy, isn't it?

iOS deep linking

Now let's take a look at the hardest part, which is making this work on iOS. There is no mobile command for iOS so we need to take a look at a basic flow on how to use a deep link with iOS. A deep link can be opened through the terminal with the following command:

xcrun simctl openurl booted swaglabs://login/<screen-name>/<ids>

But this will not be a cross-device solution, especially when you are using a local grid or a cloud solution, and have no access to simctl.

Deep links can also be opened from Safari, meaning that if the deep link is entered in the Safari address bar it will trigger a confirmation pop-up with the question of whether the user does indeed want to open the 3rd-party app (see below).

Confirmation pop-up

When the pop-up has been confirmed, the app will be opened and the deep link will bring us to the screen we wanted to open. This is a fairly cross-device solution and manual tests have proven that this works for simulators and real devices.

To be able to automate this flow with Appium, we need to follow some simple steps:

  1. Terminate the app under test (optional)

  2. Launch Safari and enter the deep link in the address bar

  3. Confirm the notification pop-up

Each step will be explained in detail below.

Step 1: Terminate the app

This is an optional step and only needed if you experience flakiness when not terminating the app or if terminating the app is part of your flow.

In Edition 6, Jonathan explained how to test iOS upgrades and he mentioned that as of Appium 1.8.0 we have the mobile: terminateApp. The command needs the bundleId of the iOS app as an argument and when that is put together we get this command:

driver.execute( 'mobile: terminateApp, { bundleId: "org.reactjs.native.example.SwagLabsMobileAppTests" } );

Step 2: Launch Safari and enter the deep link in the address bar

As explained earlier, we now need to open Safari and set the deep link address. It's best to cut this step into 2 parts: opening Safari and then entering the deep link.

Opening an iOS app can be done with mobile: launchApp. The command needs the bundleId of the iOS app, in this case the bundleId of Safari, as an argument and when that is put together we get this command.

(An overview of all bundleIds of the Apple apps can be found here)

driver.execute( 'mobile: launchApp', { bundleId: 'com.apple.mobilesafari' } );

When the launchApp command has successfully been executed Safari will be opened like this:

When the launchApp command has successfully been executed Safari will be opened

Now comes the tricky part. This part cost me some headaches because it was the most flaky part of the deep link process, but I finally got it stable, so let's check it out. First of all, we need to think about the steps a normal user would take to enter a url in Safari, which would be:

  1. Click on the address bar

  2. Enter the url

  3. Submit the url

Secondly, keep in mind that we started Appium with The App in the capabilities, meaning we are in the native context. For the next steps we need to stay in the native context, so we can use Appium Desktop to explain the steps we need to take.

When we start Appium Desktop and open Safari we need to know the selector of the address bar. Since we know that XPATH might be the slowest locator (see https://appiumpro.com/editions/8How to Speed Up Native Appium iOS Test Execution by Knowing How Appium Works), we want to use a faster locator. Because the address bar has a name attribute with the value URL, you might think we can use the Accessibility ID locator (which we can), but that one will give us back two elements: both the XCUIElementTypeButton and the XCUIElementTypeOther (which will take longer to work with).

Use the iOS predicate string locator to specifically select the url-button element.

In this case I would advise that we use the iOS predicate string locator to specifically select the url-button element. I would also advise that we use a wait strategy to be sure that the element is visible and ready for interaction. The code would look something like this:

const urlButtonSelector = 'type == \'XCUIElementTypeButton\' && name CONTAINS \'URL\''; const urlButton = $(`-ios predicate string:${ urlButtonSelector }`);

// Wait for the url button to appear and click on it so the text field will appear urlButton.waitForDisplayed(DEFAULT_TIMEOUT); urlButton.click();

If we would now refresh the screen in Appium Desktop we would see that the XCUIElementTypeButton  element is not there anymore, but changed to a XCUIElementTypeTextField element with the same URL name attribute. If we would have used the Accessibility ID locator, then this would have been the second point that could cause flakiness. The reason for this is that the switching of elements might not be picked up at the same speed for Appium, making it refer to the already-disappeared XCUIElementTypeButton element, and thus causing the script to fail to interact.

To set the value we're going to use the iOS predicate string locator again.

To set the value we're going to use the iOS predicate string locator again. After setting the url we also need to submit the url. This can be done by clicking on the Go button on the keyboard, but this can also become flaky if the keyboard doesn't appear. Appium allows you to also use Unicode characters like Enter (defined as Unicode code point \uE007) when using setValue. This means we can set and submit the url with one command:

const urlFieldSelector = 'type == \'XCUIElementTypeTextField\' && name CONTAINS \'URL\''; const urlField = $(`-ios predicate string:${ urlFieldSelector }`);

// Submit the url and add a break urlField.setValue('theapp://login/darlene/testing123\uE007');

When the url has been submitted a notification pop-up appears. This brings us to our last step.

Note: Keep in mind that this script has been made on an English iOS simulator; if you have a different language, the selector text (URL) might be different.

Step 3: Confirm the notification pop-up

After submitting the url we only need to wait for the notification pop-up to appear and click on the Open button. To keep the locator strategy aligned we are also going to use the iOS predicate string locator here.

Confirm the notification pop-up

With the wait command the code would look like this:

// Wait for the notification and accept it const openSelector = 'type == \'XCUIElementTypeButton\' && name CONTAINS \Open\''; const openButton = $(`-ios predicate string:${ openSelector }`);   openButton.waitForDisplayed(DEFAULT_TIMEOUT); openButton.click();

Note: Keep in mind that this script has been made on an English iOS simulator; if you have a different language, the selector text (Open) might be different.

Making it cross platform

We've created a deep link script for Android (a one-liner) and for iOS (more complex), so now let's make it a cross platform helper method that can be used for both platforms. If we stitch all code together we can create the following helper:

/** * Create a cross platform solution for opening a deep link * * @param {string} url */ export function openDeepLinkUrl(url) { const prefix = 'swaglabs://';   if (driver.isIOS) {  // Launch Safari to open the deep link  driver.execute('mobile: launchApp', { bundleId: 'com.apple.mobilesafari' });    // Add the deep link url in Safari in the `URL`-field  // This can be 2 different elements, or the button, or the text field  // Use the predicate string because  the accessibility label will return 2 different types  // of elements making it flaky to use. With predicate string we can be more precise  const urlButtonSelector = 'type == \'XCUIElementTypeButton\' && name CONTAINS \'URL\'';  const urlFieldSelector = 'type == \'XCUIElementTypeTextField\' && name CONTAINS \'URL\'';  const urlButton = $(`-ios predicate string:${ urlButtonSelector }`);  const urlField = $(`-ios predicate string:${ urlFieldSelector }`);    // Wait for the url button to appear and click on it so the text field will appear  // iOS 13 now has the keyboard open by default because the URL field has focus when opening the Safari browser  if(!driver.isKeyboardShown()) {   urlButton.waitForDisplayed(DEFAULT_TIMEOUT);   urlButton.click();  }    // Submit the url and add a break  urlField.setValue(`${ prefix }${ url }\uE007`);    // Wait for the notification and accept it  const openSelector = 'type == \'XCUIElementTypeButton\' && name CONTAINS \'Open\'';  const openButton = $(`-ios predicate string:${ openSelector }`);  openButton.waitForDisplayed(DEFAULT_TIMEOUT);    return openButton.click(); }   // Life is so much easier return driver.execute('mobile:deepLink', { url: `${ prefix }${ url }`, package: 'com.swaglabsmobileapp', }); }

Note: Keep in mind that this script has been made on an English iOS simulator; if you have a different language the selector text (URL/Open) might be different.

The helper (which you can also see on GitHub) can then be used like this in some test code:

describe('Deep linking', () => { it('should be able to open the Swag overview screenlogin with a deep link', () => { //... do something before

openDeepLinkUrl('swag-overview/0,1,2login/alice/mypassword); //... do something after }); });

So now you never need to worry on how to use deep linking for Android and iOS on emulators, simulators and real devices; they all work!

To try this out using Sauce Labs platform, you can sign up for a free trial account and let me know how it goes.  Happy testing!

Sauce Labs Senior Solutions Architect Wim Selles helps solve automation challenges by day—and practices his passion for front-end test automation at night. Wim enjoys creating his own node.js modules and contributing to open source projects. You can find Wim on LinkedIn and Twittter @wswebcreation.

Written by

Wim Selles