<repo_name>anggastika/typescript-webdriverIO-appium-cucumber<file_sep>/src/test-layer/step_definitions/common_steps.ts
import { expect } from 'chai';
import { defineSupportCode } from 'cucumber';
import { Messages } from '../../support/enums/messages';
import { utils } from '../../support/objects/elements-utils/elements-util';
import { LU } from '../../support/objects/elements-utils/locators-util';
import { BaseView } from '../../support/objects/views/base-view';

const baseView = new BaseView();

defineSupportCode(({ Given, When, Then }) => {

    /*
    When(/I launch '(.+)'/, async (activityName) => {
        await utils.launch(activityName);
    });
    */

    When(/I set '(.+)' location on device/, async (locationName) => {
        await utils.setGeoLocation(locationName);
    });

    When(/I launch application and skip dialog panels/, async () => {
        const dialogPanel = baseView.getContainer('dialog panel');
        await dialogPanel.continueLocationAlert();
        await dialogPanel.denyPermissions();
    });

    When(/I wait the (main|favorites|settings) view is loaded/, async (pageName) => {
        await baseView.getContainer(`${pageName} view`).isViewFullyLoaded();
    });

    When(/I click on '(.+)'/, async (elementName) => {
        await baseView.currentContainer.clickOnElement(elementName);
    });

    When(/I fill '(.+)' with '(.+)'/, async (elementName, valueToFill) => {
        await baseView.currentContainer.fillElement(elementName, valueToFill);
    });

    Then(/'(.+)' elements should be '(.+)'/, async (elementsName, expectedCount) => {
        const actualCount = await baseView.currentContainer.getElementsCount(elementsName);

        expect(actualCount).to.equal(+expectedCount);
    });

    Then(/the '(.+)' should be (invisible|visible) within (main view|favorites view|setting view|dialog panel)/,
        async (elementName, expectedVisibility, containerName) => {
        const expectedState = expectedVisibility === 'visible';

        const actualState = await baseView.getContainer(containerName).isElementVisible(elementName, 6000);
        expect(actualState).to.equal(expectedState);
    });

    Then(/element(?:s|) (?:are|is) (invisible|visible) on current container:/, async (expectedVisibility, elements) => {
        const expectedState = expectedVisibility === 'visible',
              waitDuring = expectedState ? 10000 : 5000 ;

        const expectedElements = elements.hashes().map((el) => el.itemName);

        for (const elementName of expectedElements) {
            const actualState = await baseView.currentContainer.isElementVisible(elementName, waitDuring);

            expect(actualState).to.equal(expectedState,
                `${elementName} visibility - actual: ${actualState}, expected: ${expectedState}`);
        }
    });

    Then(/element with '(.+)' text should be visible/, async (text) => {
        const requiredElement = await LU.getElementByText(text);
        const actualState = await utils.isElementVisible(requiredElement);

        expect(actualState).to.equal(true);
    });

    Then(/element '(.+)' text should be '(.*)'/, async (elementName, expectedValue) => {
        const actualValue = await baseView.currentContainer.getElementText(elementName);

        if (Messages[expectedValue]) {
            expectedValue = Messages[expectedValue];
        }

        expect(actualValue).to.equal(expectedValue,
            `${elementName} text - actual: ${actualValue}, expected: ${expectedValue}`);
    });

    Then(/keyboard should be (invisible|visible)/, async (expectedVisibility) => {
        const expectedState = expectedVisibility === 'visible';
        const actualState = await browser.isKeyboardShown();

        expect(actualState).to.equal(expectedState, `keyboard - actual: ${actualState}, expected: ${expectedState}`);
    });
});
<file_sep>/config/wdio.conf.js
const configHelper = require('./config-helper.js');

/**
 * more information: https://webdriver.io/docs/configurationfile.html
 *                   https://webdriver.io/docs/options.html
 */
