📺 "One project. One language. Three apps." Learn more in our upcoming code sharing webinar!

NativeScript Core

Plugin UI Tests

User interface testing exercises your app's UI likewise your users do without any knowledge about the code base behind. It helps you see the app the same way your users will, showing any UI issues that users run into. UI testing verifies that the whole application is functioning correctly, including its UI.

Prerequisites

The main characteristics that distinguish UI tests we will talk about in this article are two. The first is that the tests are Appium based and the second is that we will use TypeScript to write them. Considering these two distinguishing marks we have to install:

More about nativescript-dev-appium plugin you can find in its repository documentation, but in short it depends on Appium to communicate with device/emulator, uses Appium JavaScript client library and Mocha testing framework. Before we continue, take a moment and familiarize yourself with fore-mentioned tools unknown to you.

Test Implementation

By installing nativescript-dev-appium plugin in your demo app it creates e2e folder where our starting point is.

my-plugin
├── demo
|   └── app
|   └── e2e
└── src

There you will find a sample testing file using Mocha "BDD" interface which is nice to begin with, but let's see some real example that we will be able to run later on. We will use NativeScript Facebook plugin's UI tests for that purpose. The location of the tests stays the same so let's take a look at them. Let's review most notable lines of code and explain them.

import { AppiumDriver, createDriver, SearchOptions } from "nativescript-dev-appium";

We start by loading our plugin's modules that will be further used to initialize our driver and provide us some helpful functions.

