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 15 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
7 changes: 5 additions & 2 deletions lib/runner/cli/cli.js
Expand Up @@ -469,8 +469,11 @@ 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);
const Server = this.test_settings.selenium.use_appium
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use a factory here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean factory as variable name or use a TransportFactory class method to decide whether appium is being used or not?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a factory to create the server

Copy link
Member Author

@garg3133 garg3133 Jan 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check 54587b1 if it is fine.

? require('../../transport/selenium-webdriver/appium.js')
: require('../../transport/selenium-webdriver/selenium.js');

this.seleniumService = Server.startServer(this.test_settings);
promise = this.seleniumService.init();
this.test_settings.selenium['[_started]'] = true;
}
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
27 changes: 20 additions & 7 deletions lib/transport/factory.js
Expand Up @@ -48,8 +48,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 @@ -92,15 +106,14 @@ module.exports = class TransportFactory {
}

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
83 changes: 83 additions & 0 deletions lib/transport/selenium-webdriver/appium.js
@@ -0,0 +1,83 @@
const path = require('path');
const {WebDriver} = require('selenium-webdriver');
const {Executor} = require('selenium-webdriver/http');
const TransportFactory = require('../factory');
const SeleniumServer = require('./selenium');
const AppiumServiceBuilder = require('./service-builders/appium');
const http = require('selenium-webdriver/http');
const {isObject} = require('../../utils');


module.exports = class AppiumServer extends SeleniumServer {
static startServer(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) {
// 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'];
}

// 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 = this.desiredCapabilities} = {}) {
// If creating a session with BrowserStack Automate, use Selenium's session builder.
if (TransportFactory.usingBrowserstack(this.settings) && this.productNamespace === 'automate') {
return super.createDriver({options});
}
garg3133 marked this conversation as resolved.
Show resolved Hide resolved

const httpClient = new http.HttpClient(this.getServerUrl());

return WebDriver.createSession(new Executor(httpClient), options);
}
};
8 changes: 4 additions & 4 deletions lib/transport/selenium-webdriver/browserstack.js
@@ -1,11 +1,11 @@
const stripAnsi = require('strip-ansi');
const {Logger} = require('../../utils');
const BaseDriver = require('./mobile-webdriver.js');
const BaseDriver = require('./appium.js');
garg3133 marked this conversation as resolved.
Show resolved Hide resolved
const defaultsDeep = require('lodash.defaultsdeep');

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

bStackOptions() {
Expand All @@ -31,7 +31,7 @@ class Browserstack extends BaseDriver {
constructor(nightwatchInstance, browserName) {
super(nightwatchInstance, browserName);

this.isMobile = this.api.isMobile();
this.productNamespace = browserName ? 'automate' : 'app-automate';
garg3133 marked this conversation as resolved.
Show resolved Hide resolved
this.useLocal = false;

this.nightwatchInstance.on('nightwatch:session.create', (data) => {
Expand Down Expand Up @@ -146,7 +146,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
11 changes: 5 additions & 6 deletions lib/transport/selenium-webdriver/selenium.js
@@ -1,5 +1,5 @@
const DefaultSeleniumDriver = require('./');
const SeleniumServerBuilder = require('./service-builders/selenium.js');
const SeleniumServiceBuilder = require('./service-builders/selenium.js');

module.exports = class SeleniumServer extends DefaultSeleniumDriver {
/**
Expand All @@ -10,21 +10,20 @@ module.exports = class SeleniumServer extends DefaultSeleniumDriver {
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
98 changes: 98 additions & 0 deletions lib/transport/selenium-webdriver/service-builders/appium.js
@@ -0,0 +1,98 @@
const {SeleniumServer, DriverService} = require('selenium-webdriver/remote');
const {getFreePort} = require('../../../utils');
const BaseService = require('./base-service.js');

class AppiumService extends DriverService {
constructor(server_path, opt_options) {
const options = opt_options || {};
const {args, default_path_prefix} = options;

const port = options.port;
if (port !== AppiumServiceBuilder.defaultPort && !args.includes('--port')) {
args.unshift('--port', port);
}

let cmd = 'node';
if (server_path.startsWith('appium')) {
cmd = server_path;
} else {
args.unshift(server_path);
}

super(cmd, {
loopback: options.loopback,
port,
args,
path: default_path_prefix,
env: options.env,
stdio: options.stdio
});
}
}

class AppiumServiceBuilder extends BaseService {
static get serviceName() {
return 'Appium Server';
}

static get defaultPort() {
return 4723;
}

get npmPackageName() {
return 'appium';
}

get outputFile() {
return this._outputFile + '_appium-server.log';
}

get defaultPort() {
return AppiumServiceBuilder.defaultPort;
}

get serviceName() {
return 'Appium Server';
}

get downloadMessage() {
return 'install Appium globally with "npm i -g appium" command, \n and set ' +
'"selenium.server_path" config option to "appium".';
}

/**
* @param {Capabilities} opts
* @returns {Promise<void>}
*/
async createService(opts = {}) {
const {port} = this;
const options = new SeleniumServer.Options();
options.port = port || await getFreePort();
const {server_path, default_path_prefix = '/wd/hub'} = this.settings.webdriver;

const introMsg = `Starting Appium Server on port ${options.port}...`;

if (opts.showSpinner) {
opts.showSpinner(`${introMsg}\n\n`);
} else {
// eslint-disable-next-line
console.info(introMsg);
}

// TODO: read the log_path and add it to cliArgs
// above TODO is copied from ./selenium.js
options.args = this.cliArgs;
options.default_path_prefix = default_path_prefix;

if (this.hasSinkSupport() && this.needsSinkProcess()) {
this.createSinkProcess();
options.stdio = ['pipe', this.process.stdin, this.process.stdin];
}

this.service = new AppiumService(server_path, options);

return this.service.start();
}
}

module.exports = AppiumServiceBuilder;
Expand Up @@ -359,7 +359,7 @@ class BaseService {
return resolve();
}

Logger.info(`Wrote log file to: ${filePath}.`);
Logger.info(`Wrote log file to: ${filePath}`);
this.stopped = true;
resolve();
});
Expand Down