exports.config = {

    // WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally / remote machine).
    runner: 'local',    
    
    specs: ['./src/test-layer/features/*.feature'],

    // Patterns to exclude.    
    exclude: [
        // 'path/to/excluded/files'
    ],    

    host: '0.0.0.0',

    port: 4723,

    maxInstances: 1,

    capabilities: configHelper.getCapabilities(),

    services: [ 
        // https://webdriver.io/docs/appium-service.html
        'appium'        
    ],

    // http://appium.io/docs/en/writing-running-appium/server-args/index.html
    appium: {
        args: {
            address: '0.0.0.0',
            port: 4723,
            commandTimeout: 1800000,
            sessionOverride: true,
            debugLogSpacing: true
        },
        command: 'appium',
        logFileName: './temp/appium.log',
        waitforTimeout: 1800000,
        waitStartTime: 6000
    },

    logLevel: process.env.npm_config_wdioLogging ? process.env.npm_config_wdioLogging : 'silent',

    coloredLogs: true,

    waitforTimeout: 10000,

    // default timeout in milliseconds for request if Selenium doesn't send response
    connectionRetryTimeout: 300000,    

    connectionRetryCount: 3,

    framework: 'cucumber',

    cucumberOpts: configHelper.getCucumberOptions(),

    // more info: https://webdriver.io/docs/allure-reporter.html#configuration
    reporters: [ 
        [
            'allure',
            {
                outputDir: './temp/allure-results',
                useCucumberStepReporter: true,
                disableWebdriverStepsReporting: true,
                disableWebdriverScreenshotsReporting: false
            }
        ]
    ],

    // =====
    // Hooks
    // =====
    
    // Runs after a Cucumber step
    afterStep: function (uri, feature, { error, result, duration, passed }, stepData, context) {

        const stepResult = !!passed && !error ? 'passed' : 'failed';

        configHelper.logger.info(`AFTER STEP :: ${stepData.step.text} :: ${stepResult}`);

        browser.takeScreenshot();
    },

    // Runs after a Cucumber scenario
    afterScenario: async function (uri, feature, scenario, result, sourceLocation) {

        await browser.closeApp();
        configHelper.logger.info(`AFTER SCENARIO :: app closed`);

    },
    
    // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance
    // it and to build services around it. You can either apply a single function or an array of
    // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got
    // resolved to continue.
    /**
     * Gets executed once before all workers get launched.
     * @param {Object} config wdio configuration object
     * @param {Array.<Object>} capabilities list of capabilities details
     */
     onPrepare: function (config, capabilities) {
        configHelper.logger.info('ON_PREPARE :: ');
     },
    /**
     * Gets executed just before initialising the webdriver session and test framework. It allows you
     * to manipulate configurations depending on the capability or spec.
     * @param {Object} config wdio configuration object
     * @param {Array.<Object>} capabilities list of capabilities details
     * @param {Array.<String>} specs List of spec file paths that are to be run
     */
     beforeSession: function (config, capabilities, specs) {
        configHelper.logger.info('BEFORE_SESSION :: ');
     },

    // Gets executed before test execution begins. At this point you can access all global
    // variables, such as `browser`. It is the perfect place to define custom commands.
    before: function() {
        configHelper.logger.info('before hook');    
     
        /**
       * Setup the Chai assertion framework
       */
     // const chai    = require('chai');
    //  global.timeUnits = 
    //  global.expect = chai.expect;
     // global.assert = chai.assert;
     // global.should = chai.should();
     //configHelper.logger.info(`isDisplayed :: ${element.selector} :: result "${visibility}"`);
    },  

    /**
     * Runs before a WebdriverIO command gets executed.
     * @param {String} commandName hook command name
     * @param {Array} args arguments that command would receive
     */
    // beforeCommand: function (commandName, args) {
    // },
    /**
     * Runs before a Cucumber feature
     */
    // beforeFeature: function (uri, feature, scenarios) {
    //    
    // },
    /**
     * Runs before a Cucumber scenario
     */
    // beforeScenario: function (uri, feature, scenario, sourceLocation) {
    // },
    /**
     * Runs before a Cucumber step
     */
    // beforeStep: function (uri, feature, stepData, context) {
    // },
    /**
     * Runs after a Cucumber feature
     */
    // afterFeature: function (uri, feature, scenarios) {
    // },
    /**
     * Runs after a WebdriverIO command gets executed
     * @param {String} commandName hook command name
     * @param {Array} args arguments that command would receive
     * @param {Number} result 0 - command success, 1 - command error
     * @param {Object} error error object if any
     */
    // afterCommand: function (commandName, args, result, error) {
    // },
    /**
     * Gets executed after all tests are done. You still have access to all global variables from
     * the test.
     * @param {Number} result 0 - test pass, 1 - test fail
     * @param {Array.<Object>} capabilities list of capabilities details
     * @param {Array.<String>} specs List of spec file paths that ran
     */
    // after: function (result, capabilities, specs) {
    // },
    /**
     * Gets executed right after terminating the webdriver session.
     * @param {Object} config wdio configuration object
     * @param {Array.<Object>} capabilities list of capabilities details
     * @param {Array.<String>} specs List of spec file paths that ran
     */
    // afterSession: function (config, capabilities, specs) {
    // },
    /**
     * Gets executed after all workers got shut down and the process is about to exit. An error
     * thrown in the onComplete hook will result in the test run failing.
     * @param {Object} exitCode 0 - success, 1 - fail
     * @param {Object} config wdio configuration object
     * @param {Array.<Object>} capabilities list of capabilities details
     * @param {<Object>} results object containing test results
     */
    // onComplete: function(exitCode, config, capabilities, results) {
    // },
    /**
    * Gets executed when a refresh happens.
    * @param {String} oldSessionId session ID of the old session
    * @param {String} newSessionId session ID of the new session
    */
    //onReload: function(oldSessionId, newSessionId) {
    //}
}
<file_sep>/docker/android-10.0/Dockerfile
#=================================
# Starting image
# https://hub.docker.com/r/budtmo/docker-android-x86-10.0
#=================================
FROM budtmo/docker-android-x86-10.0:latest

