diff --git a/doc/check-options.md b/doc/check-options.md
index d6204d859d..6487f0092e 100644
--- a/doc/check-options.md
+++ b/doc/check-options.md
@@ -25,6 +25,7 @@
- [css-orientation-lock](#css-orientation-lock)
- [meta-viewport-large](#meta-viewport-large)
- [meta-viewport](#meta-viewport)
+ - [meta-refresh](#meta-refresh)
- [header-present](#header-present)
- [landmark](#landmark)
- [p-as-heading](#p-as-heading)
@@ -385,6 +386,13 @@ th
| -------------- | :------ | :------------------------------------------------------------------------------------------- |
| `scaleMinimum` | `2` | The `scale-maximum` CSS value the check applies to. Values above this number will be ignored |
+### meta-refresh
+
+| Option | Default | Description |
+| ---------- | :------ | :---------------------------------------------------------------------------------- |
+| `minDelay` | `0` | Passes if the redirect is equal or less than this. Can be set to `false` to disable |
+| `maxDelay` | `7200` | Passes if the redirect is greater than this. Can be set to `false` to disable |
+
### header-present
diff --git a/lib/checks/navigation/meta-refresh-evaluate.js b/lib/checks/navigation/meta-refresh-evaluate.js
index cfa57e2faa..65777a6a4c 100644
--- a/lib/checks/navigation/meta-refresh-evaluate.js
+++ b/lib/checks/navigation/meta-refresh-evaluate.js
@@ -1,8 +1,21 @@
-function metaRefreshEvaluate(node, options, virtualNode) {
- var content = virtualNode.attr('content') || '',
- parsedParams = content.split(/[;,]/);
+const separatorRegex = /[;,\s]/;
+const validRedirectNumRegex = /^[0-9.]+$/;
- return content === '' || parsedParams[0] === '0';
-}
+export default function metaRefreshEvaluate(node, options, virtualNode) {
+ const { minDelay, maxDelay } = options || {};
+ const content = (virtualNode.attr('content') || '').trim();
+ const [redirectStr] = content.split(separatorRegex);
+ if (!redirectStr.match(validRedirectNumRegex)) {
+ return true;
+ }
-export default metaRefreshEvaluate;
+ const redirectDelay = parseFloat(redirectStr);
+ this.data({ redirectDelay });
+ if (typeof minDelay === 'number' && redirectDelay <= options.minDelay) {
+ return true;
+ }
+ if (typeof maxDelay === 'number' && redirectDelay > options.maxDelay) {
+ return true;
+ }
+ return false;
+}
diff --git a/lib/checks/navigation/meta-refresh.json b/lib/checks/navigation/meta-refresh.json
index 59fc48c992..7c85c6d5d0 100644
--- a/lib/checks/navigation/meta-refresh.json
+++ b/lib/checks/navigation/meta-refresh.json
@@ -1,6 +1,10 @@
{
"id": "meta-refresh",
"evaluate": "meta-refresh-evaluate",
+ "options": {
+ "minDelay": 0,
+ "maxDelay": 72000
+ },
"metadata": {
"impact": "critical",
"messages": {
diff --git a/lib/rules/meta-refresh.json b/lib/rules/meta-refresh.json
index f6a60d5029..32fa2cc129 100644
--- a/lib/rules/meta-refresh.json
+++ b/lib/rules/meta-refresh.json
@@ -1,6 +1,6 @@
{
"id": "meta-refresh",
- "selector": "meta[http-equiv=\"refresh\"]",
+ "selector": "meta[http-equiv=\"refresh\"][content]",
"excludeHidden": false,
"tags": [
"cat.time-and-media",
diff --git a/test/checks/navigation/meta-refresh.js b/test/checks/navigation/meta-refresh.js
index d328c6dbd2..d623a3fc2b 100644
--- a/test/checks/navigation/meta-refresh.js
+++ b/test/checks/navigation/meta-refresh.js
@@ -3,68 +3,187 @@ describe('meta-refresh', function() {
var checkContext = axe.testUtils.MockCheckContext();
var checkSetup = axe.testUtils.checkSetup;
+ var metaRefreshCheck = axe.testUtils.getCheckEvaluate('meta-refresh');
afterEach(function() {
checkContext.reset();
});
- describe('; separator', function() {
- it('should return false if content value is not 0', function() {
+ it('returns false if there is a number', function() {
+ var checkArgs = checkSetup('');
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+
+ describe('returns false when valid', function() {
+ it('there is a decimal', function() {
var checkArgs = checkSetup(
- ''
+ ''
);
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
- assert.isFalse(
- axe.testUtils
- .getCheckEvaluate('meta-refresh')
- .apply(checkContext, checkArgs)
+ it('there is a number followed by a dot', function() {
+ var checkArgs = checkSetup(
+ ''
);
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
});
- it('should return false if content value does not start with 0', function() {
+ it('there is a dot followed by a number', function() {
var checkArgs = checkSetup(
- ''
+ ''
);
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
- assert.isFalse(
- axe.testUtils
- .getCheckEvaluate('meta-refresh')
- .apply(checkContext, checkArgs)
+ it('there is whitespace before the number', function() {
+ var checkArgs = checkSetup(
+ ''
);
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+
+ describe('with a valid separator', function() {
+ it('the number is followed by a semicolon', function() {
+ var checkArgs = checkSetup(
+ ''
+ );
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+
+ it('the number is followed by a comma', function() {
+ var checkArgs = checkSetup(
+ ''
+ );
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+
+ it('the number is followed spaces, and then a separator', function() {
+ var checkArgs = checkSetup(
+ ''
+ );
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+
+ it('the separator is followed by non-separator characters', function() {
+ var checkArgs = checkSetup(
+ ''
+ );
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+
+ it('the separator is a space', function() {
+ var checkArgs = checkSetup(
+ ''
+ );
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
});
+ });
- it('should return true if content value starts with 0', function() {
+ describe('returns true when invalid', function() {
+ it('the number is prefaced with a plus', function() {
var checkArgs = checkSetup(
- ''
+ ''
);
+ assert.isTrue(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
- assert.isTrue(
- axe.testUtils
- .getCheckEvaluate('meta-refresh')
- .apply(checkContext, checkArgs)
+ it('the number is prefaced with a minus', function() {
+ var checkArgs = checkSetup(
+ ''
);
+ assert.isTrue(metaRefreshCheck.apply(checkContext, checkArgs));
});
- it('should return true if content value is 0', function() {
+ it('the number is prefaced with a letter', function() {
var checkArgs = checkSetup(
- ''
+ ''
);
+ assert.isTrue(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
- assert.isTrue(
- axe.testUtils
- .getCheckEvaluate('meta-refresh')
- .apply(checkContext, checkArgs)
+ it('the number is followed by an invalid separator character', function() {
+ var checkArgs = checkSetup(
+ ''
);
+ assert.isTrue(metaRefreshCheck.apply(checkContext, checkArgs));
});
+ });
- it('should return true if there is no content value', function() {
- var checkArgs = checkSetup('');
+ describe('options.minDelay', function() {
+ it('returns false when the redirect number is greater than minDelay', function() {
+ var options = { minDelay: 2 };
+ var checkArgs = checkSetup(
+ '',
+ options
+ );
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
- assert.isTrue(
- axe.testUtils
- .getCheckEvaluate('meta-refresh')
- .apply(checkContext, checkArgs)
+ it('returns true when the redirect number equals minDelay', function() {
+ var options = { minDelay: 3 };
+ var checkArgs = checkSetup(
+ '',
+ options
+ );
+ assert.isTrue(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+
+ it('returns true when the redirect number is less than minDelay', function() {
+ var options = { minDelay: 4 };
+ var checkArgs = checkSetup(
+ '',
+ options
+ );
+ assert.isTrue(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+
+ it('ignores minDelay when set to false', function() {
+ var options = { minDelay: false };
+ var checkArgs = checkSetup(
+ '',
+ options
+ );
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+ });
+
+ describe('options.maxDelay', function() {
+ it('returns true when the redirect number is greater than maxDelay', function() {
+ var options = { maxDelay: 2 };
+ var checkArgs = checkSetup(
+ '',
+ options
+ );
+ assert.isTrue(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+
+ it('returns false when the redirect number equals maxDelay', function() {
+ var options = { maxDelay: 3 };
+ var checkArgs = checkSetup(
+ '',
+ options
+ );
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+
+ it('returns false when the redirect number is less than maxDelay', function() {
+ var options = { maxDelay: 4 };
+ var checkArgs = checkSetup(
+ '',
+ options
+ );
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
+ });
+
+ it('ignores maxDelay when set to false', function() {
+ var options = { maxDelay: false };
+ var checkArgs = checkSetup(
+ '',
+ options
);
+ assert.isFalse(metaRefreshCheck.apply(checkContext, checkArgs));
});
});
});
diff --git a/test/integration/full/meta-refresh/meta-refresh-fail.js b/test/integration/full/meta-refresh/meta-refresh-fail.js
new file mode 100644
index 0000000000..3c4fe0cb8f
--- /dev/null
+++ b/test/integration/full/meta-refresh/meta-refresh-fail.js
@@ -0,0 +1,18 @@
+describe('meta-refresh fail', function() {
+ 'use strict';
+
+ it('should be a violation', function(done) {
+ axe.run({ runOnly: 'meta-refresh' }, function(err, results) {
+ try {
+ assert.isNull(err);
+ assert.lengthOf(results.violations, 1, 'violations');
+ assert.lengthOf(results.passes, 0, 'passes');
+ assert.lengthOf(results.incomplete, 0, 'passes');
+ assert.lengthOf(results.inapplicable, 0, 'inapplicable');
+ done();
+ } catch (e) {
+ done(e);
+ }
+ });
+ });
+});
diff --git a/test/integration/full/meta-refresh/meta-refresh-fail1.html b/test/integration/full/meta-refresh/meta-refresh-fail1.html
new file mode 100644
index 0000000000..a2d554c318
--- /dev/null
+++ b/test/integration/full/meta-refresh/meta-refresh-fail1.html
@@ -0,0 +1,28 @@
+
+
+
+
+ Meta-refresh fail 1
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/integration/full/meta-refresh/meta-refresh-inapplicable.js b/test/integration/full/meta-refresh/meta-refresh-inapplicable.js
new file mode 100644
index 0000000000..3fa5db444d
--- /dev/null
+++ b/test/integration/full/meta-refresh/meta-refresh-inapplicable.js
@@ -0,0 +1,18 @@
+describe('meta-refresh inapplicable', function() {
+ 'use strict';
+
+ it('should be inapplicable', function(done) {
+ axe.run({ runOnly: 'meta-refresh' }, function(err, results) {
+ try {
+ assert.isNull(err);
+ assert.lengthOf(results.violations, 0, 'violations');
+ assert.lengthOf(results.passes, 0, 'passes');
+ assert.lengthOf(results.incomplete, 0, 'passes');
+ assert.lengthOf(results.inapplicable, 1, 'inapplicable');
+ done();
+ } catch (e) {
+ done(e);
+ }
+ });
+ });
+});
diff --git a/test/integration/full/meta-refresh/meta-refresh-inapplicable1.html b/test/integration/full/meta-refresh/meta-refresh-inapplicable1.html
new file mode 100644
index 0000000000..80ca37c0bb
--- /dev/null
+++ b/test/integration/full/meta-refresh/meta-refresh-inapplicable1.html
@@ -0,0 +1,29 @@
+
+
+
+
+ Meta-refresh inapplicable 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/integration/full/meta-refresh/meta-refresh-inapplicable2.html b/test/integration/full/meta-refresh/meta-refresh-inapplicable2.html
new file mode 100644
index 0000000000..5f0d7a975f
--- /dev/null
+++ b/test/integration/full/meta-refresh/meta-refresh-inapplicable2.html
@@ -0,0 +1,29 @@
+
+
+
+
+ Meta-refresh inapplicable 2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/integration/full/meta-refresh/meta-refresh-pass.js b/test/integration/full/meta-refresh/meta-refresh-pass.js
new file mode 100644
index 0000000000..bbcd53197c
--- /dev/null
+++ b/test/integration/full/meta-refresh/meta-refresh-pass.js
@@ -0,0 +1,17 @@
+describe('meta-refresh pass', function() {
+ 'use strict';
+
+ it('should pass', function(done) {
+ axe.run({ runOnly: 'meta-refresh' }, function(err, results) {
+ try {
+ assert.isNull(err);
+ assert.lengthOf(results.violations, 0, 'violations');
+ assert.lengthOf(results.passes, 1, 'passes');
+ assert.lengthOf(results.incomplete, 0, 'passes');
+ done();
+ } catch (e) {
+ done(e);
+ }
+ });
+ });
+});
diff --git a/test/integration/full/meta-refresh/meta-refresh-pass1.html b/test/integration/full/meta-refresh/meta-refresh-pass1.html
new file mode 100644
index 0000000000..906df8e99e
--- /dev/null
+++ b/test/integration/full/meta-refresh/meta-refresh-pass1.html
@@ -0,0 +1,29 @@
+
+
+
+
+ Meta-refresh pass 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/integration/full/meta-refresh/meta-refresh-pass2.html b/test/integration/full/meta-refresh/meta-refresh-pass2.html
new file mode 100644
index 0000000000..f36b437d97
--- /dev/null
+++ b/test/integration/full/meta-refresh/meta-refresh-pass2.html
@@ -0,0 +1,29 @@
+
+
+
+
+ Meta-refresh pass 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/integration/virtual-rules/meta-refresh.js b/test/integration/virtual-rules/meta-refresh.js
index 9d316f3d4a..baac3085e9 100644
--- a/test/integration/virtual-rules/meta-refresh.js
+++ b/test/integration/virtual-rules/meta-refresh.js
@@ -1,5 +1,5 @@
describe('meta-refresh virtual-rule', function() {
- it('should pass missing content', function() {
+ it('should be inapplicable for missing content', function() {
var results = axe.runVirtualRule('meta-refresh', {
nodeName: 'meta',
attributes: {
@@ -7,9 +7,10 @@ describe('meta-refresh virtual-rule', function() {
}
});
- assert.lengthOf(results.passes, 1);
+ assert.lengthOf(results.passes, 0);
assert.lengthOf(results.violations, 0);
assert.lengthOf(results.incomplete, 0);
+ assert.lengthOf(results.inapplicable, 1);
});
it('should pass for content=0', function() {