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 13 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
25 changes: 19 additions & 6 deletions lib/transport/factory.js
Expand Up @@ -49,7 +49,21 @@ 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
if (settings.selenium && settings.selenium.use_appium && !browserName) {
return browserName;
}

// for backward compatibility
if (browserName === null) {
// TODO:
// Deprecation warning: Setting browserName to null for running Appium tests has
// now been deprecated and will be removed from future versions. Set `use_appium`
// property to true in `selenium` config to run Appium tests.
//
// put a warning here that if trying to connect to an Appium server,
// just setting browserName=null will not work in the future.
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
102 changes: 102 additions & 0 deletions lib/transport/selenium-webdriver/appium.js
@@ -0,0 +1,102 @@
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) {
// TODO: Re-think the below temp solution
if (TransportFactory.usingBrowserstack(this.settings)) {
return super.createSessionOptions(argv);
}
garg3133 marked this conversation as resolved.
Show resolved Hide resolved

const options = this.desiredCapabilities;

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

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

delete options['appium:options'];
}

const isRelativePath = (value) => {
if (typeof value !== 'string') {
return false;
}

return value.startsWith('./') || value.startsWith('../');
};

// convert relative paths to absolute paths
for (const key of Object.keys(options)) {
if (key.startsWith('appium:') && isRelativePath(options[key])) {
options[key] = path.resolve(options[key]);
}
}

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

return options;
}

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

const session = WebDriver.createSession(new Executor(httpClient), options);

return session;
}
};
2 changes: 1 addition & 1 deletion lib/transport/selenium-webdriver/browserstack.js
@@ -1,6 +1,6 @@
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 {
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
2 changes: 0 additions & 2 deletions test/lib/mocks/mocks-w3c.yaml
Expand Up @@ -103,7 +103,6 @@ mocks:
firstMatch: [{}]
alwaysMatch:
appium:automationName: "XCUITest"
appium:app: "../samples/Wikipedia.app"
appium:appPackage: "org.wikimedia.wikipedia"
platformName: "iOS"
appium:deviceName: "iPhone 13"
Expand All @@ -113,7 +112,6 @@ mocks:
sessionId: '13521-10219-202'
capabilities:
appium:automationName: "XCUITest"
appium:app: "../samples/Wikipedia.app"
appium:appPackage: "org.wikimedia.wikipedia"
platformName: "iOS"
appium:deviceName: "iPhone 13"
Expand Down