MAINTAINER antiylia

USER root

#=======================
# Gopy apk file into container
#=======================
COPY yandex_weather_v6.5.12.apk app/yandex_weather_v6.5.12.apk<file_sep>/config/cucumber-options.js
/**
 * more information:
 * https://github.com/webdriverio/webdriverio/tree/master/packages/wdio-cucumber-framework#cucumberopts-options
 */
function getCucumberOptions() {

  return {
      require: [
          './compiled-output/test-layer/step_definitions/common_steps.js'
      ],
      backtrace: true,
      requireModule: [],
      dryRun: process.env.npm_config_dryRun ? !!process.env.npm_config_dryRun : false,
      failFast: false,
      format: ['pretty'],
      colors: true,
      snippets: true,
      source: true,
      profile: [],
      strict: true,
      tagsInTitle: false,
      timeout: 180000,
      ignoreUndefinedDefinitions: false,
  };
           
};

module.exports.getCucumberOptions = getCucumberOptions;<file_sep>/README.md

## Mobile automation for native application

using
```
appium
webdriverio
cucumber
chai
typescript
allure
```

### Requirements

1. NodeJS installed globally in the system.

2. JAVA installed in the system, **JAVA_HOME** environment variable.

3. Android (sdk) installed in the system, **ANDROID_HOME** environment variable.


### Setup

```
npm install
```

### Run Tests

* start appium server:

```
npm run start-appium
```

* execute the config files. This project has 2 config files:

[wdio.config.js](config/wdio.conf.js) - used for running tests, with already running Appium server
and emulator

```
npm run test --tags=
```

[wdio.docker.config.js](config/wdio.docker.conf.js) - starts docker container with Appium server and emuator

```
npm run test-docker --tags=
```

### Options:
```
--tags= @main-view or @main-view,@favorites,@launch-app (@launch-app | @favorites | @main-view | @settings)

--platform= (android | ios (not supported yet), default: android)

--fullPlatform= android-8.1-api-27 (default: android-10.0-api-29)

--device= for Dockerized emulators (default: 'Samsung Galaxy S10')

--adbDeviceName= (for real device - serial number, for emulators - 'emulator-5554', default: 'emulator-5554').
                  To know adbDeviceName run command: adb devices

--wdioLogging= (trace | debug | info | warn | error | silent, default: debug)

--dryRun= (true | false, default: false) for checking if all cucumber steps defined
```

Examples:

