From b16f5535d17a0089d6fa7d5c6cdc80483f36e9a5 Mon Sep 17 00:00:00 2001 From: Dan Bowling Date: Mon, 29 Aug 2022 14:20:27 -0600 Subject: [PATCH] refactor(empty-table-header): use virtual node (#3621) use virtual node references: #3473 --- lib/commons/standards/implicit-html-roles.js | 6 +- lib/commons/table/get-scope.js | 22 ++- test/commons/table/get-scope.js | 88 ++++++---- test/commons/table/is-header.js | 16 +- .../virtual-rules/empty-table-header.js | 155 ++++++++++++++++++ 5 files changed, 233 insertions(+), 54 deletions(-) create mode 100644 test/integration/virtual-rules/empty-table-header.js diff --git a/lib/commons/standards/implicit-html-roles.js b/lib/commons/standards/implicit-html-roles.js index 1e077e4446..0a7b2da142 100644 --- a/lib/commons/standards/implicit-html-roles.js +++ b/lib/commons/standards/implicit-html-roles.js @@ -134,7 +134,7 @@ const implicitHtmlRoles = { case '': return !suggestionsSourceElement ? 'textbox' : 'combobox'; - default: + default: return 'textbox'; } }, @@ -171,10 +171,10 @@ const implicitHtmlRoles = { textarea: 'textbox', tfoot: 'rowgroup', th: vNode => { - if (isColumnHeader(vNode.actualNode)) { + if (isColumnHeader(vNode)) { return 'columnheader'; } - if (isRowHeader(vNode.actualNode)) { + if (isRowHeader(vNode)) { return 'rowheader'; } }, diff --git a/lib/commons/table/get-scope.js b/lib/commons/table/get-scope.js index cef6cd8049..633e3665a3 100644 --- a/lib/commons/table/get-scope.js +++ b/lib/commons/table/get-scope.js @@ -1,23 +1,27 @@ import toGrid from './to-grid'; import getCellPosition from './get-cell-position'; import findUp from '../dom/find-up'; +import { getNodeFromTree } from '../../core/utils'; +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; /** * Determine if a `HTMLTableCellElement` is a column header, if so get the scope of the header * @method getScope * @memberof axe.commons.table * @instance - * @param {HTMLTableCellElement} cell The table cell to test + * @param {HTMLTableCellElement|AbstractVirtualNode} cell The table cell to test * @return {Boolean|String} Returns `false` if not a column header, or the scope of the column header element */ function getScope(cell) { - var scope = cell.getAttribute('scope'); - var role = cell.getAttribute('role'); + const vNode = + cell instanceof AbstractVirtualNode ? cell : getNodeFromTree(cell); - if ( - cell instanceof window.Element === false || - ['TD', 'TH'].indexOf(cell.nodeName.toUpperCase()) === -1 - ) { + cell = vNode.actualNode; + + const scope = vNode.attr('scope'); + const role = vNode.attr('role'); + + if (!['td', 'th'].includes(vNode.props.nodeName)) { throw new TypeError('Expected TD or TH element'); } @@ -27,8 +31,10 @@ function getScope(cell) { return 'row'; } else if (scope === 'col' || scope === 'row') { return scope; - } else if (cell.nodeName.toUpperCase() !== 'TH') { + } else if (vNode.props.nodeName !== 'th') { return false; + } else if (!vNode.actualNode) { + return 'auto'; } var tableGrid = toGrid(findUp(cell, 'table')); var pos = getCellPosition(cell, tableGrid); diff --git a/test/commons/table/get-scope.js b/test/commons/table/get-scope.js index eb6b5687c2..3c37660de0 100644 --- a/test/commons/table/get-scope.js +++ b/test/commons/table/get-scope.js @@ -1,4 +1,4 @@ -describe('table.getScope', function() { +describe('table.getScope', function () { 'use strict'; function $id(id) { @@ -6,13 +6,9 @@ describe('table.getScope', function() { } var fixture = $id('fixture'); + var fixtureSetup = axe.testUtils.fixtureSetup; - afterEach(function() { - fixture.innerHTML = ''; - axe._tree = undefined; - }); - - it('returns false for TD without scope attribute', function() { + it('returns false for TD without scope attribute', function () { fixture.innerHTML = '' + '' + @@ -20,25 +16,33 @@ describe('table.getScope', function() { '
1
'; var target = $id('target'); + fixtureSetup(); assert.equal(axe.commons.table.getScope(target), false); }); - it('throws if it is not passed a data cell', function() { - assert.throws(function() { + it('throws if it is not passed a data cell', function () { + assert.throws(function () { axe.commons.table.getScope(); - }); + }, TypeError); - assert.throws(function() { - axe.commons.table.getScope(document.createElement('tr')); - }); + var node = document.createElement('tr'); + axe.utils.getFlattenedTree(node); - assert.doesNotThrow(function() { - axe.commons.table.getScope(document.createElement('td')); + assert.throws(function () { + axe.commons.table.getScope(node); + }, TypeError); + }); + + it('does not throw if it is passed a data cell', function () { + var node = document.createElement('td'); + axe.utils.getFlattenedTree(node); + assert.doesNotThrow(function () { + axe.commons.table.getScope(node); }); }); - describe('auto scope', function() { - it('return `auto` with implicit row and col scope', function() { + describe('auto scope', function () { + it('return `auto` with implicit row and col scope', function () { fixture.innerHTML = '' + '' + @@ -50,7 +54,7 @@ describe('table.getScope', function() { assert.equal(axe.commons.table.getScope(target), 'auto'); }); - it('return `auto` with implicit row and col scope, not in the first column', function() { + it('return `auto` with implicit row and col scope, not in the first column', function () { fixture.innerHTML = '
1ok
' + '' + @@ -62,7 +66,7 @@ describe('table.getScope', function() { assert.equal(axe.commons.table.getScope(target), 'auto'); }); - it('return `auto` with implicit row and col scope, not in the first row', function() { + it('return `auto` with implicit row and col scope, not in the first row', function () { fixture.innerHTML = '
1
' + '' + @@ -73,10 +77,18 @@ describe('table.getScope', function() { axe.testUtils.flatTreeSetup(fixture.firstChild); assert.equal(axe.commons.table.getScope(target), 'auto'); }); + + it('return `auto` without an actualNode or in the tree', function () { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'th' + }); + + assert.equal(axe.commons.table.getScope(serialNode), 'auto'); + }); }); - describe('col scope', function() { - it('returns `col` with explicit col scope on TH', function() { + describe('col scope', function () { + it('returns `col` with explicit col scope on TH', function () { fixture.innerHTML = '
1
' + '' + @@ -84,10 +96,11 @@ describe('table.getScope', function() { '
1
'; var target = $id('target'); + fixtureSetup(); assert.equal(axe.commons.table.getScope(target), 'col'); }); - it('returns `col` with explicit col scope on TD', function() { + it('returns `col` with explicit col scope on TD', function () { fixture.innerHTML = '' + '' + @@ -95,10 +108,11 @@ describe('table.getScope', function() { '
1
'; var target = $id('target'); + fixtureSetup(); assert.equal(axe.commons.table.getScope(target), 'col'); }); - it('returns `col` with role = columnheader on TD', function() { + it('returns `col` with role = columnheader on TD', function () { fixture.innerHTML = '' + '' + @@ -106,10 +120,11 @@ describe('table.getScope', function() { '
1
'; var target = $id('target'); + fixtureSetup(); assert.equal(axe.commons.table.getScope(target), 'col'); }); - it('returns `col` when part of a row of all TH elements', function() { + it('returns `col` when part of a row of all TH elements', function () { fixture.innerHTML = '' + '' + @@ -121,7 +136,7 @@ describe('table.getScope', function() { assert.equal(axe.commons.table.getScope(target), 'col'); }); - it('returns `col` when part of both a row and a column of all TH elements', function() { + it('returns `col` when part of both a row and a column of all TH elements', function () { fixture.innerHTML = '
1
' + '' + @@ -133,7 +148,7 @@ describe('table.getScope', function() { assert.equal(axe.commons.table.getScope(target), 'col'); }); - it('understands colspan on the table', function() { + it('understands colspan on the table', function () { fixture.innerHTML = '
1
' + '' + @@ -145,7 +160,7 @@ describe('table.getScope', function() { assert.equal(axe.commons.table.getScope(target), 'col'); }); - it('understands colspan on the cell', function() { + it('understands colspan on the cell', function () { fixture.innerHTML = '
' + '' + @@ -157,8 +172,8 @@ describe('table.getScope', function() { }); }); - describe('row scope', function() { - it('returns `row` with explicit row scope on TH', function() { + describe('row scope', function () { + it('returns `row` with explicit row scope on TH', function () { fixture.innerHTML = '
' + '' + @@ -166,10 +181,11 @@ describe('table.getScope', function() { '
1
'; var target = $id('target'); + fixtureSetup(); assert.equal(axe.commons.table.getScope(target), 'row'); }); - it('returns `row` with explicit row scope on TD', function() { + it('returns `row` with explicit row scope on TD', function () { fixture.innerHTML = '' + '' + @@ -177,10 +193,11 @@ describe('table.getScope', function() { '
1
'; var target = $id('target'); + fixtureSetup(); assert.equal(axe.commons.table.getScope(target), 'row'); }); - it('returns `row` with role = rowheader on TD', function() { + it('returns `row` with role = rowheader on TD', function () { fixture.innerHTML = '' + '' + @@ -188,10 +205,11 @@ describe('table.getScope', function() { '
1
'; var target = $id('target'); + fixtureSetup(); assert.equal(axe.commons.table.getScope(target), 'row'); }); - it('returns `row` when part of a column of all TH elements', function() { + it('returns `row` when part of a column of all TH elements', function () { fixture.innerHTML = '' + '' + @@ -203,7 +221,7 @@ describe('table.getScope', function() { assert.equal(axe.commons.table.getScope(target), 'row'); }); - it('understands rowspan in the table', function() { + it('understands rowspan in the table', function () { fixture.innerHTML = '
1
' + '' + @@ -214,7 +232,7 @@ describe('table.getScope', function() { assert.equal(axe.commons.table.getScope(target), 'row'); }); - it('understands rowspan on the cell', function() { + it('understands rowspan on the cell', function () { fixture.innerHTML = '
' + '' + @@ -227,7 +245,7 @@ describe('table.getScope', function() { }); }); - it('does not throw on empty rows', function() { + it('does not throw on empty rows', function () { fixture.innerHTML = '
' + '' + diff --git a/test/commons/table/is-header.js b/test/commons/table/is-header.js index d6fb83b119..17c5beda6f 100644 --- a/test/commons/table/is-header.js +++ b/test/commons/table/is-header.js @@ -1,4 +1,4 @@ -describe('table.isHeader', function() { +describe('table.isHeader', function () { 'use strict'; var table = axe.commons.table; @@ -21,43 +21,43 @@ describe('table.isHeader', function() { '' + '
'; - it('should return true for column headers', function() { + it('should return true for column headers', function () { fixtureSetup(tableFixture); var cell = document.querySelector('#ch1'); assert.isTrue(table.isHeader(cell)); }); - it('should return true if cell is a row header', function() { + it('should return true if cell is a row header', function () { fixtureSetup(tableFixture); var cell = document.querySelector('#rh1'); assert.isTrue(table.isHeader(cell)); }); - it('should return false if cell is not a column or row header', function() { + it('should return false if cell is not a column or row header', function () { fixtureSetup(tableFixture); var cell = document.querySelector('#cell1'); assert.isFalse(table.isHeader(cell)); }); - it('should return true if referenced by another cells headers attr', function() { + it('should return true if referenced by another cells headers attr', function () { fixture.innerHTML = '' + '' + '
1
'; var target = document.querySelector('#target'); - + fixtureSetup(); assert.isTrue(table.isHeader(target)); }); - it('should return false otherwise', function() { + it('should return false otherwise', function () { fixture.innerHTML = '' + '' + '
1
'; var target = document.querySelector('#target'); - + fixtureSetup(); assert.isFalse(table.isHeader(target)); }); }); diff --git a/test/integration/virtual-rules/empty-table-header.js b/test/integration/virtual-rules/empty-table-header.js new file mode 100644 index 0000000000..f08b2c4140 --- /dev/null +++ b/test/integration/virtual-rules/empty-table-header.js @@ -0,0 +1,155 @@ +describe('empty-table-header virtual-rule', function () { + it('should incomplete when children are missing', function () { + var thNode = new axe.SerialVirtualNode({ + nodeName: 'th' + }); + thNode.children = []; + + var results = axe.runVirtualRule('empty-table-header', thNode); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); + }); + + it('should pass with a table header', function () { + var tableNode = new axe.SerialVirtualNode({ + nodeName: 'table' + }); + + var trNode = new axe.SerialVirtualNode({ + nodeName: 'tr' + }); + trNode.parent = tableNode; + + var thNode = new axe.SerialVirtualNode({ + nodeName: 'th' + }); + thNode.parent = trNode; + + var textNode = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: 'foobar' + }); + textNode.parent = thNode; + + thNode.children = [textNode]; + trNode.children = [thNode]; + tableNode.children = [trNode]; + + var results = axe.runVirtualRule('empty-table-header', tableNode); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass with scope of row', function () { + var tableNode = new axe.SerialVirtualNode({ + nodeName: 'table' + }); + + var trNode = new axe.SerialVirtualNode({ + nodeName: 'tr' + }); + trNode.parent = tableNode; + + var thNode = new axe.SerialVirtualNode({ + nodeName: 'th', + attributes: { + scope: 'row' + } + }); + thNode.parent = trNode; + + var textNode = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: 'foobar' + }); + textNode.parent = thNode; + + thNode.children = [textNode]; + trNode.children = [thNode]; + tableNode.children = [trNode]; + var results = axe.runVirtualRule('empty-table-header', thNode); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass with scope of col', function () { + var tableNode = new axe.SerialVirtualNode({ + nodeName: 'table' + }); + + var trNode = new axe.SerialVirtualNode({ + nodeName: 'tr' + }); + trNode.parent = tableNode; + + var thNode = new axe.SerialVirtualNode({ + nodeName: 'th', + attributes: { + scope: 'col' + } + }); + thNode.parent = trNode; + + var textNode = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: 'foobar' + }); + textNode.parent = thNode; + + thNode.children = [textNode]; + trNode.children = [thNode]; + tableNode.children = [trNode]; + + var results = axe.runVirtualRule('empty-table-header', thNode); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass with a table definition of role rowheader', function () { + var node = new axe.SerialVirtualNode({ + nodeName: 'td', + attributes: { + role: 'rowheader' + } + }); + var child = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: 'foobar' + }); + node.children = [child]; + + var results = axe.runVirtualRule('empty-table-header', node); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail if table header has no child nodes', function () { + var node = new axe.SerialVirtualNode({ + nodeName: 'th', + attributes: { + role: 'rowheader' + } + }); + node.children = []; + + var results = axe.runVirtualRule('empty-table-header', node); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); +});