How to Automate a Real E2E User Flow with Appium for iOS Devices

Posted Nov 21, 2019

Multiple screens of code

This post is specific to iOS apps. For Android apps, see my other post on how to automate a real E2E user flow on Android

A question I get a lot nowadays is “Why should I use Appium instead of native frameworks like Espresso and  XCUITest?”

Normally I would give you my opinion right away, but throughout the years I’ve learned that it’s better to come with some facts. There are a lot of facts that I can use to explain why you should choose one or the other, but that’s a subject for a different blog.

Today I want to focus on one specific thing that you cannot do with native frameworks, and that is automating a real end-to-end (E2E) user flow.To prevent this post from becoming a novel, I will only focus on iOS in my examples. Android will be handled in a separate post.

What is a real E2E user flow?

You might be wondering what I mean by a real E2E user flow. Well, the answer is pretty simple. Let me show you an example.  It is based on another question I get a lot from our customers around automating the user flow between app and browser, also known as a multi-app-flow

In this example, a user needs to do something in the (native) app which triggers opening a browser where the flow proceeds. This can be whatever flow where you just need to switch from your app to the browser.

To mimic this flow we added a link in our Swag Labs demo app that opens a link in the browser. If you install the app and log in you can open the menu and see the following options.

the menu

Image 1: Menu

When the About item is pressed, the browser will be opened, which will lead you to our Sauce Labs website. Here you can explore our website, click on buttons, get text and so on. 

SwagLabs gif

Automating a real E2E user flow

Before we dive into code, I want to mention that Appium can be used with different frameworks and different coding languages. The following coding examples will use WebdriverIO as a framework and JavaScript as a coding language. I tried to keep all steps as generic possible so you might be able to translate them back to your preferred framework/coding language.

I always start with writing down the functional flow in comments to get a better understanding, see below.

describe('Appium', () => {
   it('should be able to work with the browser that is opened by the app', () => {
       // 1. Login to the app
       // 2. Verify that you are logged in
       // 3. Open the menu and click on the about screen
       // 4. Figure out how we can verify that the browser is opened
       // 5. Verify that the page is loaded
   });
});

The first three steps can easily be implemented. The difficult part is step 4, where we need to switch from the app to the browser and determine if this succeeded. If we look at this in a technical way, we could say that we change from a native context to a web context. When I was mentioning this out loud I thought that this is almost the same behaviour as you have with Hybrid apps. A Hybrid app also has a native and a web context, the last is called a Webview

So I tried to figure out if switching from an app to the browser did the same for the contexts as it would normally do for Hybrid apps by implementing this piece of code at step 4.

/**

* `waitUntil` expects a condition and waits until that condition is

* fulfilled with a truthy value. So just log the contexts for 15 seconds and see what happens

*/

driver.waitUntil(() => {
   // Get all the contexts, that could look like this
   const contexts = driver.getContexts();
   // Log them to the console

   console.log('contexts = ', contexts);

   // Just return false so this function will never return a truthy value
   return false;
}, 15000);

It resulted in the following log:

contexts = [ 'NATIVE_APP']
contexts = [ 'NATIVE_APP', 'WEBVIEW_76851.4', 'WEBVIEW_76851.5']
contexts = [ 'NATIVE_APP', 'WEBVIEW_76851.1', 'WEBVIEW_76851.4', 'WEBVIEW_76851.5']

This was good and bad. The good thing was that I could see that between when the app was closed and when the browser was opened, new contexts were added. But now I had a challenge, the same challenge you might have with Hybrid apps, and that is multiple Webviews. Because how should I determine which Webview holds which url?

Two challenges

As said, we have a challenge with the Webviews, but luckily we are using Appium together with iOS and there is a specific capability that might help us with this. In March I read an article on Appium Pro about the fullContextList-capability. What it basically does is that if you provide fullContextList: true, in your capabilities for iOS, and your ask for the current contexts, it will not return an array of context-strings, but an array of Webview-objects containing the id, the title and the url of the Webview that are loaded. The advantage of this is that you don’t need to retrieve the title and url with extra Webdriver-calls, but you can retrieve it with one Webdriver call. This makes it much faster and less flaky.

When I started my iOS device with this new capability and checked the contexts again I found this in my logs.

// The first try
contexts = [ 
  { id: 'NATIVE_APP' },
  { id: 'WEBVIEW_76851.1',title: '', url: 'about:blank' },
  { id: 'WEBVIEW_76851.4', title: '', url: 'about:blank' },
  { id: 'WEBVIEW_76851.5', title: '', url: 'about:blank' }
]
// The second try
contexts = [ 
  { id: 'NATIVE_APP' },
  { id: 'WEBVIEW_76851.1',
    title: 'Cross Browser Testing, Selenium Testing, Mobile Testing | Sauce Labs',
    url: 'https://saucelabs.com/' },
  { id: 'WEBVIEW_76851.4', title: '', url: 'about:blank' },
  { id: 'WEBVIEW_76851.5', title: '', url: 'about:blank' }
]

The 'about:blank's are from multiple empty open tabs in Safari. 