* **running on default/non-default manually configured emulator (Android 8.1, 10.0):** based on [platform version distribution](https://developer.android.com/about/dashboards/index.html?utm_source=suzunone)

```
profile:
       {
        platform: 'android'
        fullPlatform: 'android-10.0-api-29'
        device: 'Nexus 5 API 29 (1080*1920)' (depends on manually started emulator,
                                              can be skipped, if non-Dockerized emulators)
        adbDeviceName: 'emulator-5554' (depends on the rinning emulators count)
       }
```

commands:
```
 npm run test --tags=@main-view
 npm run test --tags=@main-view --fullPlatform=android-8.1-api-27
```

* **running on real device:**

Device should be USB connected and configured to run test:
```
1. Develover mode is switched on

on MIUI 9 and above:

2. Settings -> Additional Settings -> Developer options ->

   turn ON "USB Debugging"
   turn ON "Install via USB"
   set USB Configuration to Charging
   turn ON "install via USB
   turn OFF "MIUI optimization" and Restart

   MTP(Media Transfer Protocol) is the default mode.
```

```
profile:
       {
        platform: 'android'
        fullPlatform: 'android-6.0.1-api-23'
        device: 'Redmi Note 3 (1080*1920)' (can be skipped, if non-Dockerized emulators)
        adbDeviceName: '8a48ad27' (on phone: Settings -> General info -> Serial number
                                   or command: adb devices)
       }
```

command:
```
npm run test --tags=@main-view --fullPlatform=android-6.0.1-api-23 --adbDeviceName='8a48ad27'
```

* **running on Dockerized emulators:**
```
profile:
       {
        platform: 'android' (!currently, only for Android 10.0 platform)
        fullPlatform: 'android-10.0-api-29'
        device: 'Samsung Galaxy S10' (default: 'Samsung Galaxy S10',
                                      configured option, see List of Devices)
        adbDeviceName: 'emulator-5554' (depends on the rinning emulators count)
       }
```

command:
```
npm run test-docker --tags=@main-view --device='Samsung Galaxy S9'
```

### List of Devices

see [list of devices](https://github.com/budtmo/docker-android#list-of-devices)

### Running tests within Dockerized Appium and Android emulators

These [solution](https://github.com/budtmo/docker-android) was used as basement for slightly customized [Docker image](https://hub.docker.com/repository/docker/antiylia/appium-android-10.0)

Requirements:

1. Nested Virtualization should be enable, Linux OS (Ubuntu)
2. Docker installed
3. downloaded Docker image

Android:

| --fullPlatform command option | docker image        |
| ----------------------------- | ------------------- |
| android-10.0-api-29           | appium-android-10.0 |

after docker container started - noVNC available on 6080 port, appium - on 4723 port.

**! Having perfomance issue with Dockerized emulators - they are freezing and can be crashed during test execution.
Using Firefox browser for VNC reviewing (localhost:6080) works faster, than Chrome, but not fully solved the perfomance issue.**

### Reporting

Allure is used for tests results reporting. For allure report generating use:

```
npm run generate-report
```

For report opened in default browser:

on Windows:

```
npm run win-launch-report
```

on Linux (Ubuntu):

```
npm run ubuntu-launch-report
```<file_sep>/src/support/objects/views/favorites-view.ts
const configHelper = require('./../../../../config/config-helper.js');
import { utils } from './../elements-utils/elements-util';
import { LU } from './../elements-utils/locators-util';

export class FavoritesView {

    private viewElements = {
        // top tool bar
        'back button': {
            android: LU.getResourceId(LU.addAppPackage('id/back_button')),
            ios: ''
        },
        'search button': {
            android: LU.getResourceId(LU.addAppPackage('id/search_button')),
            ios: ''
        },
        // searching
        'search input': {
            android: LU.getResourceId(LU.addAppPackage('id/search_input_edit_text')),
            ios: ''
        },
        'search results': {
            android: LU.getResourceId(LU.addAppPackage('id/search_recycler_view')),
            ios: ''
        },
        'search hint': {
            android: LU.getResourceId(LU.addAppPackage('id/search_placeholder')),
            ios: ''
        },
        'found item': {
            android: LU.getResourceId(LU.addAppPackage('id/item_search_root')),
            ios: ''
        },
        'reset button': {
            android: LU.getResourceId(LU.addAppPackage('id/search_clear_image_view')),
            ios: ''
        },
        // favorites segment
        'favorites segment': {
            android: LU.getResourceId(LU.addAppPackage('id/favorites_content')),
            ios: ''
        },
        'location cards': {
            android: LU.getClassName('android.widget.RelativeLayout'),
            ios: ''
        },
        'add favorites description': {
            android: LU.getResourceId(LU.addAppPackage('id/favorite_description')),
            ios: ''
        }
    };

    public async isViewFullyLoaded() {
        const elementNames = [
            'back button',
            'favorites segment'
        ];

        for (const elementName of elementNames) {
            const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);
            await utils.waitReady(requiredElement);
        }
    }

    public async getElementsCount(elementsName: string) {
        const requiredElements = await $$(this.viewElements[elementsName][configHelper.platform]);

        return requiredElements.length;
    }

    public async clickOnElement(elementName: string) {
        const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);

        return utils.clickOnElement(requiredElement);
    }

    public async isElementVisible(elementName: string, waitDuring = 30000) {
        const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);

        return utils.isElementVisible(requiredElement, waitDuring);
    }

    public async getElementText(elementName: string) {
        const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);

        return utils.getElementText(requiredElement);
    }

    public async fillElement(elementName: string, valueToFill: string) {
        const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);
        await utils.fillElement(requiredElement, valueToFill);
    }
}
<file_sep>/src/support/objects/views/main-view.ts
const configHelper = require('./../../../../config/config-helper.js');
import { utils } from './../elements-utils/elements-util';
import { LU } from './../elements-utils/locators-util';