describe("Facebook tests", async function () { // define test suite
    ...

    before(async () => {
        driver = await createDriver();
        driver.defaultWaitTime = 15000; //custom timeout when search an element
    });

    after(async () => {
        if (isSauceRun) {
            driver.sessionId().then(function (sessionId) {
                console.log("Report: https://saucelabs.com/beta/tests/" + sessionId);
            });
        }
        await driver.quit();
        console.log("Driver successfully quit");
    });
    ...

Here, we define our suite and set a custom timeout for each element to be found. The timeout setting for the whole execution is located in the mocha.opts configuration file so if needed it can be adjusted there. We use some bigger value as we run the tests in Sauce Labs and it takes a bit more time than a local execution.

Sauce Labs is a cloud-based platform for automated testing of web and mobile applications. It provides us an access to mobile emulators and simulators needed for our test execution. This way we don't have to take care of a testing environment which is great. Additionally, our testing results are public and that increases the transparency of plugin's state and how it has been tested.

Going further, two types of Mocha hooks are noticeable in the suite. The before one initialize our driver which communicates with the server and after quits it.

It is time for our tests implementation. Let's review the first test.

it("should log in via original button", async function () {
        if (isAndroid) {
            var userNameLabelElement = "[@text='Nativescript User']";
        } else {
            var loginButtonElement = "[@name='Log In']";
            var continueButtonAttribute = "[@name='Continue']";
            var userNameLabelElement = "[@name='Nativescript User']";
        }

        const facebookButton = await driver.findElementByAccessibilityId(FACEBOOK_BUTTON);
        await facebookButton.click();

        if (isAndroid) {
            const allFields = await driver.driver.waitForElementsByClassName(driver.locators.getElementByName("textfield"), 10000);
            await allFields[1].click().sendKeys(PASSWORD);
            await allFields[0].click().sendKeys(USERNAME);
        } else {
            const passField = await driver.driver.waitForElementByClassName(driver.locators.getElementByName("securetextfield"), 10000);
            await passField.click().sendKeys(PASSWORD);
            const usernameField = await driver.driver.waitForElementByClassName(driver.locators.getElementByName("textfield"), 10000);
            await usernameField.click().sendKeys(USERNAME);
        }
        await driver.driver.hideDeviceKeyboard("Done");
        if (isAndroid) {
            const logInButton = await driver.findElementByClassName(driver.locators.button);
            await logInButton.click();
            const okButton = await driver.findElementByClassName(driver.locators.button);
            await okButton.click();
        } else {
            const logInButton = await driver.findElementByXPath("//" + driver.locators.button + loginButtonElement);
            await logInButton.click();
            const continueButton = await driver.findElementByXPath("//" + driver.locators.button + continueButtonAttribute);
            await continueButton.click();
        }
        const userNameLabel = await driver.findElementByXPath("//" + driver.locators.getElementByName("label") + userNameLabelElement);
        const userName = await userNameLabel.text();
        expect(userName).to.equal(USER_NAME, "Not logged with the same user");

    });

To be able to execute our tests both on Android and iOS platforms we have to use different xpath selectors. Here comes in handy driver.locators.getElementByName("textfield") function from the plugin. It returns the native class of the element depending on the platform and platform's version by accepting as parameter the name of the element of type string. The list of all elements can be find in locators.ts file of the plugin. The last part needed to assemble our xpath selector is some distinguishing property so we are sure using the right UI element. This can be obtained by using Appium desktop app to inspect the visual tree of our app and pick a proper one.

Once we have our UI elements selectors ready it is time for the driver to find them in the visual tree so we can further manipulate and assert them. It is worth mentioning that we should use accessibility ID as a preferable selector where possible driver.findElementByAccessibilityId(FACEBOOK_BUTTON), but in most cases this is not an option and we use text driver.findElementByText(pickSingleButtonText, SearchOptions.contains);, xpath driver.findElementByXPath("//" + driver.locators.button + loginButtonElement) or class name driver.findElementByClassName(driver.locators.button).

In some scenarios we might need to use any of the wd functions, for example hideDeviceKeyboard(). Then the driver property come to help which gives us that ability await driver.driver.hideDeviceKeyboard("Done");.

Test Execution

It is time for the fun part - test execution. Before we get to the command that will run our tests we will have to add the desired configuration to our capabilities appium.capabilities.json. By installing the plugin a default capabilities file will be provided which can be further enriched and repositioned but more about this in nativescript-dev-appium's README. In our NativeScript Facebook plugin we will use two of the defined capabilities - for Android 6.0 and iOS 10.0 emulator/simulator. These capabilities are a set of keys and values sent to the Appium server to tell the server what kind of automation session we are interested in starting up. For example, if we set platformName to Android Appium will initiate Android session. The full list can be find Appium's documentation.

In order to execute the tests for those environments we will use the command for local execution. Before that we have to navigate to demo folder.

NOTE: Before running the tests we have to build our app for each platform tns build android and tns build ios. Additionally, we have to be sure that the same emulator and simulator described in the capabilities we use are available and running on our system.

npm run e2e -- --runType android23
npm run e2e -- --runType sim103iPhone6

As you can see, we execute a npm script npm run e2e that comes out-of-the-box when we install nativescript-dev-appium plugin. The rest of the command is just options configuration.

Continuous Integration

NativeScript Facebook plugin is based on nativescript-plugin-seed. Therefore, it already has .travis.yml file which eases our CI in Travis CI. We will only have to add a new stage for our UI tests and tweak it a little. In this section we will discuss only the changes that remain to be done, but you can find more information about the rest of the .travis.yml file in Ensure Plugins Quality article.

We use Sauce Labs cloud based platform to run our UI tests at. It is free for open source projects.

There are two basic changes we have to do before the integration becomes a reality. The first is to upload our application package to Sauce Labs storage as our tests require it. This is done using a curl request in the Build stage respectively for iOS and Android.

NOTE: Requests depend on SAUCE_USER and SAUCE_KEY environment variables that have to be added in Travis CI in advance. You can obtain them as described in Sauce Labs documentation.

- stage: "Build"
...
script:
        ...
    - "curl -u $SAUCE_USER:$SAUCE_KEY -X POST -H 'Content-Type: application/octet-stream' $ANDROID_SAUCE_STORAGE --data-binary @$ANDROID_PACKAGE_FOLDER/$ANDROID_PACKAGE"
- os: osx
     ...
      script: 
        ...
        - cd $IOS_PACKAGE_FOLDER && zip -r $IOS_PACKAGE demo.app
        - "curl -u $SAUCE_USER:$SAUCE_KEY -X POST -H 'Content-Type: application/octet-stream' $IOS_SAUCE_STORAGE --data-binary @$IOS_PACKAGE_FOLDER/$IOS_PACKAGE"

For iOS, we have to zip any *.app package before uploading it to Sauce Labs storage cd $IOS_PACKAGE_FOLDER && zip -r $IOS_PACKAGE demo.app.

The second change is adding our UI tests stage.

- stage: "UI Tests"
      env: 
      - Android="23"
      language: node_js
      os: linux
      node_js: "8"
      script:
        - npm i -g appium
        - cd demo && npm i
        - travis_retry npm run e2e -- --runType android23 --sauceLab --reuseDevice --appPath $ANDROID_PACKAGE
    - os: linux
      env: 
        - iOS="10"
      language: node_js 
      node_js: "8"
      script: 
        - npm i -g appium
        - cd demo && npm i
        - travis_wait travis_retry npm run e2e -- --runType sim103iPhone6 --sauceLab --reuseDevice --appPath $IOS_PACKAGE

It takes care to setup two Linux machines and executes the tests in Sauce Labs using the proper command for each platform:

npm run e2e -- --runType android23 --sauceLab --reuseDevice --appPath $ANDROID_PACKAGE

and

npm run e2e -- --runType sim103iPhone6 --sauceLab --reuseDevice --appPath $IOS_PACKAGE

See Also