Repository: nightwatchjs/nightwatch
Pull request #3729 (2023-05-15 21:13:42)

Test o11y -> v2.6
Thanks in advance for your contribution. Please follow the below steps in submitting a pull request, as it will help us with reviewing it quicker.

- [ ] Create a new branch from master (e.g. `features/my-new-feature` or `issue/123-my-bugfix`);
- [ ] If you're fixing a bug also create an issue if one doesn't exist yet;
- [ ] If it's a new feature explain why do you think it's necessary. Please check with the maintainers beforehand to make sure it is something that we will accept. Usually we only accept new features if we feel that they will benefit the entire community;
- [ ] Please avoid sending PRs which contain drastic or low level changes. If you are certain that the changes are needed, please discuss them beforehand and indicate what the impact will be;
- [ ] If your change is based on existing functionality please consider refactoring first. Pull requests that duplicate code will most likely be ignored;
- [ ] Do not include changes that are not related to the issue at hand;
- [ ] Follow the same coding style with regards to spaces, semicolons, variable naming etc.;
- [ ] Always add unit tests - PRs without tests are most of the times ignored.



Patch:
diff --git a/lib/api/assertions/hasAttribute.js b/lib/api/assertions/hasAttribute.js
index 802c9e34..54b3ac13 100644
--- a/lib/api/assertions/hasAttribute.js
+++ b/lib/api/assertions/hasAttribute.js
@@ -19,7 +19,7 @@
  * @api assertions
  */
 
-const {setElementSelectorProps, containsMultiple} = require('../../utils');
+const {setElementSelectorProps, isString} = require('../../utils');
 
 exports.assertion = function(definition, expectedAttribute, msg) {
   this.options = {
@@ -32,35 +32,31 @@ exports.assertion = function(definition, expectedAttribute, msg) {
  
 
   this.formatMessage = function() {
+    if (!isString(expectedAttribute)) {
+      throw new Error('Expected attribute must be a string');
+    }
+
     let message = msg || `Testing if element %s ${this.negate ? 'doesn\'t have attribute %s' : 'has attribute %s'}`;
  
     return {
       message,
-      args: [this.elementSelector, `'${Array.isArray(expectedAttribute) ? expectedAttribute.join(' ') : expectedAttribute}'`]
+      args: [this.elementSelector, `'${expectedAttribute}'`]
     };
   };
 
   this.evaluate = function() {
-    if (!this.attributeList) {
-      return false;
-    }
- 
-    return containsMultiple(this.attributeList, expectedAttribute, ' ');
-  };
+    const {result} = this;
 
-  this.value = function(result) {
     if (!result || !result.value) {
-      return '';
+      return false;
     }
-
-    this.attributeList = result.value.map(attribute =>  attribute.name);
-
-    return this.attributeList;
+ 
+    return true;
   };
 
   this.command = function(callback) {
     this.api.getAttribute(setElementSelectorProps(definition, {
       suppressNotFoundErrors: true
-    }), 'attributes', callback);
+    }), expectedAttribute, callback);
   };
 };
diff --git a/lib/core/treenode.js b/lib/core/treenode.js
index f8192236..ce4f53c2 100644
--- a/lib/core/treenode.js
+++ b/lib/core/treenode.js
@@ -1,5 +1,6 @@
 const EventEmitter = require('events');
 const Utils = require('../utils');