export class MainView {

    private viewElements = {
        // top tool bar
        'top toolbar segment': {
            android: LU.getResourceId(LU.addAppPackage('id/home_toolbar_toolbar_linear_layout')),
            ios: ''
        },
        'to-favorite button': {
            android: LU.getResourceId(LU.addAppPackage('id/home_toolbar_left_button')),
            ios: ''
        },
        'weather info location': {
            android: LU.getResourceId(LU.addAppPackage('id/top_text_title')),
            ios: ''
        },
        'to-setting button': {
            android: LU.getResourceId(LU.addAppPackage('id/home_toolbar_right_button')),
            ios: ''
        },
        'to-set favorite button': {
            android: LU.getResourceId(LU.addAppPackage('id/search_button')),
            ios: ''
        },
        // weather info
        'weather info segment': {
            android: LU.getResourceId(LU.addAppPackage('id/home_illustration_content')),
            ios: ''
        },
        'central weather info square': {
            android: LU.getResourceId(LU.addAppPackage('id/home_top_info_view')),
            ios: ''
        },
        'hourly forecast': {
            android: LU.getResourceId(LU.addAppPackage('id/home_hourly_forecast')),
            ios: ''
        },
        // alerts
        'alerts segment': {
            android: LU.getResourceId(LU.addAppPackage('id/home_alert_recycler_view')),
            ios: ''
        },
        'alert image': {
            android: LU.getResourceId(LU.addAppPackage('id/alert_item_images_container')),
            ios: ''
        },
        'alert text': {
            android: LU.getResourceId(LU.addAppPackage('id/alert_item_text')),
            ios: ''
        },
        'call to action': {
            android: LU.getResourceId(LU.addAppPackage('id/alert_item_call_to_action_text')),
            ios: ''
        },
        // forecast list
        'daily forecast segment': {
            android: LU.getResourceId(LU.addAppPackage('id/home_days_forecast_recycler_view')),
            ios: ''
        }
    };

    public async isElementVisible(elementName: string) {
        const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);

        return utils.isElementVisible(requiredElement);
    }

    public async isViewFullyLoaded() {
        const elementNames = [
            'top toolbar segment',
            'weather info segment',
            'alerts segment',
            'daily forecast segment'
        ];

        for (const elementName of elementNames) {
            const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);
            await utils.waitReady(requiredElement);
        }
    }

    public async clickOnElement(elementName: string) {
        const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);

        return utils.clickOnElement(requiredElement);
    }

    public async getElementText(elementName: string) {
        const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);

        return utils.getElementText(requiredElement);
    }
}
<file_sep>/src/support/enums/app-activities.ts

/**
 * using: sudo adb shell "dumpsys activity activities | grep ResumedActivity"
 */
