Skip to content

Commit

Permalink
Improve Appium support in Nightwatch (#3519)
Browse files Browse the repository at this point in the history
  • Loading branch information
garg3133 committed Jan 6, 2023
1 parent 098306c commit f040731
Show file tree
Hide file tree
Showing 25 changed files with 1,259 additions and 235 deletions.
6 changes: 2 additions & 4 deletions lib/runner/cli/cli.js
Expand Up @@ -3,6 +3,7 @@ const lodashClone = require('lodash.clone');
const ArgvSetup = require('./argv-setup.js');
const Settings = require('../../settings/settings.js');
const Globals = require('../../testsuite/globals.js');
const Factory = require('../../transport/factory.js');
const Concurrency = require('../concurrency');
const Utils = require('../../utils');
const Runner = require('../runner.js');
Expand Down Expand Up @@ -472,10 +473,7 @@ class CliRunner {
let promise = Promise.resolve();

if (this.test_settings.selenium && this.test_settings.selenium.start_process) {
const SeleniumServer = require('../../transport/selenium-webdriver/selenium.js');
this.seleniumService = SeleniumServer.startServer(this.test_settings);
promise = this.seleniumService.init();
this.test_settings.selenium['[_started]'] = true;
promise = Factory.createSeleniumService(this);
}

const {real_mobile, avd} = this.test_settings.desiredCapabilities;
Expand Down
2 changes: 1 addition & 1 deletion lib/settings/defaults.js
Expand Up @@ -166,7 +166,7 @@ module.exports = {
start_process: false,
cli_args: {},
server_path: null,
log_path: '',
log_path: './logs',
port: undefined,
check_process_delay: 500,
max_status_poll_tries: 15,
Expand Down
50 changes: 41 additions & 9 deletions lib/transport/factory.js
Expand Up @@ -12,6 +12,19 @@ const BrowsersLowerCase = {
};

module.exports = class TransportFactory {
static createSeleniumService(cliRunner) {
const usingAppium = cliRunner.test_settings.selenium.use_appium;

const Server = usingAppium
? require('./selenium-webdriver/appium.js')
: require('./selenium-webdriver/selenium.js');

cliRunner.seleniumService = Server.createService(cliRunner.test_settings);
cliRunner.test_settings.selenium['[_started]'] = true;

return cliRunner.seleniumService.init();
}

static usingSeleniumServer(settings) {
if (!settings.selenium) {
return false;
Expand Down Expand Up @@ -48,8 +61,22 @@ module.exports = class TransportFactory {

let {browserName} = settings.desiredCapabilities;

// Better support for Appium, if the browserName has explicitly been set to null we can skip all further checks
// Better support for app-testing, if the browserName is not present we can skip all further checks
const usingAppium = TransportFactory.usingSeleniumServer(settings) && settings.selenium.use_appium;
const usingBrowserStack = TransportFactory.usingBrowserstack(settings);
if ((usingAppium || usingBrowserStack) && !browserName) {
return browserName;
}

// for backward compatibility
if (browserName === null) {
// eslint-disable-next-line no-console
console.warn('DEPRECATED: Setting browserName=null for running Appium tests has been deprecated ' +
'and will not be supported in future versions. Set `use_appium` property in `selenium` config to true ' +
'in your Nightwatch configuration file to run Appium tests.');

settings.selenium.use_appium = true;

return browserName;
}

Expand Down Expand Up @@ -86,21 +113,26 @@ module.exports = class TransportFactory {

static createWebdriver(nightwatchInstance, {usingSeleniumServer, browserName}) {
if (TransportFactory.usingBrowserstack(nightwatchInstance.settings)) {
const Browserstack = require('./selenium-webdriver/browserstack.js');
if (!browserName) {
const AppAutomate = require('./selenium-webdriver/browserstack/appAutomate.js');

return new AppAutomate(nightwatchInstance, browserName);
}

const Automate = require('./selenium-webdriver/browserstack/automate.js');

return new Browserstack(nightwatchInstance, browserName);
return new Automate(nightwatchInstance, browserName);
}

if (usingSeleniumServer) {
const Selenium = require('./selenium-webdriver/selenium.js');

// support for mobile native apps
if (browserName === null) {
const Mobile = require('./selenium-webdriver/mobile-webdriver.js');
if (nightwatchInstance.settings.selenium.use_appium) {
const Appium = require('./selenium-webdriver/appium.js');

return new Mobile(nightwatchInstance);
return new Appium(nightwatchInstance, browserName);
}

const Selenium = require('./selenium-webdriver/selenium.js');

return new Selenium(nightwatchInstance, browserName);
}

Expand Down
72 changes: 72 additions & 0 deletions lib/transport/selenium-webdriver/appium.js
@@ -0,0 +1,72 @@
const AppiumBaseServer = require('./appiumBase.js');
const AppiumServiceBuilder = require('./service-builders/appium');
const {iosRealDeviceUDID} = require('../../utils/mobile.js');


class AppiumServer extends AppiumBaseServer {
static createService(settings) {
const Options = require('./options.js');
const opts = new Options({settings});
opts.updateWebdriverPath();

const appiumService = new AppiumServiceBuilder(settings);

const outputFile = settings.webdriver.log_file_name || '';
appiumService.setOutputFile(outputFile);

return appiumService;
}

get defaultBrowser() {
return null;
}

get ServiceBuilder() {
return AppiumServiceBuilder;
}

get defaultServerUrl() {
return 'http://127.0.0.1:4723';
}

get defaultPort() {
return 4723;
}

get defaultPathPrefix() {
return '/wd/hub';
}

createSessionOptions(argv) {
this.extractAppiumOptions();

// set 'appium:udid' if deviceId present in argv
if (argv && argv.deviceId) {
const platformName = this.desiredCapabilities.platformName;

let udid = argv.deviceId;
if (platformName && platformName.toLowerCase() === 'ios') {
udid = iosRealDeviceUDID(udid);
}

this.desiredCapabilities['appium:udid'] = udid;
}

// if `appium:chromedriverExecutable` is present and left blank,
// assign the path of binary from `chromedriver` NPM package to it.
if (this.desiredCapabilities['appium:chromedriverExecutable'] === '') {
const chromedriver = this.seleniumCapabilities.getChromedriverPath();
if (chromedriver) {
this.desiredCapabilities['appium:chromedriverExecutable'] = chromedriver;
}
}

return super.createSessionOptions(argv) || this.desiredCapabilities;
}

createDriver({options}) {
return this.createAppiumDriver({options});
}
};

module.exports = AppiumServer;
33 changes: 33 additions & 0 deletions lib/transport/selenium-webdriver/appiumBase.js
@@ -0,0 +1,33 @@
const {WebDriver} = require('selenium-webdriver');
const {Executor} = require('selenium-webdriver/http');
const http = require('selenium-webdriver/http');
const SeleniumServer = require('./selenium');
const {isObject} = require('../../utils');


class AppiumBaseServer extends SeleniumServer {
extractAppiumOptions() {
// break 'appium:options' to individual configs
if (isObject(this.desiredCapabilities['appium:options'])) {
const appiumOptions = this.desiredCapabilities['appium:options'];
for (let key of Object.keys(appiumOptions)) {
const value = appiumOptions[key];

if (!key.startsWith('appium:')) {
key = `appium:${key}`;
}
this.desiredCapabilities[key] = value;
}

delete this.desiredCapabilities['appium:options'];
}
}

createAppiumDriver({options}) {
const httpClient = new http.HttpClient(this.getServerUrl());

return WebDriver.createSession(new Executor(httpClient), options);
}
};

module.exports = AppiumBaseServer;
23 changes: 23 additions & 0 deletions lib/transport/selenium-webdriver/browserstack/appAutomate.js
@@ -0,0 +1,23 @@
const BrowserStack = require('./browserstack.js');

class AppAutomate extends BrowserStack {
get ApiUrl() {
return `https://api.browserstack.com/${this.productNamespace}`;
}

get productNamespace() {
return 'app-automate';
}

createSessionOptions() {
this.extractAppiumOptions();

return this.desiredCapabilities;
}

createDriver({options}) {
return this.createAppiumDriver({options});
}
}

module.exports = AppAutomate;
22 changes: 22 additions & 0 deletions lib/transport/selenium-webdriver/browserstack/automate.js
@@ -0,0 +1,22 @@
const BrowserStack = require('./browserstack.js');
const {Capabilities} = require('selenium-webdriver');

class Automate extends BrowserStack {
get ApiUrl() {
return `https://api.browserstack.com/${this.productNamespace}`;
}

get productNamespace() {
return 'automate';
}

createDriver({options = this.desiredCapabilities}) {
if (options instanceof Capabilities) {
return super.createDriver({options});
}

return this.createAppiumDriver({options});
}
}

module.exports = Automate;
@@ -1,13 +1,10 @@
const stripAnsi = require('strip-ansi');
const {Logger} = require('../../utils');
const BaseDriver = require('./mobile-webdriver.js');
const {Logger} = require('../../../utils');
const AppiumBaseServer = require('../appiumBase.js');
const defaultsDeep = require('lodash.defaultsdeep');

class Browserstack extends BaseDriver {
get ApiUrl() {
return `https://api.browserstack.com/${this.isMobile ? 'app-automate' : 'automate'}`;
}

class Browserstack extends AppiumBaseServer {
bStackOptions() {
return this.settings.desiredCapabilities['bstack:options'];
}
Expand All @@ -31,7 +28,6 @@ class Browserstack extends BaseDriver {
constructor(nightwatchInstance, browserName) {
super(nightwatchInstance, browserName);

this.isMobile = this.api.isMobile();
this.useLocal = false;

this.nightwatchInstance.on('nightwatch:session.create', (data) => {
Expand Down Expand Up @@ -146,7 +142,7 @@ class Browserstack extends BaseDriver {
await this.sendReasonToBrowserstack(!!failures, reason);
// eslint-disable-next-line no-console
console.log('\n ' + 'See more info, video, & screenshots on Browserstack:\n' +
' ' + Logger.colors.light_cyan(`https://${this.isMobile ? 'app-automate' : 'automate'}.browserstack.com/builds/${this.buildId}/sessions/${this.sessionId}`));
' ' + Logger.colors.light_cyan(`https://${this.productNamespace}.browserstack.com/builds/${this.buildId}/sessions/${this.sessionId}`));

this.sessionId = null;

Expand Down
5 changes: 1 addition & 4 deletions lib/transport/selenium-webdriver/index.js
Expand Up @@ -193,17 +193,14 @@ class Transport extends BaseTransport {

async createDriverService({options, moduleKey, reuseBrowser = false}) {
try {
moduleKey = this.settings.webdriver.log_file_name || moduleKey || '';

if (!this.shouldReuseDriverService(reuseBrowser)) {
Transport.driverService = new this.ServiceBuilder(this.settings);
await Transport.driverService.setOutputFile(reuseBrowser ? 'test' : moduleKey).init(options);
}

this.driverService = Transport.driverService;

moduleKey = this.settings.webdriver.log_file_name || moduleKey || '';

Transport.driverService = this.driverService;
} catch (err) {
this.showConnectSpinner(colors.red(`Failed to start ${this.serviceName}.`), 'warn');

Expand Down
25 changes: 0 additions & 25 deletions lib/transport/selenium-webdriver/mobile-webdriver.js

This file was deleted.

10 changes: 10 additions & 0 deletions lib/transport/selenium-webdriver/options.js
Expand Up @@ -243,10 +243,20 @@ module.exports = class SeleniumCapabilities {
}
}

getAppiumPath() {
return require.resolve('appium');
}

updateWebdriverPath() {
if (this.shouldSetupWebdriver()) {
try {
if (this.usingSeleniumServer()) {
if (this.settings.selenium.use_appium) {
this.settings.selenium.server_path = this.settings.webdriver.server_path = this.getAppiumPath();

return this;
}

this.settings.selenium.server_path = this.settings.webdriver.server_path = require('@nightwatch/selenium-server').path;
this.settings.selenium.cli_args = this.settings.selenium.cli_args || {};

Expand Down
13 changes: 6 additions & 7 deletions lib/transport/selenium-webdriver/selenium.js
@@ -1,30 +1,29 @@
const DefaultSeleniumDriver = require('./');
const SeleniumServerBuilder = require('./service-builders/selenium.js');
const SeleniumServiceBuilder = require('./service-builders/selenium.js');

module.exports = class SeleniumServer extends DefaultSeleniumDriver {
/**
* Used when running in parallel with start_process=true
*/
static startServer(settings) {
static createService(settings) {
const Options = require('./options.js');
const opts = new Options({settings});
opts.updateWebdriverPath();

const seleniumService = new SeleniumServerBuilder(settings);
const moduleKey = settings.webdriver.log_file_name || '';
const seleniumService = new SeleniumServiceBuilder(settings);

seleniumService.setOutputFile(moduleKey);
const outputFile = settings.webdriver.log_file_name || '';
seleniumService.setOutputFile(outputFile);

return seleniumService;

}

get defaultBrowser() {
return 'firefox';
}

get ServiceBuilder() {
return SeleniumServerBuilder;
return SeleniumServiceBuilder;
}

get defaultServerUrl() {
Expand Down

0 comments on commit f040731

Please sign in to comment.