From bb72acde17e9f783ac136c30a79f14144326a7df Mon Sep 17 00:00:00 2001
From: Steven Lambert <2433219+straker@users.noreply.github.com>
Date: Mon, 18 May 2020 08:29:54 -0600
Subject: [PATCH] feat(rule): add reviewOnFail option to have rule return as
 needs review instead of violation (#2235)

---
 lib/core/base/rule.js  |  48 +++++++++++++++++-
 test/core/base/rule.js | 107 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 154 insertions(+), 1 deletion(-)

diff --git a/lib/core/base/rule.js b/lib/core/base/rule.js
index 8783f6ad21..d794ee1366 100644
--- a/lib/core/base/rule.js
+++ b/lib/core/base/rule.js
@@ -46,6 +46,13 @@ function Rule(spec, parentAudit) {
 	 */
 	this.pageLevel = typeof spec.pageLevel === 'boolean' ? spec.pageLevel : false;
 
+	/**
+	 * Flag to force the rule to return as needs review rather than a violation if any of the checks fail.
+	 * @type {Boolean}
+	 */
+	this.reviewOnFail =
+		typeof spec.reviewOnFail === 'boolean' ? spec.reviewOnFail : false;
+
 	/**
 	 * Checks that any may return true to satisfy rule
 	 * @type {Array}
@@ -226,11 +233,28 @@ Rule.prototype.run = function(context, options = {}, resolve, reject) {
 			});
 
 			checkQueue
-				.then(function(results) {
+				.then(results => {
 					const result = getResult(results);
 					if (result) {
 						result.node = new DqElement(node.actualNode, options);
 						ruleResult.nodes.push(result);
+
+						// mark rule as incomplete rather than failure for rules with reviewOnFail
+						if (this.reviewOnFail) {
+							['any', 'all'].forEach(type => {
+								result[type].forEach(checkResult => {
+									if (checkResult.result === false) {
+										checkResult.result = undefined;
+									}
+								});
+							});
+
+							result.none.forEach(checkResult => {
+								if (checkResult.result === true) {
+									checkResult.result = undefined;
+								}
+							});
+						}
 					}
 					resolveNode();
 				})
@@ -285,6 +309,23 @@ Rule.prototype.runSync = function(context, options = {}) {
 				? new DqElement(node.actualNode, options)
 				: null;
 			ruleResult.nodes.push(result);
+
+			// mark rule as incomplete rather than failure for rules with reviewOnFail
+			if (this.reviewOnFail) {
+				['any', 'all'].forEach(type => {
+					result[type].forEach(checkResult => {
+						if (checkResult.result === false) {
+							checkResult.result = undefined;
+						}
+					});
+				});
+
+				result.none.forEach(checkResult => {
+					if (checkResult.result === true) {
+						checkResult.result = undefined;
+					}
+				});
+			}
 		}
 	});
 
@@ -519,6 +560,11 @@ Rule.prototype.configure = function(spec) {
 			typeof spec.pageLevel === 'boolean' ? spec.pageLevel : false;
 	}
 
+	if (spec.hasOwnProperty('reviewOnFail')) {
+		this.reviewOnFail =
+			typeof spec.reviewOnFail === 'boolean' ? spec.reviewOnFail : false;
+	}
+
 	if (spec.hasOwnProperty('any')) {
 		this.any = spec.any;
 	}
diff --git a/test/core/base/rule.js b/test/core/base/rule.js
index bd615617e6..282255503f 100644
--- a/test/core/base/rule.js
+++ b/test/core/base/rule.js
@@ -677,6 +677,47 @@ describe('Rule', function() {
 				);
 			});
 
+			it('should mark checks as incomplete if reviewOnFail is set to true', function(done) {
+				var rule = new Rule(
+					{
+						reviewOnFail: true,
+						all: ['cats'],
+						any: ['cats'],
+						none: ['dogs']
+					},
+					{
+						checks: {
+							cats: new Check({
+								id: 'cats',
+								evaluate: function() {
+									return false;
+								}
+							}),
+							dogs: new Check({
+								id: 'dogs',
+								evaluate: function() {
+									return true;
+								}
+							})
+						}
+					}
+				);
+
+				rule.run(
+					{
+						include: [axe.utils.getFlattenedTree(fixture)[0]]
+					},
+					{},
+					function(results) {
+						assert.isUndefined(results.nodes[0].all[0].result);
+						assert.isUndefined(results.nodes[0].any[0].result);
+						assert.isUndefined(results.nodes[0].none[0].result);
+						done();
+					},
+					isNotCalled
+				);
+			});
+
 			describe('NODE rule', function() {
 				it('should create a RuleResult', function() {
 					var orig = window.RuleResult;
@@ -1305,6 +1346,44 @@ describe('Rule', function() {
 				}
 			});
 
+			it('should mark checks as incomplete if reviewOnFail is set to true', function() {
+				var rule = new Rule(
+					{
+						reviewOnFail: true,
+						all: ['cats'],
+						any: ['cats'],
+						none: ['dogs']
+					},
+					{
+						checks: {
+							cats: new Check({
+								id: 'cats',
+								evaluate: function() {
+									return false;
+								}
+							}),
+							dogs: new Check({
+								id: 'dogs',
+								evaluate: function() {
+									return true;
+								}
+							})
+						}
+					}
+				);
+
+				var results = rule.runSync(
+					{
+						include: [axe.utils.getFlattenedTree(fixture)[0]]
+					},
+					{}
+				);
+
+				assert.isUndefined(results.nodes[0].all[0].result);
+				assert.isUndefined(results.nodes[0].any[0].result);
+				assert.isUndefined(results.nodes[0].none[0].result);
+			});
+
 			describe.skip('NODE rule', function() {
 				it('should create a RuleResult', function() {
 					var orig = window.RuleResult;
@@ -1615,6 +1694,27 @@ describe('Rule', function() {
 			});
 		});
 
+		describe('.reviewOnFail', function() {
+			it('should be set', function() {
+				var spec = {
+					reviewOnFail: true
+				};
+				assert.equal(new Rule(spec).reviewOnFail, spec.reviewOnFail);
+			});
+
+			it('should default to false', function() {
+				var spec = {};
+				assert.isFalse(new Rule(spec).reviewOnFail);
+			});
+
+			it('should default to false if given a bad value', function() {
+				var spec = {
+					reviewOnFail: 'monkeys'
+				};
+				assert.isFalse(new Rule(spec).reviewOnFail);
+			});
+		});
+
 		describe('.id', function() {
 			it('should be set', function() {
 				var spec = {
@@ -1775,6 +1875,13 @@ describe('Rule', function() {
 			rule.configure({ pageLevel: true });
 			assert.equal(rule._get('pageLevel'), true);
 		});
+		it('should override reviewOnFail', function() {
+			var rule = new Rule({ reviewOnFail: false });
+
+			assert.equal(rule._get('reviewOnFail'), false);
+			rule.configure({ reviewOnFail: true });
+			assert.equal(rule._get('reviewOnFail'), true);
+		});
 		it('should override any', function() {
 			var rule = new Rule({ any: ['one', 'two'] });