export enum Activities {
    launch = '.ui.activity.SplashActivity',
    'main view' = '.newui.container.ContainerActivity',
    'dark pop up' = 'ru.yandex.searchlib.splash.DarkSplashActivity',
    'settings view' = '.newui.settings.SettingsActivity',
    'favorites search' = '.newui.SearchActivity'
}
<file_sep>/src/support/objects/views/base-view.ts
const configHelper = require('./../../../../config/config-helper.js');
import { DialogPanel } from '../fragments/dialog-panel';
import { FavoritesView } from './favorites-view';
import { MainView } from './main-view';
import { SettingsView } from './settings-view';

export class BaseView {
    private curContainer;

    get currentContainer() {
        return this.curContainer;
    }

    public setContainer(containerName: string) {
        const containers = {
            'dialog panel': DialogPanel,
            'favorites view': FavoritesView,
            'main view': MainView,
            'settings view': SettingsView
        };

        this.curContainer = new containers[containerName]();
        configHelper.logger.info(`setContainer :: current container set to ${containerName}`);
    }

    public getContainer(containerName: string) {
        this.setContainer(containerName);

        return this.curContainer;
    }
}
<file_sep>/src/support/objects/views/settings-view.ts
const configHelper = require('./../../../../config/config-helper.js');
import { utils } from './../elements-utils/elements-util';
import { LU } from './../elements-utils/locators-util';

export class SettingsView {

    private viewElements = {
        'to-main-view button': {
            android: LU.getClassName('android.widget.ImageButton'),
            ios: ''
        },
        'view title': {
            android: '//*[@resource-id="<EMAIL>:id/toolbar"]//*[@class="android.widget.TextView"]',
            ios: ''
        },
        'sign in button': {
            android: LU.getResourceId(LU.addAppPackage('id/settings_auth_left_text')),
            ios: ''
        },
        'sign in description': {
            android: LU.getResourceId(LU.addAppPackage('id/settings_auth_description')),
            ios: ''
        },
        'temperature row': {
            android: LU.getResourceId(LU.addAppPackage('id/settings_temp_switch_container')),
            ios: ''
        },
        'wind speed row': {
            android: LU.getResourceId(LU.addAppPackage('id/settings_wind_switch_container')),
            ios: ''
        },
        'pressure row': {
            android: LU.getResourceId(LU.addAppPackage('id/settings_pressure_switch_container')),
            ios: ''
        },
        'location row': {
            android: LU.getResourceId(LU.addAppPackage('id/settings_location_widget_expandable')),
            ios: ''
        }
    };

    public async isViewFullyLoaded() {
        const elementNames = [
            'sign in description',
            'temperature row',
            'location row'
        ];

        for (const elementName of elementNames) {
            const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);
            await utils.waitReady(requiredElement);
        }
    }

    public async isElementVisible(elementName: string) {
        const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);

        return utils.isElementVisible(requiredElement, 30000);
    }

    public async getElementText(elementName: string) {
        const requiredElement = await $(this.viewElements[elementName][configHelper.platform]);

        return utils.getElementText(requiredElement);
    }

}
<file_sep>/src/support/enums/locations.ts
/**
 * info: http://v4.webdriver.io/api/mobile/setGeoLocation.html
 *       https://github.com/appium/appium/blob/master/docs/en/commands/session/geolocation/set-geolocation.md
 */

export const Locations = {
    'City of Mountain View': {
        latitude: '37.42187',
        longitude: '-122.08401',
        altitude: '10'
    },
    'Paris': {
        latitude: '48.8',
        longitude: '2.34',
        altitude: '35'
    }
};
<file_sep>/config/docker-options.js
/**
 * more info: https://webdriver.io/docs/wdio-docker-service.html#dockeroptionshealthcheck
 */
const healthCheck = {

    url: 'http://localhost:6080',

    maxRetries: 5,

    inspectInterval: 1000,

    startDelay: 240000
};


