Repository: nightwatchjs/nightwatch
Pull request #3496 (2022-11-24 13:06:23)

Add commandName, parameters and results to report object
Add commandName and arguments to report. Will be used in HTML Reporter v2 





Patch:
diff --git a/lib/api/_loaders/_base-loader.js b/lib/api/_loaders/_base-loader.js
index e619f217..b5571838 100644
--- a/lib/api/_loaders/_base-loader.js
+++ b/lib/api/_loaders/_base-loader.js
@@ -119,6 +119,14 @@ class BaseLoader extends EventEmitter {
     this.__isUserDefined = val;
   }
 
+  get addedInsideCallback() {
+    return this.__addedInsideCallback;
+  }
+
+  set addedInsideCallback(val) {
+    this.__addedInsideCallback = val;
+  }
+
   set ignoreUnderscoreLeadingNames(val) {
     this.__ignoreUnderscoreNames = val;
   }
@@ -268,6 +276,27 @@ class BaseLoader extends EventEmitter {
 
       const options = this.getCommandOptions();
 
+
+      if (args && args.length > 0 && Utils.isFunction(args[args.length-1])) {
+        const callback = args.pop();
+        
+        const userCallbackWrapper = (function(context) {
+          
+          const proxyFn = new Proxy(callback, {
+            apply: function(target, thisArg, argumentsList) {
+              context.addedInsideCallback = true;
+          
+              return target.apply(thisArg, argumentsList);
+            }
+          });
+          proxyFn.originalTarget = callback;
+          
+          return  proxyFn;
+        })(this);
+
+        args.push(userCallbackWrapper);
+      }
+
       // if this command was called from an async test case
       let isES6Async = options.alwaysAsync || nightwatchInstance.settings.always_async_commands;
 
diff --git a/lib/api/protocol/_base-action.js b/lib/api/protocol/_base-action.js
index 4306b7e3..0bc6f15b 100644
--- a/lib/api/protocol/_base-action.js
+++ b/lib/api/protocol/_base-action.js
@@ -172,6 +172,10 @@ module.exports = class ProtocolAction {
   executeScriptHandler(method, script, args, callback) {
     let fn;
 
+    if (script.originalTarget) {
+      script = script.originalTarget;
+    }
+
     if (isFunction(script)) {
       fn = 'var passedArgs = Array.prototype.slice.call(arguments,0); return (' +
         script.toString() + ').apply(window, passedArgs);';
diff --git a/lib/core/asynctree.js b/lib/core/asynctree.js
index 1f0f846b..08cf90ac 100644
--- a/lib/core/asynctree.js
+++ b/lib/core/asynctree.js
@@ -112,6 +112,8 @@ class AsyncTree extends EventEmitter{
     this.currentNode = node;
     this.currentNode.started = true;
     Logger.log(`\n ${Logger.colors.green('→')} Running command: ${Logger.colors.light_green(node.fullName)} (${AsyncTree.printArguments(node)})`);
+    
+    this.emit('asynctree:command:start', {node});
 
     const result = await node.run();
     const {parent} = this.currentNode;
@@ -144,6 +146,8 @@ class AsyncTree extends EventEmitter{
     Logger.log(` ${Logger.colors.green('→')} Completed command: ${Logger.colors.light_green(node.fullName)}` +
       ` (${AsyncTree.printArguments(node)}) (${node.elapsedTime}ms)`);
 
+    this.emit('asynctree:command:finished', {node, result});
+
     if (abortOnFailure) {
       this.empty();
       this.createRootNode();
diff --git a/lib/core/client.js b/lib/core/client.js
index 505678a4..8e336272 100644
--- a/lib/core/client.js
+++ b/lib/core/client.js
@@ -471,7 +471,7 @@ class NightwatchClient extends EventEmitter {
     let value = this.settings.baseUrl || this.settings.launchUrl || this.settings.launch_url || null;
 
     // For e2e and component testing on android emulator
-    if (!this.settings.desiredCapabilities.real_mobile && this.settings.desiredCapabilities.avd) {         
+    if (value && !this.settings.desiredCapabilities.real_mobile && this.settings.desiredCapabilities.avd) {         
       value = value.replace('localhost', '10.0.2.2').replace('127.0.0.1', '10.0.2.2');
     }
 
diff --git a/lib/core/queue.js b/lib/core/queue.js
index 4c6736b8..e876d8ee 100644
--- a/lib/core/queue.js
+++ b/lib/core/queue.js
@@ -31,6 +31,8 @@ class CommandQueue extends EventEmitter {
 
   add({commandName, commandFn, context, args, stackTrace, namespace, options = {}, deferred, isES6Async, rejectPromise}) {
     const {compatMode} = this;
+    const parentContext = this.currentNode.context;
+
     const node = new Node({
       name: commandName,
       parent: this.currentNode,
@@ -39,7 +41,8 @@ class CommandQueue extends EventEmitter {
       deferred,
       isES6Async,
       rejectPromise,
-      compatMode
+      compatMode,
+      addedInsideCallback: parentContext && parentContext.addedInsideCallback
     });
 
     if (this.currentNode.instance && this.currentNode.instance.isES6AsyncCommand) {
diff --git a/lib/core/treenode.js b/lib/core/treenode.js
index 442448fe..622bb915 100644
--- a/lib/core/treenode.js
+++ b/lib/core/treenode.js
@@ -51,7 +51,7 @@ class TreeNode {
   /**
    * @param {{name, namespace, stackTrace, parent, deferred}} opts
    */
-  constructor({name, parent, namespace, stackTrace, deferred, isES6Async, compatMode, rejectPromise}) {
+  constructor({name, parent, namespace, stackTrace, deferred, isES6Async, compatMode, rejectPromise, addedInsideCallback}) {
     this.__command = null;
     this.__context = null;
     this.__args = null;
@@ -75,6 +75,7 @@ class TreeNode {
     this.done = false;
     this.startTime = null;
     this.promiseSettled = false;
+    this.addedInsideCallback = addedInsideCallback;
   }
 
   getResult(result) {
@@ -136,7 +137,8 @@ class TreeNode {
     }
 
     this.done = true;
-    this.elapsedTime = new Date().getTime() - this.startTime;
+    this.endTime = new Date().getTime();
+    this.elapsedTime =  this.endTime - this.startTime;
 
     return result;
   }
diff --git a/lib/http/request.js b/lib/http/request.js
index 1c0543ec..7207d3a6 100644
--- a/lib/http/request.js
+++ b/lib/http/request.js
@@ -1,6 +1,7 @@
 const EventEmitter = require('events');
 const http   = require('http');
 const https  = require('https');
+const dns = require('dns');
 const HttpUtil = require('./http.js');
 const HttpOptions = require('./options.js');
 const Auth = require('./auth.js');
@@ -9,6 +10,11 @@ const HttpResponse = require('./response.js');
 const Utils = require('./../utils');
 const {Logger, isString} = Utils;
 
+// To handle Node v17 issue. Refer https://github.com/nodejs/node/issues/40702 for details.
+if (dns.setDefaultResultOrder && (typeof dns.setDefaultResultOrder === 'function')) {
+  dns.setDefaultResultOrder('ipv4first');
+}
+
 const __defaultSettings__ = {
   credentials: null,
   use_ssl: false,
diff --git a/lib/reporter/base-reporter.js b/lib/reporter/base-reporter.js
index e6456d57..03f49e38 100644
--- a/lib/reporter/base-reporter.js
+++ b/lib/reporter/base-reporter.js
@@ -1,6 +1,7 @@
 const fs = require('fs');
 const stripAnsi = require('strip-ansi');
 const Utils = require('../utils');
+const Results = require('./results');
 
 module.exports = class BaseReporter {
   constructor(results, opts = {}) {
@@ -36,6 +37,29 @@ module.exports = class BaseReporter {
 
       return prev;
     }, {});
+
+    module.completedSections = Object.keys(module.completedSections).reduce(function(prev, item) {
+      if (module.skipped && module.skipped.includes(item)) {
+        return prev;
+      }
+
+      const testcase = module.completedSections[item];
+      const commands = testcase.commands;
+
+      for (let i = 0; i < commands.length; i++) {
+        if (commands[i].status === Results.TEST_FAIL && commands[i].result.stack) {
+          commands[i].result.message = stripAnsi(commands[i].result.message || '');
+          if (commands[i].result.stack) {
+            commands[i].result.stack = stripAnsi(commands[i].result.stack);
+            commands[i].result.beautifiedStack = Utils.beautifyStackTrace(commands[i].result.stack, true, module.modulePath, false);
+          }
+        }
+      }
+
+      prev[item] = testcase;
+
+      return prev;
+    }, {});
   }
 
   writeReportFile(filename, rendered, shouldCreateFolder, output_folder) {
diff --git a/lib/reporter/global-reporter.js b/lib/reporter/global-reporter.js
index 22be6861..3dd974fe 100644
--- a/lib/reporter/global-reporter.js
+++ b/lib/reporter/global-reporter.js
@@ -1,5 +1,6 @@
 const path = require('path');
 const Convert = require('ansi-to-html');
+const stripAnsi = require('strip-ansi');
 
 const Concurrency = require('../runner/concurrency');
 const DefaultSettings = require('../settings/defaults.js');
@@ -21,7 +22,8 @@ module.exports = class GlobalReporter {
       tests: 0,
       assertions: 0,
       errmessages: [],
-      modules: {}
+      modules: {},
+      modulesWithEnv: {}
     };
   }
 
@@ -57,11 +59,16 @@ module.exports = class GlobalReporter {
 
   addTestSuiteResults(testResults, httpOutput) {
     testResults = testResults || {};
-    testResults.httpOutput = (httpOutput || Logger.collectOutput()).map(item => {
+    const loggerOutput = httpOutput || Logger.collectOutput();
+    testResults.httpOutput = loggerOutput.map(item => {
       return [item[0], this.ansiConverter.toHtml(item[1]),
         (item[2] ? this.ansiConverter.toHtml(item[2]) : '')];
     });
 
+    testResults.rawHttpOutput = loggerOutput.map(item => {
+      return [item[0], stripAnsi(item[1] || '').replace(/&#39;/g, '\''),
+        (item[2] ? stripAnsi(item[2]) : '').replace(/&#39;/g, '\'')];
+    });
     this.suiteResults.push(testResults);
 
     return this;
@@ -340,6 +347,10 @@ module.exports = class GlobalReporter {
   }
 
   save() {
+    if (Concurrency.isChildProcess()) {
+      return Promise.resolve();
+    }
+
     return Promise.all([
       this.runCustomGlobalReporter(this.globalResults),
       this.writeReportToFile(this.globalResults)
diff --git a/lib/reporter/index.js b/lib/reporter/index.js
index 841b6a48..de41920d 100644
--- a/lib/reporter/index.js
+++ b/lib/reporter/index.js
@@ -31,6 +31,7 @@ class Reporter extends SimplifiedReporter {
     this.currentContext = null;
     this.__printA11Report = false;
     this.reporter = addOpts.repoter;
+    this.isMobile = addOpts.isMobile;
 
     this.testResults.initCurrentTest({
       module: addOpts.moduleKey,
@@ -47,6 +48,10 @@ class Reporter extends SimplifiedReporter {
   get currentTest() {
     return this.testResults.currentTest;
   }
+  
+  get currentSection() {
+    return this.testResults.currentSection;
+  }
 
   get printA11Report() {
     return this.__printA11Report;
@@ -59,9 +64,14 @@ class Reporter extends SimplifiedReporter {
   setCurrentTest(testcase, context) {
     this.currentContext = context;
 
+    this.setCurrentSection(testcase);
     this.testResults.setCurrentTest(testcase);
   }
 
+  setCurrentSection(testcase) {
+    this.testResults.setCurrentSection(testcase);
+  }
+
   setAxeResults(results) {
     this.currentTest.results.a11y = results;
   }
@@ -162,6 +172,76 @@ class Reporter extends SimplifiedReporter {
     this.testResults.logAssertion(result);
   }
 
+  /**
+   * @param {TheeNode} node
+   * @param {Object} result
+   */
+  logCommandResult({node, result}) {
+    try {
+      let commandResult = result;
+
+      const isSuccess =  result === undefined ||
+        result == null ||
+        !((result instanceof Error) ||
+          (result.error instanceof Error) ||
+          result.status === -1
+        );
+
+      // Use only necessary values 
+      if (result) {
+        const {status, message, showDiff, name, abortOnFailure, stack, beautifiedStack} = result;
+        commandResult ={
+          status,
+          message,
+          showDiff,
+          name,
+          abortOnFailure,
+          stack,
+          beautifiedStack
+        };
+      } 
+      
+      if (this.shouldLogCommand(node)) {
+        this.testResults.logCommand({
+          name: node.fullName,
+          args: Reporter.stringifyArgs(node.args),
+          startTime: node.startTime,
+          endTime: node.endTime,
+          elapsedTime: node.elapsedTime,
+          status: isSuccess ? TestResults.TEST_PASS : TestResults.TEST_FAIL,
+          result: commandResult
+        });
+      }
+
+    } catch (e) {
+      // ignore exceptions
+    }
+  }
+
+  static stringifyArgs(args) {
+    if (Array.isArray(args)) {
+      return args.map((arg) => arg && arg.toString());
+    }
+
+    return args;
+  }
+
+  shouldLogCommand(node) {
+    if (!node.parent) { //root node
+      return false;
+    }
+    
+    if (node.parent.isRootNode) {
+      return true;
+    }
+
+    if (node.addedInsideCallback) {
+      return true;
+    }
+
+    return false;
+  }
+
   registerPassed(message) {
     Logger.logDetailedMessage(`  ${colors.green(Utils.symbols.ok)} ${message}`);
     this.testResults.incrementPassedCount();
diff --git a/lib/reporter/results.js b/lib/reporter/results.js
index 3235e053..6d085e8f 100644
--- a/lib/reporter/results.js
+++ b/lib/reporter/results.js
@@ -11,29 +11,38 @@ module.exports = class Results {
     return 'pass';
   }
 
+  static get TEST_SKIP() {
+    return 'skip';
+  }
+
   constructor(tests = [], opts, settings) {
     this.skipped = tests.slice(0);
     this.testcases = {};
+    this.testSections = {};
     this.suiteName = opts.suiteName;
     this.moduleKey = opts.moduleKey;
     this.modulePath = opts.modulePath;
     this.groupName = opts.groupName;
     this.reportPrefix = opts.reportPrefix;
+    this.testEnv = settings.testEnv;
+    this.isMobile = opts.isMobile;
     this.globalStartTime = new Date().getTime();
     this.startTimestamp = this.globalStartTime;
     this.endTimestamp = this.globalStartTime;
     this.currentTestName = '';
+    this.currentSectionName = '';
     this.__currentTest = null;
     this.__initialResult = {
       errors: 0,
       failed: 0,
       passed: 0,
       assertions: [],
+      commands: [],
       tests: 0
     };
     // Adding sessionInfo to reporter
     const {capabilities = {}, desiredCapabilities = {}} = settings;
-    this.sessionCapabilities = {};
+    this.sessionCapabilities = capabilities || desiredCapabilities;
     this.sessionId = '';
     this.projectName = capabilities.projectName || desiredCapabilities.projectName || '';
     this.buildName = capabilities.buildName || desiredCapabilities.buildName || '';
@@ -64,6 +73,7 @@ module.exports = class Results {
     }
 
     const currentTest = this.testcases[testName];
+
     if (returnFullResult) {
       currentTest.steps = this.skipped;
       currentTest.stackTrace = this.stackTrace;
@@ -86,6 +96,14 @@ module.exports = class Results {
     return currentTest;
   }
 
+  getTestSection(testName) {
+    if (!testName || !this.testSections[testName]) {
+      return this.initialResult;
+    }
+
+    return this.testSections[testName];
+  }
+
   /**
    * @return {TestCase}
    */
@@ -109,6 +127,10 @@ module.exports = class Results {
     };
   }
 
+  get currentSection() {
+    return this.getTestSection(this.currentSectionName);
+  }
+
   hasFailures() {
     return this.currentTestResult.errors > 0 || this.currentTestResult.failed > 0;
   }
@@ -189,7 +211,9 @@ module.exports = class Results {
     suiteResults.results.lastError = lastError;
     suiteResults.results.skipped = this.skipped;
     suiteResults.results.time = this.time;
+    suiteResults.results.timeMs = this.timeMs;
     suiteResults.results.completed = this.testcases;
+    suiteResults.results.completedSections = this.testSections;
     suiteResults.results.errmessages = this.errmessages;
     suiteResults.results.testsCount = this.testsCount;
     suiteResults.results.skippedCount = this.skippedCount;
@@ -204,7 +228,11 @@ module.exports = class Results {
     suiteResults.results.sessionId = this.sessionId;
     suiteResults.results.projectName = this.projectName;
     suiteResults.results.buildName = this.buildName;
-
+    suiteResults.results.testEnv = this.testEnv;
+    suiteResults.results.isMobile = this.isMobile;
+    suiteResults.results.status = this.getTestStatus();
+    suiteResults.results.seleniumLog = this.seleniumLog;
+    
     // Backwards compat
     suiteResults.results.tests = this.testsCount;
     suiteResults.results.failures = this.failedCount;
@@ -228,6 +256,10 @@ module.exports = class Results {
     this.currentTestName = '';
   }
 
+  resetCurrentSectionName() {
+    this.currentSectionName = '';
+  }
+
   getCurrentTestName() {
     // FIXME: see resetCurrentTestName()
     const {testName} = this.getCurrentTest() || {};
@@ -243,12 +275,14 @@ module.exports = class Results {
     return {
       time: 0,
       assertions: [],
+      commands: [],
       passed: 0,
       errors: 0,
       failed: 0,
       retries: testcase.retriesCount,
       skipped: 0,
-      tests: 0
+      tests: 0,
+      status: Results.TEST_PASS
     };
   }
 
@@ -332,6 +366,13 @@ module.exports = class Results {
     return this;
   }
 
+  logCommand(command) {
+    const result = this.getTestSection(this.currentSectionName);
+    result.commands.push(command);
+
+    return this;
+  }
+  
   initCount(tests) {
     this.__passedCount = 0;
     this.__failedCount = 0;
@@ -365,18 +406,22 @@ module.exports = class Results {
 
   setTestStatus() {
     const currentTest = this.getCurrentTest();
+    const currentSection = this.getTestSection(this.currentSectionName);
     if (!currentTest) {
       return;    
     }
 
     if (this.hasFailures()) {
       this.currentTestResult.status = Results.TEST_FAIL;
+      currentSection.status = Results.TEST_FAIL;
     } else {
       this.currentTestResult.status = Results.TEST_PASS;
+      currentSection.status = Results.TEST_PASS;
     }
   }
 
   setTotalElapsedTime() {
+    this.timeMs = this.time;
     this.time = (this.time/1000).toPrecision(4);
 
     return this;
@@ -406,6 +451,15 @@ module.exports = class Results {
     return this;
   }
 
+  setCurrentSection(testcase) {
+    this.currentSectionName = testcase.testName;
+    this.testSections[testcase.testName] = this.createTestCaseResults(testcase);
+  }
+
+  setSeleniumLogFile(outputFilePath) {
+    this.seleniumLog = outputFilePath;
+  }
+
   logScreenshotFile(screenshotFile) {
     let assertions = this.currentTestResult.assertions;
     let lastAssertion = assertions[assertions.length - 1];
@@ -420,6 +474,18 @@ module.exports = class Results {
     return this.failedCount === 0 && this.errorsCount === 0;
   }
 
+  getTestStatus() {
+    if (this.failedCount > 0 || this.errorsCount > 0) {
+      return Results.TEST_FAIL;
+    } 
+
+    if (this.passedCount > 0) {
+      return Results.TEST_PASS;
+    }
+
+    return Results.TEST_SKIP;
+  }
+
   /**
    * Combines all the individual test suite reports into a global one
    *
@@ -448,7 +514,10 @@ module.exports = class Results {
       }
 
       results.httpOutput = item.httpOutput || [];
+      results.rawHttpOutput = item.rawHttpOutput || [];
       prev.modules[item.moduleKey] = results;
+      prev.modulesWithEnv[item.results.testEnv] = prev.modulesWithEnv[item.results.testEnv] || {};
+      prev.modulesWithEnv[item.results.testEnv][item.moduleKey] = results;
 
       return prev;
     }, initialReport);
diff --git a/lib/reporter/simplified.js b/lib/reporter/simplified.js
index 3d3070f4..314518ec 100644
--- a/lib/reporter/simplified.js
+++ b/lib/reporter/simplified.js
@@ -30,6 +30,13 @@ class SimplifiedReporter {
     return this;
   }
 
+  /**
+   * @param {Object} result
+   */
+  logCommandResult(node, result) {
+    return this;
+  }
+
   logFailedAssertion(err) {
     Logger.logDetailedMessage(`  ${Logger.colors.red(Utils.symbols.fail)} ${err.message}`);
 
diff --git a/lib/reporter/summary.js b/lib/reporter/summary.js
index b09ea37c..05ba36b7 100644
--- a/lib/reporter/summary.js
+++ b/lib/reporter/summary.js
@@ -74,7 +74,7 @@ module.exports = class Summary {
   static printErrors(testSuite) {
     if (Array.isArray(testSuite.errmessages)) {
       testSuite.errmessages = testSuite.errmessages.reduce((prev, val) => {
-        if (!testSuite.globalErrorRegister.includes(val)) {
+        if (testSuite.globalErrorRegister && !testSuite.globalErrorRegister.includes(val)) {
           testSuite.globalErrorRegister.push(val);
           prev.push(val);
         }
diff --git a/lib/settings/settings.js b/lib/settings/settings.js
index 82d843ad..f78682b1 100644
--- a/lib/settings/settings.js
+++ b/lib/settings/settings.js
@@ -138,6 +138,7 @@ class Settings {
    */
   initSettingsObject() {
     this.settings = Settings.getDefaults();
+    this.settings.testEnv = this.testEnv;
   }
 
   /**
diff --git a/lib/transport/selenium-webdriver/index.js b/lib/transport/selenium-webdriver/index.js
index f5b63c31..b64a2e3f 100644
--- a/lib/transport/selenium-webdriver/index.js
+++ b/lib/transport/selenium-webdriver/index.js
@@ -451,6 +451,10 @@ class Transport extends BaseTransport {
     );
   }
 
+  getOutputFilePath() {
+    return this.driverService.getOutputFilePath();
+  }
+
   getErrorResponse(result) {
     return result instanceof Error ? result : result.error;
   }
diff --git a/lib/transport/selenium-webdriver/options.js b/lib/transport/selenium-webdriver/options.js
index f5eb9e5b..246a0ad0 100644
--- a/lib/transport/selenium-webdriver/options.js
+++ b/lib/transport/selenium-webdriver/options.js
@@ -317,8 +317,8 @@ module.exports = class SeleniumCapabilities {
   }
 
   addHeadlessOption({options}) {
-    if (this.argv.headless && (options instanceof Capabilities) && options.headless) {
-      options.headless();
+    if (this.argv.headless && (options instanceof Capabilities) && (this.isChrome || options.headless)) {
+      this.isChrome ? options.addArguments('headless=chrome') : options.headless();
     }
 
     return this;
diff --git a/lib/transport/selenium-webdriver/service-builders/base-service.js b/lib/transport/selenium-webdriver/service-builders/base-service.js
index 2d83d5ea..f6c0890b 100644
--- a/lib/transport/selenium-webdriver/service-builders/base-service.js
+++ b/lib/transport/selenium-webdriver/service-builders/base-service.js
@@ -233,6 +233,9 @@ class BaseService {
     const {hostname, port} = this;
     let serverPathLog = '';
 
+
+    this.seleniumLogPath = this.settings.webdriver.log_path;
+
     if (server_path) {
       serverPathLog = ` with server_path=${server_path}`;
     }
@@ -316,6 +319,10 @@ class BaseService {
     return this;
   }
 
+  getSeleniumLogPath() {
+    return path.resolve(this.seleniumLogPath || 'logs');
+  }
+
   getLogPath() {
     let {log_path = 'logs'} = this.settings.webdriver;
     if (log_path === false) {
@@ -325,6 +332,15 @@ class BaseService {
     return path.resolve(log_path);
   }
 
+  getSeleniumOutputFilePath() {
+    const {log_file_name} = this.settings.webdriver;
+    if (log_file_name) {
+      this._outputFile = log_file_name;
+    }
+
+    return path.join(this.getSeleniumLogPath(), this.outputFile);
+  }
+
   getOutputFilePath() {
     const logPath = this.getLogPath();
     if (!logPath) {
diff --git a/lib/utils/beautifyStackTrace.js b/lib/utils/beautifyStackTrace.js
index 19a33056..d7ef4f18 100644
--- a/lib/utils/beautifyStackTrace.js
+++ b/lib/utils/beautifyStackTrace.js
@@ -9,29 +9,38 @@ const {colors} = require('./colors.js');
  * Read the User file from the stackTrace and create a string with highlighting the line with error
  * @param {Error} err
  */ 
-function beautifyStackTrace(err, errStackPassed = false, modulepath) {
+function beautifyStackTrace(err, errStackPassed = false, modulepath, cli = true) {
   if ((err instanceof AssertionError) || alwaysDisplayError(err) || errStackPassed) {
     try {
       const errorStack = errStackPassed ? err : err.stack;
 
       const parsedStacks = stackTraceParser.parse(filterStackTrace(errorStack));
-      const parsedStack = modulepath ? parsedStacks.find(o => o.file === modulepath) : parsedStacks[0];
+      let parsedStack = modulepath ? parsedStacks.find(o => o.file === modulepath) : parsedStacks[0];
 
+      if (!parsedStack) {
+        parsedStack = parsedStacks[0];
+      }
+      
       const file = fs.readFileSync(parsedStack.file, 'utf-8');
       const errorLinesofFile = file.split(/\r?\n/);
-      
-      const desiredLines = errorLinesofFile.reduce(function(lines, newLine, lineIndex) {
-        const currentLine = lineIndex + 1;
-        if (currentLine === parsedStack.lineNumber) {
-          lines += '\n    ' + colors.light_gray(` ${currentLine} | ${newLine} `, colors.background.red);
-        } else if (currentLine <= (parsedStack.lineNumber + 2) && currentLine >= (parsedStack.lineNumber - 2)) {
-          lines += `\n     ${currentLine} | ${newLine}`;
+
+      const formattedStack = formatStackTrace(errorLinesofFile, parsedStack);
+      if (!cli) {
+        return formattedStack;
+      }
+
+      const desiredLines = formattedStack.codeSnippet.reduce(function(lines, newLine) {
+        const currentLine = newLine.line_number;
+        if (currentLine === formattedStack.error_line_number) {
+          lines += '\n    ' + colors.light_gray(` ${currentLine} | ${newLine.code} `, colors.background.red);
+        } else if (currentLine <= (formattedStack.error_line_number + 2) && currentLine >= (formattedStack.error_line_number - 2)) {
+          lines += `\n     ${currentLine} | ${newLine.code}`;
         }
     
         return lines;
       }, '');
 
-      let delimiter = (new Array(parsedStack.file.length + 3).join('–'));
+      const delimiter = (new Array(parsedStack.file.length + 3).join('–'));
 
       return '    '  + parsedStack.file + ':\n    ' + delimiter + desiredLines + '\n    ' + delimiter + '\n';
     } catch (err) {
@@ -42,5 +51,31 @@ function beautifyStackTrace(err, errStackPassed = false, modulepath) {
   return '';
 }
 
+/**
+ * Read the User file from the stackTrace and create a string with highlighting the line with error
+ * @param {Error} err
+ */ 
+function formatStackTrace(errorLinesofFile, parsedStack) {
+
+  const result = {
+    filePath: parsedStack.file,
+    error_line_number: parsedStack.lineNumber,
+    codeSnippet: []
+  };
+
+  errorLinesofFile.reduce(function(lines, newLine, lineIndex) {
+    const currentLine = lineIndex + 1;
+    if (currentLine <= (parsedStack.lineNumber + 2) && currentLine >= (parsedStack.lineNumber - 2)) {
+      result.codeSnippet.push({
+        line_number: currentLine, code: newLine
+      });
+    }
+  }, '');
+
+
+  return result;
+   
+}
+
 module.exports = beautifyStackTrace;
 


Test patch:
diff --git a/lib/testsuite/index.js b/lib/testsuite/index.js
index 4bffa560..72d3cb2a 100644
--- a/lib/testsuite/index.js
+++ b/lib/testsuite/index.js
@@ -213,7 +213,8 @@ class TestSuite {
         modulePath,
         reportPrefix: '',
         reportFileName: this.argv['report-filename'],
-        groupName
+        groupName,
+        isMobile: this.client.api.isMobile()
       }
     });
   }
@@ -424,6 +425,10 @@ class TestSuite {
 
   runHook(hookName) {
     Logger.log(`${Logger.colors.green('→')} Running [${hookName}]:`);
+    
+    if (hookName === 'before' || hookName === 'after') {
+      this.client.reporter.setCurrentSection({testName: `__${hookName}_hook`});
+    }
 
     return this.handleRunnable(hookName, () => {
       return this.hooks[hookName].run(this.client);
@@ -439,6 +444,8 @@ class TestSuite {
       return Promise.resolve();
     }
 
+    this.client.reporter.setCurrentSection({testName: `__global_${hookName}_hook`});
+    
     return this.handleRunnable(hookName, async () => {
       if (this.globalsInstance) {
         await this.globalsInstance.runPluginHook(hookName, [this.settings]);
@@ -482,7 +489,9 @@ class TestSuite {
             this.settings.deviceUDID = data.capabilities['safari:deviceUDID'];
           }
         }
-        
+
+        this.commandQueue.tree.on('asynctree:command:finished', this.reporter.logCommandResult.bind(this.reporter));
+
         return this.runGlobalHook('beforeEach');
       });
   }
@@ -507,6 +516,8 @@ class TestSuite {
     return this.startTestSuite()
       .then(() => this.runHook('before'))
       .then(result => {
+        this.reporter.setTestStatus();
+
         if (result instanceof Error) {
           Logger.error(result);
         }
@@ -730,6 +741,10 @@ class TestSuite {
         return err;
       })
       .then(possibleError => {
+        if (this.transport.driverService && this.reporter.testResults) {
+          this.reporter.testResults.setSeleniumLogFile(this.transport.driverService.getSeleniumOutputFilePath());
+        }
+        
         // if there was an error in the testcase and skip_testcases_on_fail, we must send it forward, but after we run afterEach and after hooks
         return this.runHook('afterEach')
           .then(() => this.testCaseFinished())
@@ -798,14 +813,20 @@ class TestSuite {
         this.api.saveScreenshot(fileNamePath, (result, err) => {
           if (!err && this.transport.isResultSuccess(result))  {
             const assertions = this.api.currentTest.results.assertions || [];
-
+            const commands = this.reporter.currentSection.commands || [];
+            
             if (assertions.length > 0) {
               const currentAssertion = assertions[assertions.length-1];
+              const currentCommand = commands[commands.length-1];
 
               if (currentAssertion) {
                 currentAssertion.screenshots = currentAssertion.screenshots || [];
                 currentAssertion.screenshots.push(fileNamePath);
               }
+
+              if (currentCommand) {
+                currentCommand.screenshot = fileNamePath;
+              }
             }
           } else {
             Logger.warn('Error saving screenshot...', err || result);
@@ -977,6 +998,16 @@ class TestSuite {
       // eslint-disable-next-line no-console
       console.log(Logger.colors.green(`Testsuite "${this.context.moduleName}" is disabled, skipping...`));
 
+      // send report even if test is skipped
+      if (Concurrency.isChildProcess() && typeof process.send == 'function') {
+        process.send(SafeJSON.stringify({
+          type: 'testsuite_finished',
+          itemKey: process.env.__NIGHTWATCH_ENV_LABEL,
+          results: this.reporter.exportResults(),
+          httpOutput: Logger.collectOutput()
+        }));
+      }
+
       return Promise.resolve();
     }
 
diff --git a/test/lib/command-mocks.js b/test/lib/command-mocks.js
index 236f64b7..2bb21d9b 100644
--- a/test/lib/command-mocks.js
+++ b/test/lib/command-mocks.js
@@ -385,7 +385,7 @@ module.exports = {
     url = '/wd/hub/session'
   }) {
     const browserName = 'chrome';
-    const headlessOpt = headless ? 'headless' : '';
+    const headlessOpt = headless ? 'headless=chrome' : '';
     const options = {
       ['goog:chromeOptions']: {}
     };
diff --git a/test/src/core/testCreateSession.js b/test/src/core/testCreateSession.js
index 951ba9f6..654e1097 100644
--- a/test/src/core/testCreateSession.js
+++ b/test/src/core/testCreateSession.js
@@ -198,7 +198,7 @@ describe('test Request With Credentials', function () {
       sessionId: '1352110219202',
       capabilities: {browserName: 'chrome', version: 'TEST', platform: 'TEST'}
     });
-    assert.deepStrictEqual(sessionOptions.get('goog:chromeOptions'), {args: ['headless']});
+    assert.deepStrictEqual(sessionOptions.get('goog:chromeOptions'), {args: ['headless=chrome']});
   });
 
   it('Test create session with headless mode in Edge', async function () {
@@ -287,7 +287,7 @@ describe('test Request With Credentials', function () {
     assert.deepStrictEqual(sessionOptions.get('goog:chromeOptions'), {
       args: [
         '--no-sandbox',
-        'headless'
+        'headless=chrome'
       ]
     });
   });
diff --git a/test/src/index/testMobileComponent.js b/test/src/index/testMobileComponent.js
index 8fa284fe..cfd1618c 100644
--- a/test/src/index/testMobileComponent.js
+++ b/test/src/index/testMobileComponent.js
@@ -33,4 +33,62 @@ describe('Mobile Component Testing in Android Emulator', function () {
 
     assert.strictEqual(client.api.baseUrl, 'http://10.0.2.2:3000');
   });
-});
\ No newline at end of file
+
+  it('test baseUrl - not defined',  function(){
+    const client = NightwatchClient.client({
+      webdriver: {
+        start_process: true
+      },
+      desiredCapabilities: {
+        avd: 'nightwatch-android-11',
+        real_mobile: false
+      }
+    });
+
+    assert.strictEqual(client.api.baseUrl, null);
+  });
+
+  it('test baseUrl - non local',  function(){
+    const client = NightwatchClient.client({
+      baseUrl: 'https://www.google.com/',
+      webdriver: {
+        start_process: true
+      },
+      desiredCapabilities: {
+        avd: 'nightwatch-android-11',
+        real_mobile: false
+      }
+    });
+
+    assert.strictEqual(client.api.baseUrl, 'https://www.google.com/');
+  });
+
+  it('test baseUrl - real mobile',  function(){
+    const client = NightwatchClient.client({
+      baseUrl: 'http://127.0.0.1:3000',
+      webdriver: {
+        start_process: true
+      },
+      desiredCapabilities: {
+        avd: 'nightwatch-android-11',
+        real_mobile: true
+      }
+    });
+
+    assert.strictEqual(client.api.baseUrl, 'http://127.0.0.1:3000');
+  });
+
+  it('test baseUrl - no mobile',  function(){
+    const client = NightwatchClient.client({
+      baseUrl: 'http://127.0.0.1:3000',
+      webdriver: {
+        start_process: true
+      },
+      desiredCapabilities: {
+        real_mobile: false
+      }
+    });
+
+    assert.strictEqual(client.api.baseUrl, 'http://127.0.0.1:3000');
+  });
+});
diff --git a/test/src/index/transport/testChromeOptions.js b/test/src/index/transport/testChromeOptions.js
index afe90c23..b1a82695 100644
--- a/test/src/index/transport/testChromeOptions.js
+++ b/test/src/index/transport/testChromeOptions.js
@@ -108,7 +108,7 @@ describe('Test chrome options', function () {
     const options = client.transport.createSessionOptions({headless: true});
 
     assert.strictEqual(options instanceof ChromeOptions, true);
-    assert.deepStrictEqual(options.options_.args, ['headless']);
+    assert.deepStrictEqual(options.options_.args, ['headless=chrome']);
   });
 
   it('devtools cli arg', function(){
diff --git a/test/src/runner/testReporter.js b/test/src/runner/testReporter.js
index 9afa89ae..f5d35a2a 100644
--- a/test/src/runner/testReporter.js
+++ b/test/src/runner/testReporter.js
@@ -263,4 +263,64 @@ describe('testReporter', function() {
     assert.strictEqual(possibleError, null);
 
   });
+
+  it('Check reporter output for commands', async function () {
+    let possibleError = null;
+    const testsPath = [path.join(__dirname, '../../sampletests/simple/test/sample.js')];
+
+    try {
+      await runTests({
+        source: testsPath
+      },
+      settings({
+        output_folder: 'output',
+        globals: {
+          waitForConditionPollInterval: 20,
+          waitForConditionTimeout: 50,
+          retryAssertionTimeout: 50,
+          reporter: function (results) {
+            const module = results.modules['sample'];
+
+            assert.ok(Object.keys(module).includes('completedSections'));
+
+            const completedSections = module['completedSections'];
+
+            // check for module properties
+            assert.ok(Object.keys(completedSections).includes('__after_hook'));
+            assert.ok(Object.keys(completedSections).includes('__before_hook'));
+            assert.ok(Object.keys(completedSections).includes('__global_afterEach_hook'));
+            assert.ok(Object.keys(completedSections).includes('__global_beforeEach_hook'));
+            assert.ok(Object.keys(completedSections).includes('demoTest'));
+            
+            assert.strictEqual(completedSections['__after_hook']['commands'].length, 1);
+            assert.strictEqual(completedSections['__after_hook']['commands'][0].name, 'end');
+
+            // check for individual test properties
+            const test = completedSections['demoTest'];
+            assert.ok(Object.keys(test).includes('status'));
+            assert.ok(Object.keys(test).includes('commands'));
+            assert.strictEqual(test.commands.length, 3);
+            assert.strictEqual(test.commands[0].name, 'assert.equal');
+            assert.strictEqual(test.commands[1].name, 'url');
+            assert.strictEqual(test.commands[2].name, 'assert.elementPresent');
+
+            const command = test.commands[1];
+            assert.ok(Object.keys(command).includes('startTime'));
+            assert.ok(Object.keys(command).includes('endTime'));
+            assert.ok(Object.keys(command).includes('elapsedTime'));
+            assert.ok(Object.keys(command).includes('result'));
+            assert.deepEqual(command.args, ['http://localhost']);
+            assert.strictEqual(command.status, 'pass');
+          }
+        },
+        silent: true,
+        output: false
+      }));
+
+    } catch (error) {
+      possibleError = error;
+    }
+    assert.strictEqual(possibleError, null);
+
+  });
 });
diff --git a/test/src/runner/testRunWithGlobalHooks.js b/test/src/runner/testRunWithGlobalHooks.js
index 2e1b7715..57b0bbac 100644
--- a/test/src/runner/testRunWithGlobalHooks.js
+++ b/test/src/runner/testRunWithGlobalHooks.js
@@ -246,7 +246,7 @@ describe('testRunWithGlobalHooks', function() {
         assert.strictEqual(testTimestamp.getFullYear(), currentTimestamp.getFullYear());
         assert.strictEqual(testTimestamp.getMonth(), currentTimestamp.getMonth());
         assert.strictEqual(testTimestamp.getDate(), currentTimestamp.getDate());
-        assert.deepStrictEqual(client.currentTest.results, {errors: 0, failed: 0, passed: 0, assertions: [], tests: 0});
+        assert.deepStrictEqual(client.currentTest.results, {errors: 0, failed: 0, passed: 0, assertions: [], commands: [], tests: 0});
         assert.strictEqual(client.currentTest.module, 'sample');
         assert.strictEqual(client.currentTest.name, '');
         globals.calls++;