diff --git a/doc/rule-descriptions.md b/doc/rule-descriptions.md index 2645ea3612..65a042e4f5 100644 --- a/doc/rule-descriptions.md +++ b/doc/rule-descriptions.md @@ -111,7 +111,7 @@ Rules that do not necessarily conform to WCAG success criterion but are industry | [scope-attr-valid](https://dequeuniversity.com/rules/axe/4.4/scope-attr-valid?application=RuleDescription) | Ensures the scope attribute is used correctly on tables | Moderate, Critical | cat.tables, best-practice | failure | | | [skip-link](https://dequeuniversity.com/rules/axe/4.4/skip-link?application=RuleDescription) | Ensure all skip links have a focusable target | Moderate | cat.keyboard, best-practice | failure, needs review | | | [tabindex](https://dequeuniversity.com/rules/axe/4.4/tabindex?application=RuleDescription) | Ensures tabindex attribute values are not greater than 0 | Serious | cat.keyboard, best-practice | failure | | -| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.4/table-duplicate-name?application=RuleDescription) | Ensure the <caption> element does not contain the same text as the summary attribute | Minor | cat.tables, best-practice | failure | | +| [table-duplicate-name](https://dequeuniversity.com/rules/axe/4.4/table-duplicate-name?application=RuleDescription) | Ensure the <caption> element does not contain the same text as the summary attribute | Minor | cat.tables, best-practice | failure, needs review | | ## WCAG 2.0 and 2.1 level AAA rules diff --git a/lib/checks/tables/same-caption-summary-evaluate.js b/lib/checks/tables/same-caption-summary-evaluate.js index 649372925f..6aafd3f087 100644 --- a/lib/checks/tables/same-caption-summary-evaluate.js +++ b/lib/checks/tables/same-caption-summary-evaluate.js @@ -1,12 +1,23 @@ -import { accessibleText } from '../../commons/text'; - -function sameCaptionSummaryEvaluate(node) { - // passing node.caption to accessibleText instead of using - // the logic in accessibleTextVirtual on virtualNode - return ( - !!(node.summary && node.caption) && - node.summary.toLowerCase() === accessibleText(node.caption).toLowerCase() - ); -} +import { sanitize, subtreeText } from '../../commons/text'; export default sameCaptionSummaryEvaluate; + +function sameCaptionSummaryEvaluate(node, options, virtualNode) { + if (virtualNode.children === undefined) { + return undefined; + } + + var summary = virtualNode.attr('summary'); + var captionNode = virtualNode.children.find(isCaptionNode); + var caption = captionNode ? sanitize(subtreeText(captionNode)) : false; + + if (!caption || !summary) { + return false; + } + + return sanitize(summary).toLowerCase() === sanitize(caption).toLowerCase(); +} + +function isCaptionNode(virtualNode) { + return virtualNode.props.nodeName === 'caption'; +} diff --git a/lib/checks/tables/same-caption-summary.json b/lib/checks/tables/same-caption-summary.json index e7bd484fa1..04b8bee95f 100644 --- a/lib/checks/tables/same-caption-summary.json +++ b/lib/checks/tables/same-caption-summary.json @@ -5,7 +5,8 @@ "impact": "minor", "messages": { "pass": "Content of summary attribute and are not duplicated", - "fail": "Content of summary attribute and element are identical" + "fail": "Content of summary attribute and element are identical", + "incomplete": "Unable to determine if element has a caption" } } } diff --git a/locales/_template.json b/locales/_template.json index 3552bda7f5..454b4c1d40 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -993,7 +993,8 @@ }, "same-caption-summary": { "pass": "Content of summary attribute and
are not duplicated", - "fail": "Content of summary attribute and element are identical" + "fail": "Content of summary attribute and element are identical", + "incomplete": "Unable to determine if element has a caption" }, "scope-value": { "pass": "Scope attribute is used correctly", diff --git a/test/checks/tables/same-caption-summary.js b/test/checks/tables/same-caption-summary.js index 1340235ba7..495c4d3640 100644 --- a/test/checks/tables/same-caption-summary.js +++ b/test/checks/tables/same-caption-summary.js @@ -1,20 +1,18 @@ -describe('same-caption-summary', function() { +describe('same-caption-summary', function () { 'use strict'; - var fixture = document.getElementById('fixture'); var checkSetup = axe.testUtils.checkSetup; var shadowCheckSetup = axe.testUtils.shadowCheckSetup; var shadowSupport = axe.testUtils.shadowSupport; var checkContext = axe.testUtils.MockCheckContext(); - afterEach(function() { - fixture.innerHTML = ''; + afterEach(function () { checkContext.reset(); axe._tree = undefined; }); - it('should return false there is no caption', function() { + it('should return false there is no caption', function () { var params = checkSetup( '
' ); @@ -26,7 +24,7 @@ describe('same-caption-summary', function() { ); }); - it('should return false there is no summary', function() { + it('should return false there is no summary', function () { var params = checkSetup( '
Hi
' ); @@ -38,7 +36,7 @@ describe('same-caption-summary', function() { ); }); - it('should return false if summary and caption are different', function() { + it('should return false if summary and caption are different', function () { var params = checkSetup( '
Hi
' ); @@ -50,7 +48,7 @@ describe('same-caption-summary', function() { ); }); - it('should return true if summary and caption are the same', function() { + it('should return true if summary and caption are the same', function () { var params = checkSetup( '
Hi
' ); @@ -62,7 +60,7 @@ describe('same-caption-summary', function() { ); }); - it('should return true if summary and caption are the same with mixed casing', function() { + it('should return true if summary and caption are the same with mixed casing', function () { var params = checkSetup( '' + '' + @@ -84,7 +82,7 @@ describe('same-caption-summary', function() { (shadowSupport.v1 ? it : xit)( 'should match slotted caption elements', - function() { + function () { var params = shadowCheckSetup( '
' + 'Caption' + diff --git a/test/integration/virtual-rules/table-duplicate-name.js b/test/integration/virtual-rules/table-duplicate-name.js new file mode 100644 index 0000000000..7edf5eb04a --- /dev/null +++ b/test/integration/virtual-rules/table-duplicate-name.js @@ -0,0 +1,138 @@ +describe('table-duplicate-name virtual-rule', function () { + it('should incomplete on table element with children undefined', function () { + var tableNode = new axe.SerialVirtualNode({ + nodeName: 'table' + }); + tableNode.children = undefined; + + var results = axe.runVirtualRule('table-duplicate-name', tableNode); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); + }); + + it('should pass on table element', function () { + var tableNode = new axe.SerialVirtualNode({ + nodeName: 'table' + }); + + tableNode.children = []; + + var results = axe.runVirtualRule('table-duplicate-name', tableNode); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass when table has empty summary', function () { + var tableNode = new axe.SerialVirtualNode({ + nodeName: 'table', + attributes: { + summary: '' + } + }); + tableNode.children = []; + + var results = axe.runVirtualRule('table-duplicate-name', tableNode); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass when table has empty caption and summary', function () { + var tableNode = new axe.SerialVirtualNode({ + nodeName: 'table', + attributes: { + summary: '' + } + }); + + var captionNode = new axe.SerialVirtualNode({ + nodeName: 'caption' + }); + captionNode.parent = tableNode; + + var textNode = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: '' + }); + textNode.parent = captionNode; + + captionNode.children = [textNode]; + tableNode.children = [captionNode]; + + var results = axe.runVirtualRule('table-duplicate-name', tableNode); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail when table summary and
my table have the same text', function () { + var DUPLICATED_TEXT = 'foobar'; + + var tableNode = new axe.SerialVirtualNode({ + nodeName: 'table', + attributes: { + summary: DUPLICATED_TEXT + } + }); + + var captionNode = new axe.SerialVirtualNode({ + nodeName: 'caption' + }); + captionNode.parent = tableNode; + + var textNode = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: DUPLICATED_TEXT + }); + textNode.parent = captionNode; + + captionNode.children = [textNode]; + tableNode.children = [captionNode]; + + var results = axe.runVirtualRule('table-duplicate-name', tableNode); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail when table summary and have the same text, excluding whitespace', function () { + var DUPLICATED_TEXT = 'foobar'; + + var tableNode = new axe.SerialVirtualNode({ + nodeName: 'table', + attributes: { + summary: ' ' + DUPLICATED_TEXT + } + }); + + var captionNode = new axe.SerialVirtualNode({ + nodeName: 'caption' + }); + captionNode.parent = tableNode; + + var textNode = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: ' \t ' + DUPLICATED_TEXT + }); + textNode.parent = captionNode; + + captionNode.children = [textNode]; + tableNode.children = [captionNode]; + + var results = axe.runVirtualRule('table-duplicate-name', tableNode); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); +});