/**
 * more information: https://github.com/budtmo/docker-android
 * docker run command:
 *      sudo docker run 
 *          --privileged                                            (only for ubuntu OS. This flag allow to use system image x86 for better performance)
 *          -d 
 *          -p 6080:6080 
 *          -p 5554:5554 
 *          -p 5555:5555 
 *          -p 4723:4723 
 *          -e DEVICE="Samsung Galaxy S10" 
 *          -e EMULATOR_ARGS="-gpu swiftshader_indirect -accel on"  (see: https://gist.github.com/JonathanLalou/180c87554d8278b0e6d7)
 *          -e APPIUM=true                                          (see: https://github.com/budtmo/docker-android/blob/master/README_APPIUM_AND_SELENIUM.md) 
 *          antiylia/appium-android-10.0:1.0.0
 */ 
function getDockerOptions(platformVersion, deviceName) {

    return {
      
        image: 'antiylia/appium-android-10.0:1.0.0',

        healthCheck: healthCheck,

        options: {
            // stop container after build run
            rm: true,
            privileged: true,
            d: true,
            e: [
                `DEVICE=${deviceName}`,
                'EMULATOR_ARGS=-gpu swiftshader_indirect -accel on',                
                'APPIUM=true'
             ],
            p: [
                '6080:6080',
                '5554:5554',
                '5555:5555',
                '4723:4723'
            ]            
        }
    };
};

module.exports.getDockerOptions = getDockerOptions;<file_sep>/src/support/objects/elements-utils/locators-util.ts
/**
 * WebriverIO info: https://webdriver.io/docs/selectors.html#mobile-selectors
 * Appium info:     http://appium.io/docs/en/commands/element/find-elements/#selector-strategies
 * UiSelector info: https://developer.android.com/reference/android/support/test/uiautomator/UiSelector
 */
class LocatorsUtils {

    public addInstallerPackage(locatorValue) {
        return `com.android.packageinstaller:${locatorValue}`;
    }

    public addAppPackage(locatorValue) {
        return `ru.yandex.weatherplugin:${locatorValue}`;
    }

    public addPermissionPackage(locatorValue) {
        return `com.android.permissioncontroller:${locatorValue}`;
    }

    public getResourceId(resourceId: string, platform: string = 'android') {
        return `${platform}=new UiSelector().resourceId("${resourceId}")`;
    }

    public getClassName(className: string, platform: string = 'android') {
        return `${platform}=new UiSelector().className("${className}")`;
    }

    public getElementByText(text: string, platform: string = 'android') {
        return $(`${platform}=new UiSelector().text("${text}")`);
    }
}

export const LU = new LocatorsUtils();
<file_sep>/src/support/enums/messages.ts
export enum Messages {
    app_needs_to_know_location = 'Yandex.Weather needs to know your location in order to show you a precise forecast',
    allow_app_to_access_location = 'Allow Weather to access this device\'s location?',
    allow_all_time = 'Allow all the time',
    allow_only_during_app_using = 'Allow only while using the app',
    deny = 'Deny',
    city_or_area = 'City or area',
    search_hint = 'Enter location name or exact address',
    add_favorites_description = 'Add a city, area or specific address to your Favorites to quickly see what the weather is like there.\n\n' +
        'To add Favorites, first search for them, view their forecast and then tap ',
    settings_title = 'Settings',
    sign_in_description = 'Signing in lets you sync your favorite locations between your devices.'
}
<file_sep>/src/support/objects/fragments/dialog-panel.ts
const configHelper = require('./../../../../config/config-helper.js');
import { utils } from './../elements-utils/elements-util';
import { LU } from './../elements-utils/locators-util';

export class DialogPanel {

    private panelElements = {
        'location alert': {
            android: LU.getResourceId(LU.addAppPackage('id/action_bar_root')),
            ios: ''
        },
        'location message': {
            android: LU.getResourceId('android:id/message'),
            ios: ''
        },
        'continue button': {
            android: LU.getResourceId('android:id/button1'),
            ios: ''
        },
        'permissions container': {
            android: {
                '8.1': LU.getResourceId(LU.addInstallerPackage('id/dialog_container')),
                '10.0': LU.getResourceId(LU.addPermissionPackage('id/content_container'))
            },
            ios: ''
        },
        'permission icon': {
            android: LU.getResourceId(LU.addPermissionPackage('id/permission_icon')),
            ios: ''
        },
        'permission message': {
            android: LU.getResourceId(LU.addPermissionPackage('id/permission_message')),
            ios: ''
        },
        'allow always button': {
            android: LU.getResourceId(LU.addPermissionPackage('id/permission_allow_always_button')),
            ios: ''
        },
        'allow foreground button': {
            android: LU.getResourceId(LU.addPermissionPackage('id/permission_allow_foreground_only_button')),
            ios: ''
        },
        'deny button': {
            android: {
                '8.1': LU.getResourceId(LU.addInstallerPackage('id/permission_deny_button')),
                '10.0': LU.getResourceId(LU.addPermissionPackage('id/permission_deny_button'))
            },
            ios: ''
        }
    };