As you can see, this makes it much easier to select the correct Webview. You can select the right Webview with some JavaScript Array magic, like below:

           // Store the correct context into this variable
           const correctWebview = driver.getContexts()
               // Then filter out the 'NATIVE_APP', because it doesn’t have a `url`
               .filter(context => {
                   return !context.id.includes('NATIVE_APP')
               })
               // Now find the webview that contains the correct URL
               .find(context => {

                   return context.url.includes('https://saucelabs.com/')

               });

The above code will provide the matching Webview selected out of many. We can then use the id to switch to the right Webview context with the following code in JavaScript:

driver.switchContext(correctWebview.id); 

and interact with the browser like we normally do. This means we can use the same locators and selectors like we would use on a desktop web page, but…… I made a small thinking error which led to my second challenge. 

A Webview uses Safari, but Safari is not a Webview, meaning my test failed because it could not interact with the webpage in Safari. After some digging I found out that this was something that was missing in Appium, so I filed bug, see here. The Appium team added a new capability in Appium 1.15.0 called includeSafariInWebviews. It does what it describes, it will add Safari as a Webview which should fix the issue I was facing and eventually it did solve my second challenge.

So by adding two extra capabilities and updating my Appium version to the latest 1.15.0 version (which also supports iOS 13) I am now able to automate a real E2E user flow with Appium

The complete code

In the end I was able to automate the real E2E user flow with Appium with the following code for iOS.

// ============
// Capabilities
// ============
config.capabilities = [
   {
       deviceName: 'iPhone X',
       platformName: 'iOS',
       platformVersion: '12.2',
       orientation: 'PORTRAIT',

       app: join(process.cwd(), './apps/iOS.Simulator.SauceLabs.Mobile.Sample.app.2.1.0.app.zip'),

       maxInstances: 1,
       // The 2 extra added capabilities
       // Return an array of Webview-objects containing the id, the title and the url
       fullContextList: true,
       // Add Safari as a Webview
       includeSafariInWebviews: true,
   },
];

import LoginScreen from '../screenObjects/login'
import InventoryListScreen from '../screenObjects/inventoryList'
import Menu from '../screenObjects/menu';
const DEFAULT_TIMEOUT = 15000;
describe('Appium', () => {
   it('should be able to work with the browser that is opened by the app', () => {
       // Login to the app and verify that it succeeded
       LoginScreen.waitForIsShown();
       LoginScreen.signIn({username: 'standard_user', password: 'secret_sauce'});
       InventoryListScreen.waitForIsShown();

       expect(InventoryListScreen.isShown()).toEqual(true);

       // Open the menu and click on the about screen
       Menu.open();
       Menu.openAbout();
       // Now do all the context magic
       let correctWebview = null;

       /**

        * Wait until the right context with the right url has loaded

        * This `waitUntil` will wait until the condition of the provided

        * function will return true, in our case it will return true if

        * the `correctWebview` contains a value

        */

       driver.waitUntil(() => {

           // Get all the contexts, that could look like this
           // [
           //     {
           //         id: 'NATIVE_APP',
           //     },
           //     {
           //         id: 'WEBVIEW_74323.1',
           //         title: 'Smashing Magazine — For Web Designers And Developers — Smashing Magazine',
           //         url: 'https://www.smashingmagazine.com/',
           //     },
           //     {
           //         id: 'WEBVIEW_74323.2',
           //         title: 'Cross Browser Testing, Selenium Testing, Mobile Testing | Sauce Labs',
           //         url: 'https://saucelabs.com/',
           //     },
           //  ]
           const contexts = driver.getContexts();

           // Store the correct context into this variable
           // if there is no match don't do anything
           correctWebview = contexts
           // First filter out the 'NATIVE_APP'
               .filter(context => {
                   return !context.id.includes('NATIVE_APP')
               })
               // Now filter out the webview that contains the correct URL
               .find(context => {
                   return context.url.includes('https://saucelabs.com/')
               });
           // If the `correctWebview` contains a value this function will evaluate to `true` meaning
           // the `waitUntil` can stop
           return correctWebview || false;
       }, DEFAULT_TIMEOUT);

       // Now switch to the right context
       driver.switchContext(correctWebview.id);

       expect(driver.getTitle()).toEqual('Cross Browser Testing, Selenium Testing, Mobile Testing | Sauce Labs');
   });
});

Closing remark

With this example, I shared how to automate an end-to-end user flow using Appium, which is not something you can do with native frameworks.  This isn’t to say that you shouldn’t use native frameworks like Espresso and XCUITest, because each tool has its own pros/cons and added business value. You could even use native frameworks and Appium together, each covering a different purpose. I just wanted to show you the power of using Appium. And the beauty of Appium is that there might be more ways to automate E2E user flows, so if you know a different way, please let us know and share your experiences. 

I hope you found this tech tip useful. If you’re looking for a quick and easy way to test real E2E user flows with your own app on iOS, you can try for free with Sauce Labs.  Until next time….happy testing!

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

Topics

AppiumApp testing

Categories