Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Appium support in Nightwatch #3519

Merged
merged 26 commits into from Jan 6, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0531906
Automatically start appium server in Nightwatch.
garg3133 Dec 13, 2022
e6846e9
Create separate startServer method in AppiumServer.
garg3133 Dec 15, 2022
752812a
Convert relative paths to absolute paths for Appium.
garg3133 Dec 21, 2022
124b46e
Remove appium_version, for now.
garg3133 Dec 21, 2022
f9d4c14
Use chromedriver npm package if path not explicitely passed.
garg3133 Dec 22, 2022
39eca9f
Add tests for appium session and service builder.
garg3133 Dec 23, 2022
f058e1f
Add more tests for changes.
garg3133 Dec 25, 2022
9d56c82
Fix testAppiumOptions tests.
garg3133 Dec 25, 2022
a8d2c11
Add backward compatibility for browserName=null.
garg3133 Dec 26, 2022
8916448
Skip Appium's createSessionOptions for BrowserStack.
garg3133 Dec 26, 2022
709fd41
Fix tests.
garg3133 Dec 26, 2022
7b40016
Fix client.click() test.
garg3133 Dec 26, 2022
c76e481
isAppium -> use_appium
garg3133 Dec 26, 2022
e6c85aa
Remove the logic for converting relative paths to absolute for Appium.
garg3133 Dec 29, 2022
e000786
Resolve BrowserStack related issues.
garg3133 Dec 29, 2022
1167538
Create separate classes for Automate and AppAutomate.
garg3133 Dec 30, 2022
d7a996e
Modify BrowserStack's folder structure.
garg3133 Jan 3, 2023
bf62d81
Fix appium server tests.
garg3133 Jan 3, 2023
48bb1b9
Fix import paths.
garg3133 Jan 3, 2023
273af20
Add browserstack transport and createSession tests.
garg3133 Jan 3, 2023
27ba564
Increase appium server startup timeout to 15sec.
garg3133 Jan 4, 2023
54587b1
Create a factory method to create service.
garg3133 Jan 4, 2023
0706064
AppiumMixin -> AppiumBaseServer.
garg3133 Jan 4, 2023
79bebbb
Pass udid using --deviceId flag.
garg3133 Jan 4, 2023
019b395
Fix failing tests.
garg3133 Jan 4, 2023
e7fac99
Remove unused function from testAppiumServer.
garg3133 Jan 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -469,10 +470,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 @@ -146,7 +146,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;
garg3133 marked this conversation as resolved.
Show resolved Hide resolved
} 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