    public async isElementVisible(elementName: string, hasVersion: boolean = false) {
        const requiredElement = await $(this.getLocator(elementName, hasVersion));

        return utils.isElementVisible(requiredElement);
    }

    public async getElementText(elementName: string) {
        const requiredElement = await $(this.getLocator(elementName, false));

        return utils.getElementText(requiredElement);
    }

    public async clickOnElement(elementName: string, hasVersion: boolean = false) {
        const requiredElement = await $(this.getLocator(elementName, hasVersion));

        return utils.clickOnElement(requiredElement);
    }

    public async continueLocationAlert() {
        const isVisible = await this.isElementVisible('location alert');

        if (isVisible) {
            await this.clickOnElement('continue button');
        }
    }

    public async denyPermissions() {
        const isVisible = await this.isElementVisible('permissions container', true);

        if (isVisible) {
            await this.clickOnElement('deny button', true);
        }
    }

    private getLocator(elementName: string, hasVersion) {

        const [, version] = configHelper.fullPlatform.split('-');
        const versionForLocator = version === '10.0' ? '10.0' : '8.1';

        configHelper.logger.info(`platform version :: ${version} :: ${versionForLocator} used for locator`);

        const locator = this.panelElements[elementName][configHelper.platform];

        return hasVersion
            ? locator[versionForLocator]
            : locator;
    }
}
<file_sep>/src/support/objects/elements-utils/elements-util.ts
const configHelper = require('./../../../../config/config-helper.js');
import { Activities } from './../../enums/app-activities';
import { Locations } from './../../enums/locations';

class ElementsUtils {

    public async waitReady(element, timeOut = 3000) {
        try {
            await element.waitForDisplayed(timeOut);
        } catch (error) {
            configHelper.logger.error(`element ${element.selector} wait for displayed: ${error.message}`);
        }
    }

    public async isElementVisible(element, timeOut = 3000) {
        await this.waitReady(element, timeOut);
        const visibility = await element.isDisplayed();

        configHelper.logger.info(`isDisplayed :: ${element.selector} :: result "${visibility}"`);

        return visibility;
    }

    public async getElementText(element) {
        let text;
        try {
            text = await element.getText();
            configHelper.logger.info(`getText :: ${element.selector} :: text "${text}"`);
        } catch (error) {
            configHelper.logger.error(`getText :: ${element.selector} :: error ${error.message}`);
        }

        return text;
    }

    public async clearElementValue(element) {
        await element.clearValue();

        const result = await element.getValue();
        if (result !== '') {
            configHelper.logger.error(`clearValue :: ${element.selector} :: cannot clean value`);
        }
    }

    public async fillElement(element, valueToFill) {
       await element.setValue(valueToFill);
    }

    public async clickOnElement(element) {
        try {
            await element.click();
            configHelper.logger.info(`click on :: ${element.selector}`);
        } catch (error) {
            configHelper.logger.error(`click on :: ${element.selector} :: error ${error.message}`);
        }
    }

    /* do not work: wanted to launch some app view - launching activity, but no throuth UI
    public async launch(activityName) {
        const appPackage = 'ru.yandex.weatherplugin';
        await browser.startActivity(appPackage, Activities[activityName]);

        configHelper.logger.info(`launch activity :: ${Activities[activityName]}`);
    }
    */

    public async setGeoLocation(locationName: string) {

        // setGeoLocation do not change location - TODO
        await browser.setGeoLocation(Locations[locationName]);
        const currentLocation = await browser.getGeoLocation();

        configHelper.logger.info(`current geoLocation "${JSON.stringify(currentLocation)}"`);
    }

}

export const utils = new ElementsUtils();
