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() {