+const Debuggability = require('../utils/debuggability.js');
 
 class TreeNode {
   get command() {
@@ -114,6 +115,11 @@ class TreeNode {
 
   handleError(err) {
     err.abortOnFailure = err.abortOnFailure || err.abortOnFailure === undefined;
+
+    // Set abortOnFailure to false when command is being executed in debugMode
+    if (Debuggability.debugMode) {
+      err.abortOnFailure = false;
+    }
     let errorName = err.name !== 'Error' ? `[${err.name}] ` : '';
     let originalError = `${errorName}${err.message}`;
 
diff --git a/lib/reporter/index.js b/lib/reporter/index.js
index de41920d..e2085f3b 100644
--- a/lib/reporter/index.js
+++ b/lib/reporter/index.js
@@ -127,10 +127,18 @@ class Reporter extends SimplifiedReporter {
     this.testResults.setElapsedTime();
   }
 
+  setTestSectionElapsedTime() {
+    this.testResults.setTestSectionElapsedTime();
+  }
+
   setTestStatus() {
     this.testResults.setTestStatus();
   }
 
+  collectTestSectionOutput() {
+    this.testResults.collectTestSectionOutput();
+  }
+
   testSuiteFinished() {
     this.testResults.setTotalElapsedTime();
   }
diff --git a/lib/reporter/reporters/minimalJson.js b/lib/reporter/reporters/minimalJson.js
new file mode 100644
index 00000000..a2da7091
--- /dev/null
+++ b/lib/reporter/reporters/minimalJson.js
@@ -0,0 +1,68 @@
+const lodashPick = require('lodash.pick');
+const path = require('path');
+const Utils = require('../../utils');
+const {Logger} = Utils;
+const BaseReporter = require('../base-reporter.js');
+
+class MinimalJsonReporter extends BaseReporter {
+
+  adaptModule(module) {
+    // Pick only the necessary fields
+    const result = lodashPick(module, [
+      'modulePath',
+      'status'
+    ]);
+
+    return result;
+  }
+
+  adaptResults() {
+    const {modules}  = this.results;
+    this.modules = {};
+
+    Object.keys(modules).forEach((moduleKey) => {
+      this.modules[moduleKey] = this.adaptModule(modules[moduleKey]);
+    });
+
+    return this.modules;
+  }
+
+  writeReport() {
+    const results = this.adaptResults();
+    const {output_folder} = this.options;
+    const filename = path.join(output_folder, 'minimal_report.json');
+    const shouldCreateFolder = !Utils.dirExistsSync(output_folder);
+
+    const report = {
+      modules: results
+    };
+
+    return this.writeReportFile(filename, JSON.stringify(report), shouldCreateFolder, output_folder)
+      .then(_ => {
+        Logger.info(Logger.colors.stack_trace(`Wrote Rerun Json report file to: ${path.resolve(filename)}`));
+
+        // Setting env varaible with minimalJsonReporter path.
+        // Next time user runs nightwatch with rerun functionality, json reporter can be read from this
+        process.env.NIGHTWATCH_RERUN_REPORT_FILE = filename;
+        process.env.NEW_FOO = filename;
+      });
+  }
+}
+
+function write(results, options, callback) {
+  const reporter = new MinimalJsonReporter(results, options);
+
+  reporter.writeReport()
+    .then(_ => {
+      callback();
+    })
+    .catch(err => {
+      Logger.error(err);
+      callback(err);
+    });
+}
+
+module.exports = {
+  MinimalJsonReporter,
+  write
+};
diff --git a/lib/reporter/results.js b/lib/reporter/results.js
index 15a9807b..055a3a28 100644
--- a/lib/reporter/results.js
+++ b/lib/reporter/results.js
@@ -49,6 +49,9 @@ module.exports = class Results {
     this.buildName = capabilities.buildName || desiredCapabilities.buildName || '';
     const {webdriver = {}} = settings;
     this.host = webdriver.host || '';
+    this.name = opts.suiteName || '';
+    this.tags = opts.tags || [];
+    this.__retryTest = false;
 
     this.initCount(tests);
   }
@@ -236,6 +239,8 @@ module.exports = class Results {
     suiteResults.results.status = this.getTestStatus();
     suiteResults.results.seleniumLog = this.seleniumLog;
     suiteResults.results.host = this.host;
+    suiteResults.results.name = this.name;
+    suiteResults.results.tags = this.tags;
 
     // Backwards compat
     suiteResults.results.tests = this.testsCount;
@@ -271,12 +276,20 @@ module.exports = class Results {
     return this.currentTestName;
   }
 
+  set retryTest(value) {
+    this.__retryTest = value;
+  }
+
+  get retryTest() {
+    return this.__retryTest;
+  }
+  
   /**
    * @param {TestCase} testcase
    * @return {Object}
    */
   createTestCaseResults(testcase) {
-    return {
+    const result = {
       time: 0,
       assertions: [],
       commands: [],
@@ -286,8 +299,26 @@ module.exports = class Results {
       retries: testcase.retriesCount,
       skipped: 0,
       tests: 0,
-      status: Results.TEST_PASS
+      status: Results.TEST_PASS,
+      startTimestamp: new Date().getTime(),
+      httpOutput: []
     };
+
+    if (this.retryTest && this.testSections[testcase.testName]) {
+      const retryTestData = this.testSections[testcase.testName];
+
+      result['retryTestData'] = [retryTestData];
+
+      if (retryTestData['retryTestData']) {
+        result['retryTestData'].push(...retryTestData['retryTestData']);
+
+        delete retryTestData['retryTestData'];
+      }
+
+      this.retryTest = false;
+    }
+
+    return result;
   }
 
   resetLastError() {
@@ -408,6 +439,26 @@ module.exports = class Results {
     return this;
   }
 
+  setTestSectionElapsedTime() {
+    const currentSection = this.getTestSection(this.currentSectionName);
+    const startTime = currentSection ? currentSection.startTimestamp : this.globalStartTime;
+    const endTime = new Date().getTime();
+    const elapsedTime = endTime - startTime;
+    this.endTimestamp = endTime;
+
+    this.time += elapsedTime;
+
+    if (currentSection) {
+      currentSection.time = (elapsedTime/1000).toPrecision(4);
+      currentSection.timeMs = elapsedTime;
+      currentSection.startTimestamp = startTime;
+      currentSection.endTimestamp = endTime;
+
+    }
+
+    return this;
+  }
+
   setTestStatus() {
     const currentTest = this.getCurrentTest();
     const currentSection = this.getTestSection(this.currentSectionName);
@@ -424,6 +475,15 @@ module.exports = class Results {
     }
   }
 
+  collectTestSectionOutput() {
+    const currentSection = this.getTestSection(this.currentSectionName);
+    if (!currentSection) {
+      return;
+    }
+
+    currentSection.httpOutput = Logger.collectTestSectionOutput();
+  }
+
   setTotalElapsedTime() {
     this.timeMs = this.time;
     this.time = (this.time/1000).toPrecision(4);
diff --git a/lib/runner/cli/argv-setup.js b/lib/runner/cli/argv-setup.js
index 5b0f44b1..dd1284e0 100644
--- a/lib/runner/cli/argv-setup.js
+++ b/lib/runner/cli/argv-setup.js
@@ -495,6 +495,11 @@ module.exports = new (function () {
         description: 'Set the udid of real iOS device'
       });
 
+      this.option('rerun-failed', {
+        description: 'Rerun failed test suites'
+
+      });
+
       return this;
     }
   }
diff --git a/lib/runner/process-listener.js b/lib/runner/process-listener.js
index 2bcf4166..07c0fcb6 100644
--- a/lib/runner/process-listener.js
+++ b/lib/runner/process-listener.js
@@ -80,6 +80,9 @@ module.exports = class {
   }
 
   uncaught(err, {type = 'uncaughtException'} = {}) {
+    Logger.setOutputEnabled(true); // force log for uncaught exception
+    Logger.enable();
+
     Logger.error(`${type}: ${err.message}\n${err.stack}`);
     analyticsCollector.exception(err);
 
diff --git a/lib/settings/defaults.js b/lib/settings/defaults.js
index a5de5171..19e9a465 100644
--- a/lib/settings/defaults.js
+++ b/lib/settings/defaults.js
@@ -146,7 +146,9 @@ module.exports = {
     folder_format: null,
 
     // The file name formatting to use while saving HTML report
-    filename_format: null
+    filename_format: null,
+
+    minimal_report_file_path: 'tests_output/minimal_report.json'
   },
 
   // A string or array of folders (excluding subfolders) where the tests are located.
@@ -322,7 +324,7 @@ module.exports = {
 
   unit_testing_mode: false,
 
-  default_reporter: ['junit', 'json', 'html'],
+  default_reporter: ['junit', 'json', 'minimalJson', 'html'],
 
   // In Nightwatch v1.x, when used with "await" operator, API commands will return the full result object as {value: `<VALUE>`}
   // whereas in v2, the value is return directly; if using a callback, the behaviour remains unchanged
diff --git a/lib/utils/debuggability.js b/lib/utils/debuggability.js
index 437f3455..909ee3c4 100644
--- a/lib/utils/debuggability.js
+++ b/lib/utils/debuggability.js
@@ -6,7 +6,15 @@ class Debuggability {
   static set stepOverAndPause(value) {
     this._stepOverAndPause = value;
   }
-  
+
+  static get debugMode() {
+    return this._debugMode;
+  }
+
+  static set debugMode(value) {
+    this._debugMode = value;
+  }
+
   static reset() {
     this._stepOverAndPause = false;
   }
diff --git a/lib/utils/logger/index.js b/lib/utils/logger/index.js
index 91d8546f..4ac84db2 100644
--- a/lib/utils/logger/index.js
+++ b/lib/utils/logger/index.js
@@ -152,6 +152,7 @@ function logRequest(message, params) {
   const instance = Logger.getInstance();
 
   instance.output.push([timeIso, lodashEscape(message), lodashEscape(inspectObject(params))]);
+  instance.testSectionOutput.push([timeIso, lodashEscape(message), lodashEscape(inspectObject(params))]);
 }
 
 function logError(severity, errOrMessage, args) {
@@ -167,6 +168,7 @@ class Logger {
   constructor() {
     this.colors = colors;
     this.output = [];
+    this.testSectionOutput = [];
   }
 
   logMessage(...args) {
@@ -447,3 +449,16 @@ module.exports.collectOutput = function() {
 
   return output;
 };
+
+module.exports.collectTestSectionOutput = function() {
+  const instance = Logger.getInstance();
+
+  if (!instance) {
+    return [];
+  }
+
+  const {testSectionOutput} = instance;
+  instance.testSectionOutput = [];
+
+  return testSectionOutput;
+};


Test patch:
diff --git a/lib/runner/test-source.js b/lib/runner/test-source.js
index 1cb96d25..b764cd48 100644
--- a/lib/runner/test-source.js
+++ b/lib/runner/test-source.js
@@ -2,7 +2,9 @@ const path = require('path');
 const fs = require('fs');
 const minimatch = require('minimatch');
 const Utils = require('../utils');
+const Concurrency = require('./concurrency');
 const {Logger, validExtensions, tsFileExt, jsFileExt, singleSourceFile} = Utils;
+const {MinimalJsonReporter} = require('../reporter/reporters/minimalJson');
 
 class TestSource {
   static get GroupNameDelimiter() {
@@ -46,11 +48,70 @@ class TestSource {
     return testsource;
   }
 
+  getRerunFailedFile(minimal_report_file_path) {
+    const jsonFile = path.resolve(process.env.NIGHTWATCH_RERUN_REPORT_FILE || minimal_report_file_path || '');
+
+    if (!Utils.fileExistsSync(jsonFile)) {
+      const err = new Error('Unable to find the Json reporter file to rerun failed tests');
+
+      err.showTrace = false;
+      err.detailedErr = 'Configure the environment variable NIGHTWATCH_RERUN_REPORT_FILE with Json reporter file path';
+      err.help = [
+        `Try setting ${Logger.colors.cyan('minimal_report_file_path: "JSON-REPORTER-PATH"')} in nightwatch configuration`,
+        `Or, try running: ${Logger.colors.cyan('export NIGHTWATCH_RERUN_REPORT_FILE="JSON-REPORTER-PATH"')}`
+      ];
+
+      throw err;
+    }
+
+    return jsonFile;
+  }
+  
+  getTestSourceForRerunFailed() {
+    const {reporter_options: {minimal_report_file_path}} = this.settings;
+    const minimalJsonFile = this.getRerunFailedFile(minimal_report_file_path);
+  
+    try {
+      const {modules = {}} = require(minimalJsonFile);
+      const testsource = [];
+    
+      Object.keys(modules).forEach(moduleKey => {
+        if (modules[moduleKey] && modules[moduleKey].status === 'fail') {
+          testsource.push(modules[moduleKey].modulePath);
+        }
+      });
+
+      if (testsource.length === 0) {
+        const err = new Error('Rerun Failed Tests: No failed tests found to rerun.');
+        err.noFailedTestFound = true;
+        err.showTrace = false;
+        err.detailedErr = 'Run nightwatch with --help to display usage info.';
+
+        throw err;
+      }
+
+      return testsource;
+    } catch (err) {
+      if (err.noFailedTestFound) {
+        err.message = 'Rerun Failed Tests: Invalid Json reporter.';
+
+        err.showTrace = false;
+        err.detailedErr = 'Please set env variable NIGHTWATCH_RERUN_REPORT_FILE with valid Json reporter path.';
+      }
+
+      throw err;
+    }
+  }
+
   /**
    * Returns the path where the tests are located
    * @returns {*}
    */
   getSource() {
+    if ((process.env.NIGHTWATCH_RERUN_FAILED === 'true' || this.argv['rerun-failed']) && Concurrency.isMasterProcess()) {
+      return this.getTestSourceForRerunFailed(this.argv);
+    }
+    
     if (this.argv['test-worker'] || singleSourceFile(this.argv)) {
       return this.getTestSourceForSingle(this.argv.test || this.argv._source);
     }
diff --git a/lib/testsuite/index.js b/lib/testsuite/index.js
index d604ba9e..d254546f 100644
--- a/lib/testsuite/index.js
+++ b/lib/testsuite/index.js
@@ -216,7 +216,8 @@ class TestSuite {
         reportPrefix: '',
         reportFileName: this.argv['report-filename'],
         groupName,
-        isMobile: this.client.api.isMobile()
+        isMobile: this.client.api.isMobile(),
+        tags: this.context.getTags()
       }
     });
   }
@@ -460,7 +461,11 @@ class TestSuite {
         await this.globalsInstance.runPluginHook(hookName, [this.settings]);
       }
 
-      return this.globalHooks[hookName].run(this.client);
+      const result = await this.globalHooks[hookName].run(this.client);
+      
+      this.onTestSectionFinished();
+
+      return result;
     });
   }
 
@@ -525,8 +530,8 @@ class TestSuite {
     return this.startTestSuite()
       .then(() => this.runHook('before'))
       .then(result => {
-        this.reporter.setTestStatus();
-
+        this.onTestSectionFinished();
+        
         if (result instanceof Error) {
           Logger.error(result);
         }
@@ -564,6 +569,7 @@ class TestSuite {
             if ((possibleError instanceof Error) && this.failFastMode) {
               throw possibleError;
             }
+            this.onTestSectionFinished();
 
             return result;
           });
@@ -575,6 +581,12 @@ class TestSuite {
       .then((errorOrFailures) => this.onTestSuiteFinished(errorOrFailures));
   }
 
+  onTestSectionFinished() {
+    this.reporter.setTestStatus();
+    this.reporter.setTestSectionElapsedTime();
+    this.reporter.collectTestSectionOutput();
+  }
+
   onTestSuiteFinished(errorOrFailures = false) {
     this.__snapShot = undefined;
 
@@ -786,7 +798,7 @@ class TestSuite {
 
   testCaseFinished() {
     this.reporter.setElapsedTime();
-    this.reporter.setTestStatus();
+    this.onTestSectionFinished();
 
     if (!this.testcase) {
       return Promise.resolve();
@@ -803,6 +815,7 @@ class TestSuite {
     let currentTestName = this.testcase.testName;
     this.suiteRetries.incrementTestRetriesCount(currentTestName);
     this.reporter.resetCurrentTestPassedCount();
+    this.reporter.testResults.retryTest = true;
     this.commandQueue.clearScheduled();
 
     return this.runCurrentTest(currentTestName);
diff --git a/test/extra/minimalJsonReporter.json b/test/extra/minimalJsonReporter.json
new file mode 100644
index 00000000..1ec64e55
--- /dev/null
+++ b/test/extra/minimalJsonReporter.json
@@ -0,0 +1,12 @@
+{
+    "modules": {
+        "demoTagTest": {
+            "modulePath": "./test/sampletests/tags/sample.js",
+            "status": "pass"
+        },
+        "demoTest": {
+            "modulePath": "./test/sampletests/simple/test/sample.js",
+            "status": "fail"
+        }
+    }
+}
diff --git a/test/mochatests/integration/sampleWithAsyncTestcase.js b/test/mochatests/integration/sampleWithAsyncTestcase.js
index 7e4b1084..661fd8dd 100644
--- a/test/mochatests/integration/sampleWithAsyncTestcase.js
+++ b/test/mochatests/integration/sampleWithAsyncTestcase.js
@@ -7,7 +7,7 @@ describe('Mocha tests with async testcase', function() {
   this.suiteRetries(2);
 
   assert.deepStrictEqual(this.settings.desiredCapabilities, {browserName: 'firefox'});
-  assert.deepStrictEqual(this.argv.reporter, ['junit', 'json', 'html']);
+  assert.deepStrictEqual(this.argv.reporter, ['junit', 'json', 'minimalJson', 'html']);
   assert.strictEqual(this.mochaOptions.timeout, 5000);
   assert.strictEqual(this.waitForTimeout(), 1100);
   assert.strictEqual(this.waitForRetryInterval(), 100);
diff --git a/test/src/api/assertions/testHasAttribute.js b/test/src/api/assertions/testHasAttribute.js
index 390aa7ed..8037314b 100644
--- a/test/src/api/assertions/testHasAttribute.js
+++ b/test/src/api/assertions/testHasAttribute.js
@@ -13,7 +13,7 @@ describe('assert.hasAttribute', function () {
     return assertionTest({
       args: ['.test_element', 'data-test', 'Test message'],
       commandResult: {
-        value: [{name: 'data-track'}, {name: 'data-test'}]
+        value: 'data-track'
       },
       assertMessage: true,
       assertion({instance, failure}) {
@@ -28,7 +28,7 @@ describe('assert.hasAttribute', function () {
     return assertionTest({
       args: ['.test_element', 'data-test'],
       commandResult: {
-        value: [{name: 'data-track'}]
+        value: 'data-track'
       },
       negate: true,
       assertion({instance, queueOpts, message}) {
@@ -36,8 +36,8 @@ describe('assert.hasAttribute', function () {
         assert.strictEqual(queueOpts.negate, true);
 
         assert.strictEqual(instance.hasFailure(), false);
-        assert.deepStrictEqual(instance.getValue(), ['data-track']);
-        assert.deepStrictEqual(instance.getActual(), ['data-track']);
+        assert.deepStrictEqual(instance.getValue(), 'data-track');
+        assert.deepStrictEqual(instance.getActual(), 'data-track');
         assert.strictEqual(instance.message, 'Testing if element <.test_element> doesn\'t have attribute \'data-test\'');
         assert.ok(message.startsWith('Testing if element <.test_element> doesn\'t have attribute \'data-test\''), message);
       }
@@ -48,15 +48,15 @@ describe('assert.hasAttribute', function () {
     return assertionTest({
       args: ['.test_element', 'data-test'],
       commandResult: {
-        value: [{name: 'data-test'}]
+        value: 'data-test'
       },
       negate: true,
       assertError: true,
       assertion({instance, queueOpts, err}) {
         assert.strictEqual(queueOpts.negate, true);
         assert.strictEqual(instance.hasFailure(), false);
-        assert.deepStrictEqual(instance.getValue(), ['data-test']);
-        assert.deepStrictEqual(instance.getActual(), ['data-test']);
+        assert.deepStrictEqual(instance.getValue(), 'data-test');
+        assert.deepStrictEqual(instance.getActual(), 'data-test');
         assert.strictEqual(err.message, `Testing if element <.test_element> doesn't have attribute 'data-test' in 5ms - expected "has not data-test" but got: "data-test" (${instance.elapsedTime}ms)`);
       }
     });
@@ -66,13 +66,13 @@ describe('assert.hasAttribute', function () {
     return assertionTest({
       args: [{selector: '.test_element'}, 'data-test'],
       commandResult: {
-        value: [{name: 'data-test'}]
+        value: 'data-test'
       },
       assertResult: true,
       assertion({instance, failure, message, err}) {
         assert.strictEqual(err, undefined);
         assert.deepStrictEqual(instance.options, {elementSelector: true});
-        assert.deepStrictEqual(instance.getActual(), ['data-test']);
+        assert.deepStrictEqual(instance.getActual(), 'data-test');
         assert.strictEqual(instance.hasFailure(), false);
         assert.ok(message.startsWith('Testing if element <.test_element> has attribute \'data-test\''), message);
         assert.strictEqual(failure, false);
@@ -84,33 +84,13 @@ describe('assert.hasAttribute', function () {
     return assertionTest({
       args: ['.test_element', 'data-test', 'Test message'],
       commandResult: {
-        value: [{name: 'not_expected'}]
+        value: null
       },
       assertError: true,
       assertResult: true,
       assertion({instance, failure}) {
-        assert.deepStrictEqual(instance.getActual(), ['not_expected']);
-        assert.strictEqual(failure, 'Expected "has data-test" but got: "not_expected"');
-      }
-    });
-  });
-
-  it('hasAttribute assertion - element not found', function() {
-    return assertionTest({
-      args: ['.test_element', 'data-test', 'Test attribute %s from element "%s" == %s'],
-      commandResult: {
-        status: -1,
-        value: []
-      },
-      assertError: true,
-      assertFailure: true,
-      assertResult: true,
-      assertion({instance, failure, err}) {
-        assert.strictEqual(instance.getActual(), 'element could not be located');
-        assert.strictEqual(instance.expected(), 'has data-test');
-        assert.strictEqual(instance.getValue(), null);
-        assert.strictEqual(failure, 'Expected "has data-test" but got: "element could not be located"');
-        assert.strictEqual(err.message, `Test attribute <.test_element> from element "'data-test'" == %s in 5ms - expected "has data-test" but got: "element could not be located" (${instance.elapsedTime}ms)`);
+        assert.deepStrictEqual(instance.getActual(), null);
+        assert.strictEqual(failure, 'Expected "has data-test" but got: "null"');
       }
     });
   });
@@ -132,4 +112,17 @@ describe('assert.hasAttribute', function () {
       }
     });
   });
+
+  it('hasAttribute assertion failed for wrong parameters', function () {
+    return assertionTest({
+      args: ['.test_element', ['data-test', 'dummy'], 'Test message'],
+      commandResult: {
+        value: 'data-track'
+      },
+      assertMessage: true
+    }).catch((err) => {
+      assert.ok(err instanceof Error);
+      assert.strictEqual(err.message, 'Error while running "assert.hasAttribute" command: Expected attribute must be a string');
+    });
+  });
 });
diff --git a/test/src/index/testNightwatchIndex.js b/test/src/index/testNightwatchIndex.js
index d3d0190d..bfb0129b 100644
--- a/test/src/index/testNightwatchIndex.js
+++ b/test/src/index/testNightwatchIndex.js
@@ -256,7 +256,7 @@ describe('test NightwatchIndex', function () {
       assert.deepStrictEqual(argv, {
         config: path.resolve('./test/extra/nightwatch.json'),
         verbose: true,
-        reporter: ['junit', 'json', 'html'],
+        reporter: ['junit', 'json', 'minimalJson', 'html'],
         source: 'test.js',
         _source: ['test.js']
       });
diff --git a/test/src/runner/testReporter.js b/test/src/runner/testReporter.js
index 4f5a15ea..b75ba5d4 100644
--- a/test/src/runner/testReporter.js
+++ b/test/src/runner/testReporter.js
@@ -252,6 +252,9 @@ describe('testReporter', function() {
             assert.ok(Object.keys(module).includes('startTimestamp'));
             assert.ok(Object.keys(module).includes('endTimestamp'));
             assert.ok(Object.keys(module).includes('host'));
+            assert.ok(Object.keys(module).includes('name'));
+            assert.ok(Object.keys(module).includes('tags'));
+
             // check for individual test properties
             const test = module.completed['demoTest'];
             assert.ok(Object.keys(test).includes('status'));
@@ -292,12 +295,17 @@ describe('testReporter', function() {
 
             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'));
+            // check module properties all for hooks
+            const hooks = ['__after_hook', '__before_hook', '__global_afterEach_hook', '__global_beforeEach_hook', 'demoTest'];
+
+            hooks.forEach(hook => {
+              assert.ok(Object.keys(completedSections).includes(hook));
+
+              const sectionData = completedSections[hook];
+              assert.ok(Object.keys(sectionData).includes('startTimestamp'));
+              assert.ok(Object.keys(sectionData).includes('endTimestamp'));
+              assert.ok(Object.keys(sectionData).includes('httpOutput'));
+            });
             
             assert.strictEqual(completedSections['__after_hook']['commands'].length, 1);
             assert.strictEqual(completedSections['__after_hook']['commands'][0].name, 'end');
@@ -394,4 +402,30 @@ describe('testReporter', function() {
       assert.deepStrictEqual(result, ['nightwatch_reporter_output', 'html_reporter_output']);
     });
   });
+
+  it('test to check retry data logging', function() {
+    this.timeout(100000);
+
+    const testsPath = path.join(__dirname, '../../sampletests/withfailures');
+    const globals = {
+      calls: 0,
+      reporter(results, cb) {
+        assert.ok('sample' in results.modules);
+        assert.ok('completedSections' in results.modules['sample']);
+        assert.ok('demoTest' in results.modules['sample']['completedSections']);
+        assert.ok('retryTestData' in results.modules['sample']['completedSections']['demoTest']);
+        assert.ok(results.modules['sample']['completedSections']['demoTest']['retryTestData'].length <= 3);
+        cb();
+      },
+      retryAssertionTimeout: 0
+    };
+
+    return runTests({
+      retries: 3,
+      _source: [testsPath]
+    }, settings({
+      skip_testcases_on_fail: false,
+      globals
+    }));
+  });
 });
diff --git a/test/src/runner/testRerunFunctionality.js b/test/src/runner/testRerunFunctionality.js
new file mode 100644
index 00000000..6237db57
--- /dev/null
+++ b/test/src/runner/testRerunFunctionality.js
@@ -0,0 +1,106 @@
+const path = require('path');
+const assert = require('assert');
+const common = require('../../common.js');
+const CommandGlobals = require('../../lib/globals/commands.js');
+const MockServer = require('../../lib/mockserver.js');
+const {settings} = common;
+const {runTests} = common.require('index.js');
+
+describe('testRerun', function () {
+  beforeEach(function (done) {
+    process.removeAllListeners('exit');
+    process.removeAllListeners('uncaughtException');
+    process.removeAllListeners('unhandledRejection');
+    
+    this.server = MockServer.init();
+    
+    this.server.on('listening', () => {
+      done();
+    });
+  });
+    
+  afterEach(function (done) {
+    CommandGlobals.afterEach.call(this, function () {
+      Object.keys(require.cache).forEach(function (module) {
+        delete require.cache[module];
+      });
+    
+      done();
+    });
+  });
+
+  it('Rerun with --rerun-failed cli argument without setting env variable for json reporter', function () {
+
+    return runTests({
+      'rerun-failed': true
+    }, settings({
+      output: false
+    })).catch(err => {
+      return err;
+    }).then(err => {
+      assert.ok(err instanceof Error);
+      assert.strictEqual(err.message, 'Unable to find the Json reporter file to rerun failed tests');
+      assert.strictEqual(err.detailedErr, 'Configure the environment variable NIGHTWATCH_RERUN_REPORT_FILE with Json reporter file path');
+    });
+  });
+
+  it('Rerun with env varaible without setting env variable for json reporter', function () {
+    process.env.NIGHTWATCH_RERUN_FAILED = 'true';
+
+    return runTests({
+      'rerun-failed': true
+    }, settings({
+      output: false
+    })).catch(err => {
+      return err;
+    }).then(err => {
+      assert.ok(err instanceof Error);
+      assert.strictEqual(err.message, 'Unable to find the Json reporter file to rerun failed tests');
+      assert.strictEqual(err.detailedErr, 'Configure the environment variable NIGHTWATCH_RERUN_REPORT_FILE with Json reporter file path');
+
+      delete process.env.NIGHTWATCH_RERUN_FAILED;
+    });
+  });
+
+  it('Rerun with env varaible set and with setting env variable for json reporter', function () {
+    process.env.NIGHTWATCH_RERUN_FAILED = 'true';
+    process.env.NIGHTWATCH_RERUN_REPORT_FILE = path.join(__dirname, '../../extra/minimalJsonReporter.json');
+
+    return runTests({
+    }, settings({
+      output: false,
+      globals: {
+        reporter(results) {
+          assert.strictEqual(Object.keys(results.modules).length, 1);
+          assert.strictEqual(results.assertions, 2);
+        }
+      }
+    })).then(err => {
+      assert.strictEqual(err, false);
+
+      delete process.env.NIGHTWATCH_RERUN_FAILED;
+      delete process.env.NIGHTWATCH_RERUN_REPORT_FILE;
+    });
+  });
+
+  it('Rerun with cli flag and with setting env variable for json reporter', function () {
+    process.env.NIGHTWATCH_RERUN_REPORT_FILE = path.join(__dirname, '../../extra/minimalJsonReporter.json');
+
+    return runTests({
+      'rerun-failed': true
+    }, settings({
+      output: false,
+      globals: {
+        reporter(results) {
+          assert.strictEqual(Object.keys(results.modules).length, 1);
+          assert.strictEqual(results.assertions, 2);
+        }
+      }
+    })).then(err => {
+      assert.strictEqual(err, false);
+      delete process.env.NIGHTWATCH_RERUN_REPORT_FILE;
+    });
+  });
+
+
+});
diff --git a/test/src/runner/testRunnerRerunJsonOutput.js b/test/src/runner/testRunnerRerunJsonOutput.js
new file mode 100644
index 00000000..88361e99
--- /dev/null
+++ b/test/src/runner/testRunnerRerunJsonOutput.js
@@ -0,0 +1,87 @@
+const path = require('path');
+const fs = require('fs');
+const assert = require('assert');
+const common = require('../../common.js');
+const MockServer = require('../../lib/mockserver.js');
+const CommandGlobals = require('../../lib/globals/commands.js');
+const rimraf = require('rimraf');
+const {settings} = common;
+const {runTests} = common.require('index.js');
+const {mkpath} = common.require('utils');
+
+describe('testRunnerRerunJsonOutput', function() {
+  before(function(done) {
+    this.server = MockServer.init();
+    this.server.on('listening', () => done());
+  });
+
+  beforeEach(function(done) {
+    mkpath('output', function(err) {
+      if (err) {
+        return done(err);
+      }
+      done();
+    });
+  });
+
+  afterEach(function(done) {
+    rimraf('output', done);
+  });
+
+  after(function(done) {
+    CommandGlobals.afterEach.call(this, done);
+  });
+
+  it('testRunWithRerunJsonOutput', function() {
+    const testsPath = [
+      path.join(__dirname, '../../sampletests/withsubfolders')
+    ];
+
+    return runTests({source: testsPath, reporter: 'minimalJson'}, settings({
+      output_folder: 'output',
+      silent: true,
+      globals: {reporter: function() {}}
+    }))
+      .then(_ => {
+        return readDirPromise(testsPath[0]);
+      })
+      .then(list => {
+        const rerunJsonReportFile = path.resolve('output/minimal_report.json');
+        assert.ok(fileExistsSync(rerunJsonReportFile), 'The simple report file was not created.');
+
+        return require(rerunJsonReportFile);
+      })
+      .then(data => {
+        assert.strictEqual(typeof data.modules, 'object');
+        Object.keys(data.modules).forEach((moduleKey) => {
+          const module = data.modules[moduleKey];
+      
+          assert.ok(Object.keys(module).includes('modulePath'));
+          assert.ok(Object.keys(module).includes('status'));
+        });
+      });
+  });
+});
+
+function readDirPromise(dirName) {
+  return new Promise(function(resolve, reject) {
+    fs.readdir(dirName, function(err, result) {
+      if (err) {
+        return reject(err);
+      }
+
+      resolve(result);
+    });
+  });
+}
+
+// util to replace deprecated fs.existsSync
+function fileExistsSync(path) {
+  try {
+    fs.statSync(path);
+
+    return true;
+  } catch (e) {
+    return false;
+  }
+}