diff --git a/lib/commons/aria/get-element-unallowed-roles.js b/lib/commons/aria/get-element-unallowed-roles.js index b371dee71a..b746ff2e53 100644 --- a/lib/commons/aria/get-element-unallowed-roles.js +++ b/lib/commons/aria/get-element-unallowed-roles.js @@ -85,9 +85,7 @@ aria.getElementUnallowedRoles = function getElementUnallowedRoles( } // check if role is allowed on element - if (!aria.isAriaRoleAllowedOnElement(node, role)) { - return true; - } + return !aria.isAriaRoleAllowedOnElement(node, role); }); return unallowedRoles; diff --git a/lib/commons/aria/index.js b/lib/commons/aria/index.js index 8422a4722b..996afd87c9 100644 --- a/lib/commons/aria/index.js +++ b/lib/commons/aria/index.js @@ -3,9 +3,11 @@ * @namespace commons.aria * @memberof axe */ +const aria = (commons.aria = {}); +const lookupTable = (aria.lookupTable = {}); -var aria = (commons.aria = {}), - lookupTable = (aria.lookupTable = {}); +const isNull = value => value === null; +const isNotNull = value => value !== null; lookupTable.attributes = { 'aria-activedescendant': { @@ -261,65 +263,6 @@ lookupTable.globalAttributes = [ 'aria-relevant' ]; -const elementConditions = { - CANNOT_HAVE_LIST_ATTRIBUTE: node => { - const nodeAttrs = Array.from(node.attributes).map(a => - a.name.toUpperCase() - ); - if (nodeAttrs.includes('LIST')) { - return false; - } - return true; - }, - CANNOT_HAVE_HREF_ATTRIBUTE: node => { - const nodeAttrs = Array.from(node.attributes).map(a => - a.name.toUpperCase() - ); - if (nodeAttrs.includes('HREF')) { - return false; - } - return true; - }, - MUST_HAVE_HREF_ATTRIBUTE: node => { - if (!node.href) { - return false; - } - return true; - }, - MUST_HAVE_SIZE_ATTRIBUTE_WITH_VALUE_GREATER_THAN_1: node => { - const attr = 'SIZE'; - const nodeAttrs = Array.from(node.attributes).map(a => - a.name.toUpperCase() - ); - if (!nodeAttrs.includes(attr)) { - return false; - } - return Number(node.getAttribute(attr)) > 1; - }, - MUST_HAVE_ALT_ATTRIBUTE: node => { - const attr = 'ALT'; - const nodeAttrs = Array.from(node.attributes).map(a => - a.name.toUpperCase() - ); - if (!nodeAttrs.includes(attr)) { - return false; - } - return true; - }, - MUST_HAVE_ALT_ATTRIBUTE_WITH_VALUE: node => { - const attr = 'ALT'; - const nodeAttrs = Array.from(node.attributes).map(a => - a.name.toUpperCase() - ); - if (!nodeAttrs.includes(attr)) { - return false; - } - const attrValue = node.getAttribute(attr); - // ensure attrValue is defined and have a length (empty string is not allowed) - return attrValue && attrValue.length > 0; - } -}; - lookupTable.role = { // valid roles below alert: { @@ -331,7 +274,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, alertdialog: { type: 'widget', @@ -342,7 +285,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['DIALOG', 'SECTION'] + allowedElements: ['dialog', 'section'] }, application: { type: 'landmark', @@ -354,14 +297,14 @@ lookupTable.role = { context: null, unsupported: false, allowedElements: [ - 'ARTICLE', - 'AUDIO', - 'EMBED', - 'IFRAME', - 'OBJECT', - 'SECTION', - 'SVG', - 'VIDEO' + 'article', + 'audio', + 'embed', + 'iframe', + 'object', + 'section', + 'svg', + 'video' ] }, article: { @@ -390,7 +333,7 @@ lookupTable.role = { context: null, implicit: ['header'], unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, button: { type: 'widget', @@ -411,8 +354,10 @@ lookupTable.role = { unsupported: false, allowedElements: [ { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE + nodeName: 'a', + attributes: { + href: isNotNull + } } ] }, @@ -448,7 +393,7 @@ lookupTable.role = { context: null, implicit: ['input[type="checkbox"]'], unsupported: false, - allowedElements: ['BUTTON'] + allowedElements: ['button'] }, columnheader: { type: 'structure', @@ -492,9 +437,9 @@ lookupTable.role = { unsupported: false, allowedElements: [ { - tagName: 'INPUT', - attributes: { - TYPE: 'TEXT' + nodeName: 'input', + properties: { + type: 'text' } } ] @@ -514,7 +459,7 @@ lookupTable.role = { context: null, implicit: ['aside'], unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, composite: { nameFrom: ['author'], @@ -531,7 +476,7 @@ lookupTable.role = { context: null, implicit: ['footer'], unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, definition: { type: 'structure', @@ -554,7 +499,7 @@ lookupTable.role = { context: null, implicit: ['dialog'], unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, directory: { type: 'structure', @@ -565,7 +510,7 @@ lookupTable.role = { nameFrom: ['author', 'contents'], context: null, unsupported: false, - allowedElements: ['OL', 'UL'] + allowedElements: ['ol', 'ul'] }, document: { type: 'structure', @@ -577,7 +522,7 @@ lookupTable.role = { context: null, implicit: ['body'], unsupported: false, - allowedElements: ['ARTICLE', 'EMBED', 'IFRAME', 'SECTION', 'SVG', 'OBJECT'] + allowedElements: ['article', 'embed', 'iframe', 'section', 'svg', 'object'] }, 'doc-abstract': { type: 'section', @@ -588,7 +533,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-acknowledgments': { type: 'landmark', @@ -599,7 +544,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-afterword': { type: 'landmark', @@ -610,7 +555,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-appendix': { type: 'landmark', @@ -621,7 +566,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-backlink': { type: 'link', @@ -634,8 +579,10 @@ lookupTable.role = { unsupported: false, allowedElements: [ { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE + nodeName: 'a', + attributes: { + href: isNotNull + } } ] }, @@ -654,7 +601,7 @@ lookupTable.role = { nameFrom: ['author'], context: ['doc-bibliography'], unsupported: false, - allowedElements: ['LI'] + allowedElements: ['li'] }, 'doc-bibliography': { type: 'landmark', @@ -665,7 +612,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-biblioref': { type: 'link', @@ -678,8 +625,10 @@ lookupTable.role = { unsupported: false, allowedElements: [ { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE + nodeName: 'a', + attributes: { + href: isNotNull + } } ] }, @@ -692,7 +641,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-colophon': { type: 'section', @@ -703,7 +652,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-conclusion': { type: 'landmark', @@ -714,7 +663,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-cover': { type: 'img', @@ -735,7 +684,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-credits': { type: 'landmark', @@ -746,7 +695,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-dedication': { type: 'section', @@ -757,7 +706,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-endnote': { type: 'listitem', @@ -774,7 +723,7 @@ lookupTable.role = { namefrom: ['author'], context: ['doc-endnotes'], unsupported: false, - allowedElements: ['LI'] + allowedElements: ['li'] }, 'doc-endnotes': { type: 'landmark', @@ -785,7 +734,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-epigraph': { type: 'section', @@ -806,7 +755,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-errata': { type: 'landmark', @@ -817,7 +766,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-example': { type: 'section', @@ -828,7 +777,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['ASIDE', 'SECTION'] + allowedElements: ['aside', 'section'] }, 'doc-footnote': { type: 'section', @@ -839,7 +788,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['ASIDE', 'FOOTER', 'HEADER'] + allowedElements: ['aside', 'footer', 'header'] }, 'doc-foreword': { type: 'landmark', @@ -850,7 +799,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-glossary': { type: 'landmark', @@ -861,7 +810,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['DL'] + allowedElements: ['dl'] }, 'doc-glossref': { type: 'link', @@ -874,8 +823,10 @@ lookupTable.role = { unsupported: false, allowedElements: [ { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE + nodeName: 'a', + attributes: { + href: isNotNull + } } ] }, @@ -888,7 +839,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['NAV', 'SECTION'] + allowedElements: ['nav', 'section'] }, 'doc-introduction': { type: 'landmark', @@ -899,7 +850,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-noteref': { type: 'link', @@ -912,8 +863,10 @@ lookupTable.role = { unsupported: false, allowedElements: [ { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE + nodeName: 'a', + attributes: { + href: isNotNull + } } ] }, @@ -926,7 +879,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-pagebreak': { type: 'separator', @@ -937,7 +890,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['HR'] + allowedElements: ['hr'] }, 'doc-pagelist': { type: 'navigation', @@ -948,7 +901,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['NAV', 'SECTION'] + allowedElements: ['nav', 'section'] }, 'doc-part': { type: 'landmark', @@ -959,7 +912,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-preface': { type: 'landmark', @@ -970,7 +923,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-prologue': { type: 'landmark', @@ -981,7 +934,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-pullquote': { type: 'none', @@ -992,7 +945,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['ASIDE', 'SECTION'] + allowedElements: ['aside', 'section'] }, 'doc-qna': { type: 'section', @@ -1003,7 +956,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, 'doc-subtitle': { type: 'sectionhead', @@ -1014,7 +967,9 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'] + allowedElements: { + nodeName: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] + } }, 'doc-tip': { type: 'note', @@ -1025,7 +980,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['ASIDE'] + allowedElements: ['aside'] }, 'doc-toc': { type: 'navigation', @@ -1036,7 +991,7 @@ lookupTable.role = { namefrom: ['author'], context: null, unsupported: false, - allowedElements: ['NAV', 'SECTION'] + allowedElements: ['nav', 'section'] }, feed: { type: 'structure', @@ -1049,7 +1004,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['ARTICLE', 'ASIDE', 'SECTION'] + allowedElements: ['article', 'aside', 'section'] }, figure: { type: 'structure', @@ -1120,14 +1075,14 @@ lookupTable.role = { implicit: ['details', 'optgroup'], unsupported: false, allowedElements: [ - 'DL', - 'FIGCAPTION', - 'FIELDSET', - 'FIGURE', - 'FOOTER', - 'HEADER', - 'OL', - 'UL' + 'dl', + 'figcaption', + 'fieldset', + 'figure', + 'footer', + 'header', + 'ol', + 'ul' ] }, heading: { @@ -1152,7 +1107,7 @@ lookupTable.role = { context: null, implicit: ['img'], unsupported: false, - allowedElements: ['EMBED', 'IFRAME', 'OBJECT', 'SVG'] + allowedElements: ['embed', 'iframe', 'object', 'svg'] }, input: { nameFrom: ['author'], @@ -1175,17 +1130,11 @@ lookupTable.role = { implicit: ['a[href]'], unsupported: false, allowedElements: [ - 'BUTTON', - { - tagName: 'INPUT', - attributes: { - TYPE: 'IMAGE' - } - }, + 'button', { - tagName: 'INPUT', - attributes: { - TYPE: 'IMAGE' + nodeName: 'input', + properties: { + type: ['image', 'button'] } } ] @@ -1222,7 +1171,7 @@ lookupTable.role = { context: null, implicit: ['select'], unsupported: false, - allowedElements: ['OL', 'UL'] + allowedElements: ['ol', 'ul'] }, listitem: { type: 'structure', @@ -1250,7 +1199,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, main: { type: 'landmark', @@ -1262,7 +1211,7 @@ lookupTable.role = { context: null, implicit: ['main'], unsupported: false, - allowedElements: ['ARTICLE', 'SECTION'] + allowedElements: ['article', 'section'] }, marquee: { type: 'widget', @@ -1273,7 +1222,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, math: { type: 'structure', @@ -1303,7 +1252,7 @@ lookupTable.role = { context: null, implicit: ['menu[type="context"]'], unsupported: false, - allowedElements: ['OL', 'UL'] + allowedElements: ['ol', 'ul'] }, menubar: { type: 'composite', @@ -1319,7 +1268,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['OL', 'UL'] + allowedElements: ['ol', 'ul'] }, menuitem: { type: 'widget', @@ -1337,23 +1286,19 @@ lookupTable.role = { implicit: ['menuitem[type="command"]'], unsupported: false, allowedElements: [ - 'BUTTON', - 'LI', + 'button', + 'li', { - tagName: 'INPUT', - attributes: { - TYPE: 'IMAGE' + nodeName: 'iput', + properties: { + type: ['image', 'button'] } }, { - tagName: 'INPUT', + nodeName: 'a', attributes: { - TYPE: 'BUTTON' + href: isNotNull } - }, - { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE } ] }, @@ -1373,29 +1318,20 @@ lookupTable.role = { implicit: ['menuitem[type="checkbox"]'], unsupported: false, allowedElements: [ - 'BUTTON', - 'LI', { - tagName: 'INPUT', - attributes: { - TYPE: 'CHECKBOX' - } + nodeName: ['button', 'li'] }, { - tagName: 'INPUT', - attributes: { - TYPE: 'IMAGE' + nodeName: 'input', + properties: { + type: ['checkbox', 'image', 'button'] } }, { - tagName: 'INPUT', + nodeName: 'a', attributes: { - TYPE: 'BUTTON' + href: isNotNull } - }, - { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE } ] }, @@ -1416,23 +1352,20 @@ lookupTable.role = { implicit: ['menuitem[type="radio"]'], unsupported: false, allowedElements: [ - 'BUTTON', - 'LI', { - tagName: 'INPUT', - attributes: { - TYPE: 'IMAGE' - } + nodeName: ['button', 'li'] }, { - tagName: 'INPUT', - attributes: { - TYPE: 'BUTTON' + nodeName: 'input', + properties: { + type: ['image', 'button', 'radio'] } }, { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE + nodeName: 'a', + attributes: { + href: isNotNull + } } ] }, @@ -1446,7 +1379,7 @@ lookupTable.role = { context: null, implicit: ['nav'], unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, none: { type: 'structure', @@ -1456,28 +1389,34 @@ lookupTable.role = { context: null, unsupported: false, allowedElements: [ - 'ARTICLE', - 'ASIDE', - 'DL', - 'EMBED', - 'FIGCAPTION', - 'FIELDSET', - 'FIGURE', - 'FOOTER', - 'FORM', - 'H1', - 'H2', - 'H3', - 'H4', - 'H5', - 'H6', - 'HEADER', - 'LI', - 'SECTION', - 'OL', { - tagName: 'IMG', - condition: elementConditions.MUST_HAVE_ALT_ATTRIBUTE + nodeName: [ + 'article', + 'aside', + 'dl', + 'embed', + 'figcaption', + 'fieldset', + 'figure', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'header', + 'li', + 'section', + 'ol' + ] + }, + { + nodeName: 'img', + attributes: { + alt: isNotNull + } } ] }, @@ -1490,7 +1429,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['ASIDE'] + allowedElements: ['aside'] }, option: { type: 'widget', @@ -1509,23 +1448,20 @@ lookupTable.role = { implicit: ['option'], unsupported: false, allowedElements: [ - 'BUTTON', - 'LI', { - tagName: 'INPUT', - attributes: { - TYPE: 'CHECKBOX' - } + nodeName: ['button', 'li'] }, { - tagName: 'INPUT', - attributes: { - TYPE: 'BUTTON' + nodeName: 'input', + properties: { + type: ['checkbox', 'button'] } }, { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE + nodeName: 'a', + attributes: { + href: isNotNull + } } ] }, @@ -1537,30 +1473,34 @@ lookupTable.role = { context: null, unsupported: false, allowedElements: [ - 'ARTICLE', - 'ASIDE', - 'DL', - 'EMBED', - 'FIGCAPTION', - 'FIELDSET', - 'FIGURE', - 'FOOTER', - 'FORM', - 'H1', - 'H2', - 'H3', - 'H4', - 'H5', - 'H6', - 'HEADER', - 'HR', - 'LI', - 'OL', - 'SECTION', - 'UL', { - tagName: 'IMG', - condition: elementConditions.MUST_HAVE_ALT_ATTRIBUTE + nodeName: [ + 'article', + 'aside', + 'dl', + 'embed', + 'figcaption', + 'fieldset', + 'figure', + 'footer', + 'form', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'header', + 'li', + 'section', + 'ol' + ] + }, + { + nodeName: 'img', + attributes: { + alt: isNotNull + } } ] }, @@ -1600,18 +1540,13 @@ lookupTable.role = { implicit: ['input[type="radio"]'], unsupported: false, allowedElements: [ - 'BUTTON', - 'LI', { - tagName: 'INPUT', - attributes: { - TYPE: 'IMAGE' - } + nodeName: ['button', 'li'] }, { - tagName: 'INPUT', - attributes: { - TYPE: 'BUTTON' + nodeName: 'input', + properties: { + type: ['image', 'button'] } } ] @@ -1633,7 +1568,9 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['OL', 'UL'] + allowedElements: { + nodeName: ['ol', 'ul'] + } }, range: { nameFrom: ['author'], @@ -1658,7 +1595,9 @@ lookupTable.role = { 'section[title]' ], unsupported: false, - allowedElements: ['ARTICLE', 'ASIDE'] + allowedElements: { + nodeName: ['article', 'aside'] + } }, roletype: { type: 'abstract', @@ -1745,7 +1684,9 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['ASIDE', 'FORM', 'SECTION'] + allowedElements: { + nodeName: ['aside', 'form', 'section'] + } }, searchbox: { type: 'widget', @@ -1765,14 +1706,12 @@ lookupTable.role = { context: null, implicit: ['input[type="search"]'], unsupported: false, - allowedElements: [ - { - tagName: 'INPUT', - attributes: { - TYPE: 'TEXT' - } + allowedElements: { + nodeName: 'input', + properties: { + type: 'text' } - ] + } }, section: { nameFrom: ['author', 'contents'], @@ -1807,7 +1746,7 @@ lookupTable.role = { context: null, implicit: ['hr'], unsupported: false, - allowedElements: ['LI'] + allowedElements: ['li'] }, slider: { type: 'widget', @@ -1842,14 +1781,12 @@ lookupTable.role = { context: null, implicit: ['input[type="number"]'], unsupported: false, - allowedElements: [ - { - tagName: 'INPUT', - attributes: { - TYPE: 'TEXT' - } + allowedElements: { + nodeName: 'input', + properties: { + type: 'text' } - ] + } }, status: { type: 'widget', @@ -1861,7 +1798,7 @@ lookupTable.role = { context: null, implicit: ['output'], unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, structure: { type: 'abstract', @@ -1878,28 +1815,18 @@ lookupTable.role = { context: null, unsupported: false, allowedElements: [ - 'BUTTON', - { - tagName: 'INPUT', - attributes: { - TYPE: 'CHECKBOX' - } - }, + 'button', { - tagName: 'INPUT', - attributes: { - TYPE: 'IMAGE' + nodeName: 'input', + properties: { + type: ['checkbox', 'image', 'button'] } }, { - tagName: 'INPUT', + nodeName: 'a', attributes: { - TYPE: 'BUTTON' + href: isNotNull } - }, - { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE } ] }, @@ -1919,23 +1846,20 @@ lookupTable.role = { context: ['tablist'], unsupported: false, allowedElements: [ - 'BUTTON', - 'H1', - 'H2', - 'H3', - 'H4', - 'H5', - 'H6', - 'LI', { - tagName: 'INPUT', - attributes: { - TYPE: 'BUTTON' + nodeName: ['button', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li'] + }, + { + nodeName: 'input', + properties: { + type: 'button' } }, { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE + nodeName: 'a', + attributes: { + href: isNotNull + } } ] }, @@ -1970,7 +1894,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['OL', 'UL'] + allowedElements: ['ol', 'ul'] }, tabpanel: { type: 'widget', @@ -1981,7 +1905,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['SECTION'] + allowedElements: ['section'] }, term: { type: 'structure', @@ -2046,7 +1970,7 @@ lookupTable.role = { context: null, implicit: ['menu[type="toolbar"]'], unsupported: false, - allowedElements: ['OL', 'UL'] + allowedElements: ['ol', 'ul'] }, tooltip: { type: 'widget', @@ -2076,7 +2000,7 @@ lookupTable.role = { nameFrom: ['author'], context: null, unsupported: false, - allowedElements: ['OL', 'UL'] + allowedElements: ['ol', 'ul'] }, treegrid: { type: 'composite', @@ -2119,10 +2043,12 @@ lookupTable.role = { context: ['group', 'tree'], unsupported: false, allowedElements: [ - 'LI', + 'li', { - tagName: 'A', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE + nodeName: 'a', + attributes: { + href: isNotNull + } } ] }, @@ -2140,283 +2066,212 @@ lookupTable.role = { // Source: https://www.w3.org/TR/html-aria/ lookupTable.elementsAllowedNoRole = [ { - tagName: 'AREA', - condition: elementConditions.MUST_HAVE_HREF_ATTRIBUTE - }, - 'BASE', - 'BODY', - 'CAPTION', - 'COL', - 'COLGROUP', - 'DATALIST', - 'DD', - 'DETAILS', - 'DT', - 'HEAD', - 'HTML', - { - tagName: 'INPUT', - attributes: { - TYPE: 'COLOR' - } - }, - { - tagName: 'INPUT', - attributes: { - TYPE: 'DATE' - } - }, - { - tagName: 'INPUT', - attributes: { - TYPE: 'DATETIME' - } - }, - { - tagName: 'INPUT', - condition: elementConditions.CANNOT_HAVE_LIST_ATTRIBUTE, - attributes: { - TYPE: 'EMAIL' - } - }, - { - tagName: 'INPUT', - attributes: { - TYPE: 'FILE' - } - }, - { - tagName: 'INPUT', - attributes: { - TYPE: 'HIDDEN' - } - }, - { - tagName: 'INPUT', - attributes: { - TYPE: 'MONTH' - } - }, - { - tagName: 'INPUT', - attributes: { - TYPE: 'NUMBER' - } - }, - { - tagName: 'INPUT', - attributes: { - TYPE: 'PASSWORD' - } - }, - { - tagName: 'INPUT', - attributes: { - TYPE: 'RANGE' - } - }, - { - tagName: 'INPUT', - attributes: { - TYPE: 'RESET' - } - }, - { - tagName: 'INPUT', - condition: elementConditions.CANNOT_HAVE_LIST_ATTRIBUTE, - attributes: { - TYPE: 'SEARCH' - } - }, - { - tagName: 'INPUT', - attributes: { - TYPE: 'SUBMIT' - } - }, - { - tagName: 'INPUT', - condition: elementConditions.CANNOT_HAVE_LIST_ATTRIBUTE, - attributes: { - TYPE: 'TEL' - } - }, - { - tagName: 'INPUT', - attributes: { - TYPE: 'TIME' - } - }, - { - tagName: 'INPUT', - condition: elementConditions.CANNOT_HAVE_LIST_ATTRIBUTE, - attributes: { - TYPE: 'URL' - } + // Plain HTML nodes + nodeName: [ + 'base', + 'body', + 'caption', + 'col', + 'colgroup', + 'datalist', + 'dd', + 'details', + 'dt', + 'head', + 'html', + 'keygen', + 'label', + 'legend', + 'main', + 'map', + 'math', + 'meta', + 'meter', + 'noscript', + 'optgroup', + 'param', + 'picture', + 'progress', + 'script', + 'source', + 'style', + 'template', + 'textarea', + 'title', + 'track' + ] }, { - tagName: 'INPUT', + nodeName: 'area', attributes: { - TYPE: 'WEEK' + href: isNotNull } }, - 'KEYGEN', - 'LABEL', - 'LEGEND', { - tagName: 'LINK', - attributes: { - TYPE: 'HREF' + nodeName: 'input', + properties: { + type: [ + 'color', + 'data', + 'datatime', + 'file', + 'hidden', + 'month', + 'number', + 'password', + 'range', + 'reset', + 'submit', + 'time', + 'week' + ] } }, - 'MAIN', - 'MAP', - 'MATH', { - tagName: 'MENU', + nodeName: 'input', attributes: { - TYPE: 'CONTEXT' + list: isNull + }, + properties: { + type: ['email', 'search', 'tel', 'url'] } }, { - tagName: 'MENUITEM', + nodeName: 'link', attributes: { - TYPE: 'COMMAND' + href: isNotNull } }, { - tagName: 'MENUITEM', + nodeName: 'menu', attributes: { - TYPE: 'CHECKBOX' + type: 'context' } }, { - tagName: 'MENUITEM', + nodeName: 'menuitem', attributes: { - TYPE: 'RADIO' + type: ['command', 'checkbox', 'radio'] } }, - 'META', - 'METER', - 'NOSCRIPT', - 'OPTGROUP', - 'PARAM', - 'PICTURE', - 'PROGRESS', - 'SCRIPT', { - tagName: 'SELECT', - condition: - elementConditions.MUST_HAVE_SIZE_ATTRIBUTE_WITH_VALUE_GREATER_THAN_1, - attributes: { - TYPE: 'MULTIPLE' + nodeName: 'select', + condition: node => { + return Number(node.getAttribute('size')) > 1; + }, + properties: { + multiple: true } }, - 'SOURCE', - 'STYLE', - 'TEMPLATE', - 'TEXTAREA', - 'TITLE', - 'TRACK', // svg elements (below) - 'CLIPPATH', - 'CURSOR', - 'DEFS', - 'DESC', - 'FEBLEND', - 'FECOLORMATRIX', - 'FECOMPONENTTRANSFER', - 'FECOMPOSITE', - 'FECONVOLVEMATRIX', - 'FEDIFFUSELIGHTING', - 'FEDISPLACEMENTMAP', - 'FEDISTANTLIGHT', - 'FEDROPSHADOW', - 'FEFLOOD', - 'FEFUNCA', - 'FEFUNCB', - 'FEFUNCG', - 'FEFUNCR', - 'FEGAUSSIANBLUR', - 'FEIMAGE', - 'FEMERGE', - 'FEMERGENODE', - 'FEMORPHOLOGY', - 'FEOFFSET', - 'FEPOINTLIGHT', - 'FESPECULARLIGHTING', - 'FESPOTLIGHT', - 'FETILE', - 'FETURBULENCE', - 'FILTER', - 'HATCH', - 'HATCHPATH', - 'LINEARGRADIENT', - 'MARKER', - 'MASK', - 'MESHGRADIENT', - 'MESHPATCH', - 'MESHROW', - 'METADATA', - 'MPATH', - 'PATTERN', - 'RADIALGRADIENT', - 'SOLIDCOLOR', - 'STOP', - 'SWITCH', - 'VIEW' + { + nodeName: [ + 'clippath', + 'cursor', + 'defs', + 'desc', + 'feblend', + 'fecolormatrix', + 'fecomponenttransfer', + 'fecomposite', + 'feconvolvematrix', + 'fediffuselighting', + 'fedisplacementmap', + 'fedistantlight', + 'fedropshadow', + 'feflood', + 'fefunca', + 'fefuncb', + 'fefuncg', + 'fefuncr', + 'fegaussianblur', + 'feimage', + 'femerge', + 'femergenode', + 'femorphology', + 'feoffset', + 'fepointlight', + 'fespecularlighting', + 'fespotlight', + 'fetile', + 'feturbulence', + 'filter', + 'hatch', + 'hatchpath', + 'lineargradient', + 'marker', + 'mask', + 'meshgradient', + 'meshpatch', + 'meshrow', + 'metadata', + 'mpath', + 'pattern', + 'radialgradient', + 'solidcolor', + 'stop', + 'switch', + 'view' + ] + } ]; // Source: https://www.w3.org/TR/html-aria/ lookupTable.elementsAllowedAnyRole = [ { - tagName: 'A', - condition: elementConditions.CANNOT_HAVE_HREF_ATTRIBUTE - }, - 'ABBR', - 'ADDRESS', - 'CANVAS', - 'DIV', - 'P', - 'PRE', - 'BLOCKQUOTE', - 'INS', - 'DEL', - 'OUTPUT', - 'SPAN', - 'TABLE', - 'TBODY', - 'THEAD', - 'TFOOT', - 'TD', - 'EM', - 'STRONG', - 'SMALL', - 'S', - 'CITE', - 'Q', - 'DFN', - 'ABBR', - 'TIME', - 'CODE', - 'VAR', - 'SAMP', - 'KBD', - 'SUB', - 'SUP', - 'I', - 'B', - 'U', - 'MARK', - 'RUBY', - 'RT', - 'RP', - 'BDI', - 'BDO', - 'BR', - 'WBR', - 'TH', - 'TR' + nodeName: 'a', + attributes: { + href: isNull + } + }, + { + nodeName: [ + 'abbr', + 'address', + 'canvas', + 'div', + 'p', + 'pre', + 'blockquote', + 'ins', + 'del', + 'output', + 'span', + 'table', + 'tbody', + 'thead', + 'tfoot', + 'td', + 'em', + 'strong', + 'small', + 's', + 'cite', + 'q', + 'dfn', + 'abbr', + 'time', + 'code', + 'var', + 'samp', + 'kbd', + 'sub', + 'sup', + 'i', + 'b', + 'u', + 'mark', + 'ruby', + 'rt', + 'rp', + 'bdi', + 'bdo', + 'br', + 'wbr', + 'th', + 'tr' + ] + } ]; lookupTable.evaluateRoleForElement = { @@ -2472,7 +2327,6 @@ lookupTable.evaluateRoleForElement = { } return true; }, - LINK: ({ node }) => !node.href, MENU: ({ node }) => { if (node.getAttribute('type') === 'context') { return false; diff --git a/lib/commons/aria/is-aria-role-allowed-on-element.js b/lib/commons/aria/is-aria-role-allowed-on-element.js index 2a2fd376eb..8ef8ec9945 100644 --- a/lib/commons/aria/is-aria-role-allowed-on-element.js +++ b/lib/commons/aria/is-aria-role-allowed-on-element.js @@ -1,4 +1,4 @@ -/* global aria */ +/* global aria, matches */ /** * @description validate if a given role is an allowed ARIA role for the supplied node * @method isAriaRoleAllowedOnElement @@ -10,18 +10,15 @@ aria.isAriaRoleAllowedOnElement = function isAriaRoleAllowedOnElement( node, role ) { - const tagName = node.nodeName.toUpperCase(); + const nodeName = node.nodeName.toUpperCase(); const lookupTable = axe.commons.aria.lookupTable; // if given node can have no role - return false - if (aria.validateNodeAndAttributes(node, lookupTable.elementsAllowedNoRole)) { + if (matches(node, lookupTable.elementsAllowedNoRole)) { return false; } - // if given node allows any role - return true - if ( - aria.validateNodeAndAttributes(node, lookupTable.elementsAllowedAnyRole) - ) { + if (matches(node, lookupTable.elementsAllowedAnyRole)) { return true; } @@ -29,30 +26,16 @@ aria.isAriaRoleAllowedOnElement = function isAriaRoleAllowedOnElement( const roleValue = lookupTable.role[role]; // if given role does not exist in lookupTable - return false - if (!roleValue) { + if (!roleValue || !roleValue.allowedElements) { return false; } - // check if role has allowedElements - if not return false - if ( - !( - roleValue.allowedElements && - Array.isArray(roleValue.allowedElements) && - roleValue.allowedElements.length - ) - ) { - return false; - } - - let out = false; // validate attributes and conditions (if any) from allowedElement to given node - out = aria.validateNodeAndAttributes(node, roleValue.allowedElements); + let out = matches(node, roleValue.allowedElements); // if given node type has complex condition to evaluate a given aria-role, execute the same - if (Object.keys(lookupTable.evaluateRoleForElement).includes(tagName)) { - out = lookupTable.evaluateRoleForElement[tagName]({ node, role, out }); + if (Object.keys(lookupTable.evaluateRoleForElement).includes(nodeName)) { + return lookupTable.evaluateRoleForElement[nodeName]({ node, role, out }); } - - // return return out; }; diff --git a/lib/commons/aria/validate-node-and-attributes.js b/lib/commons/aria/validate-node-and-attributes.js deleted file mode 100644 index 1ebd4b81ee..0000000000 --- a/lib/commons/aria/validate-node-and-attributes.js +++ /dev/null @@ -1,89 +0,0 @@ -/* global aria */ -/** - * @description Method that validates a given node against a list of constraints. - * @param {HTMLElement} node node to verify attributes against constraints - * @param {Array} constraintsArray an array containing TAGNAME or an OBJECT abstraction with conditions and attributes - * @return {Boolean} true/ false based on if node passes the constraints expected - */ -aria.validateNodeAndAttributes = function validateNodeAndAttributes( - node, - constraintsArray -) { - const tagName = node.nodeName.toUpperCase(); - - // get all constraints from the list that are of type string - // these string are tag names which can then be validated against the node's tag name - const stringConstraints = constraintsArray.filter(c => typeof c === 'string'); - - // if tag name of the node is part of listed constraints - return true - if (stringConstraints.includes(tagName)) { - return true; - } - - // get all constraints from the list that are of type object - // the further filter the constraints to those that match the given nodes tag name - const objectConstraints = constraintsArray - .filter(c => typeof c === 'object') - .filter(c => { - return c.tagName === tagName; - }); - - // get all attrubutes that are applied on the given node - const nodeAttrs = Array.from(node.attributes).map(a => a.name.toUpperCase()); - - // iterate through all object constraints - // to filter only constraints that have valid attributes and or conditions that are applicable to the given node - const validConstraints = objectConstraints.filter(c => { - // if the constraints does not have any attribtues return false - if (!c.attributes) { - // edge case, where constraints does not have attribute - // but has condition - keep the object - return true - if (c.condition) { - return true; - } - return false; - } - - // get all attributes from constraints - const keys = Object.keys(c.attributes); - if (!keys.length) { - return false; - } - - let keepConstraint = false; - // iterate through each attribute and validate the same on the node - keys.forEach(k => { - if (!nodeAttrs.includes(k)) { - return; - } - // get value of attribute on the given node - const attrValue = node - .getAttribute(k) - .trim() - .toUpperCase(); - // validate a match in the value - if (attrValue === c.attributes[k]) { - keepConstraint = true; - } - }); - return keepConstraint; - }); - - // if not valid constraints to validate against, return - if (!validConstraints.length) { - return false; - } - - // at this juncture there is a match - // only thing to evaluate is a condition on the constraint against the node - let out = true; - - validConstraints.forEach(c => { - if (c.condition && typeof c.condition === 'function') { - out = c.condition(node); - } - }); - - // return - return out; -}; diff --git a/lib/commons/matches/attributes.js b/lib/commons/matches/attributes.js new file mode 100644 index 0000000000..b8d2a043c7 --- /dev/null +++ b/lib/commons/matches/attributes.js @@ -0,0 +1,23 @@ +/* global matches */ +/** + * Check if a node matches some attribute(s) + * + * Note: matches.attributes(node, matcher) can be indirectly used through + * matches(node, { attributes: matcher }) + * + * Example: + * ```js + * matches.attributes(node, { + * 'aria-live': 'assertive', // Simple string match + * 'aria-expanded': /true|false/i, // either boolean, case insensitive + * }) + * ``` + * + * @param {HTMLElement|VirtualNode} node + * @param {Object} Attribute matcher + * @returns {Boolean} + */ +matches.attributes = function matchesAttributes(node, matcher) { + node = node.actualNode || node; + return matches.fromFunction(attrName => node.getAttribute(attrName), matcher); +}; diff --git a/lib/commons/matches/condition.js b/lib/commons/matches/condition.js new file mode 100644 index 0000000000..b9dfb2cede --- /dev/null +++ b/lib/commons/matches/condition.js @@ -0,0 +1,19 @@ +/* global matches */ +/** + * Check if a "thing" is truthy according to a "condition" + * + * Note: matches.condition(node, matcher) can be indirectly used through + * matches(node, { condition: matcher }) + * + * Example: + * ```js + * matches.condition(node, (arg) => arg === null) + * ``` + * + * @param {any} argument + * @param {Function|Null|undefined} condition + * @returns {Boolean} + */ +matches.condition = function(arg, condition) { + return !!condition(arg); +}; diff --git a/lib/commons/matches/from-definition.js b/lib/commons/matches/from-definition.js new file mode 100644 index 0000000000..09044dc00a --- /dev/null +++ b/lib/commons/matches/from-definition.js @@ -0,0 +1,47 @@ +/* global matches */ +const matchers = ['nodeName', 'attributes', 'properties', 'condition']; + +/** + * Check if a node matches some definition + * + * Note: matches.fromDefinition(node, definition) can be indirectly used through + * matches(node, definition) + * + * Example: + * ```js + * matches.fromDefinition(node, { + * nodeName: ['div', 'span'] + * attributes: { + * 'aria-live': 'assertive' + * } + * }) + * ``` + * + * @private + * @param {HTMLElement|VirtualNode} node + * @param {Object|Array} definition + * @returns {Boolean} + */ +matches.fromDefinition = function matchFromDefinition(node, definition) { + node = node.actualNode || node; + if (Array.isArray(definition)) { + return definition.some(definitionItem => matches(node, definitionItem)); + } + if (typeof definition === 'string') { + return axe.utils.matchesSelector(node, definition); + } + + return Object.keys(definition).every(matcherName => { + if (!matchers.includes(matcherName)) { + throw new Error(`Unknown matcher type "${matcherName}"`); + } + // Find the specific matches method to. + // matches.attributes, matches.nodeName, matches.properties, etc. + const matchMethod = matches[matcherName]; + + // Find the matcher that goes into the matches method. + // 'div', /^div$/, (str) => str === 'div', etc. + const matcher = definition[matcherName]; + return matchMethod(node, matcher); + }); +}; diff --git a/lib/commons/matches/from-function.js b/lib/commons/matches/from-function.js new file mode 100644 index 0000000000..55b41fece3 --- /dev/null +++ b/lib/commons/matches/from-function.js @@ -0,0 +1,38 @@ +/* global matches */ + +/** + * Check if the value from a function matches some condition + * + * Each key on the matcher object is passed to getValue, the returned value must match + * with the value of that matcher + * + * Example: + * ```js + * matches.fromFunction( + * (attr => node.getAttribute(attr), + * { + * 'aria-hidden': /^true|false$/i + * } + * ) + * ``` + * + * @private + * @param {Function} getValue + * @param {Object} matcher matcher + * @returns {Boolean} + */ +matches.fromFunction = function matchFromFunction(getValue, matcher) { + const matcherType = typeof matcher; + if ( + matcherType !== 'object' || + Array.isArray(matcher) || + matcher instanceof RegExp + ) { + throw new Error('Expect matcher to be an object'); + } + + // Check that the property has all the expected values + return Object.keys(matcher).every(propName => { + return matches.fromPrimative(getValue(propName), matcher[propName]); + }); +}; diff --git a/lib/commons/matches/from-primative.js b/lib/commons/matches/from-primative.js new file mode 100644 index 0000000000..9a68907808 --- /dev/null +++ b/lib/commons/matches/from-primative.js @@ -0,0 +1,30 @@ +/* global matches */ + +/** + * Check if some value matches + * + * ```js + * match.fromPrimative('foo', 'foo') // true, string is the same + * match.fromPrimative('foo', ['foo', 'bar']) // true, string is included + * match.fromPrimative('foo', /foo/) // true, string matches regex + * match.fromPrimative('foo', str => str.toUpperCase() === 'FOO') // true, function return is truthy + * ``` + * + * @private + * @param {String|Boolean|Array|Number|Null|Undefined} someString + * @param {String|RegExp|Function|Array|Null|Undefined} matcher + * @returns {Boolean} + */ +matches.fromPrimative = function matchFromPrimative(someString, matcher) { + const matcherType = typeof matcher; + if (Array.isArray(matcher) && typeof someString !== 'undefined') { + return matcher.includes(someString); + } + if (matcherType === 'function') { + return !!matcher(someString); + } + if (matcher instanceof RegExp) { + return matcher.test(someString); + } + return matcher === someString; +}; diff --git a/lib/commons/matches/index.js b/lib/commons/matches/index.js new file mode 100644 index 0000000000..917b050cfe --- /dev/null +++ b/lib/commons/matches/index.js @@ -0,0 +1,37 @@ +/* exported matches */ + +/** + * Check if a DOM element matches a definition + * + * Example: + * ```js + * // Match a single nodeName: + * axe.commons.matches(elm, 'div') + * + * // Match one of multiple nodeNames: + * axe.commons.matches(elm, ['ul', 'ol']) + * + * // Match a node with nodeName 'button' and with aria-hidden: true: + * axe.commons.matches(elm, { + * nodeName: 'button', + * attributes: { 'aria-hidden': 'true' } + * }) + * + * // Mixed input. Match button nodeName, input[type=button] and input[type=reset] + * axe.commons.matches(elm, ['button', { + * nodeName: 'input', // nodeName match isn't case sensitive + * properties: { type: ['button', 'reset'] } + * }]) + * ``` + * + * @namespace matches + * @memberof axe.commons + * @param {HTMLElement|VirtualNode} node node to verify attributes against constraints + * @param {Array|String|Object|Function|Regex} definition + * @return {Boolean} true/ false based on if node passes the constraints expected + */ +function matches(node, definition) { + return matches.fromDefinition(node, definition); +} + +commons.matches = matches; diff --git a/lib/commons/matches/node-name.js b/lib/commons/matches/node-name.js new file mode 100644 index 0000000000..732f781088 --- /dev/null +++ b/lib/commons/matches/node-name.js @@ -0,0 +1,34 @@ +/* global matches */ +let isXHTMLGlobal; +/** + * Check if the nodeName of a node matches some value + * + * Note: matches.nodeName(node, matcher) can be indirectly used through + * matches(node, { nodeName: matcher }) + * + * Example: + * ```js + * matches.nodeName(node, ['div', 'span']) + * ``` + * + * @param {HTMLElement|VirtualNode} node + * @param {Object} Attribute matcher + * @returns {Boolean} + */ +matches.nodeName = function matchNodeName(node, matcher, { isXHTML } = {}) { + node = node.actualNode || node; + if (typeof isXHTML === 'undefined') { + // When the matcher is a string, use native .matches() function: + if (typeof matcher === 'string') { + return axe.utils.matchesSelector(node, matcher); + } + + if (typeof isXHTMLGlobal === 'undefined') { + isXHTMLGlobal = axe.utils.isXHTML(node.ownerDocument); + } + isXHTML = isXHTMLGlobal; + } + + const nodeName = isXHTML ? node.nodeName : node.nodeName.toLowerCase(); + return matches.fromPrimative(nodeName, matcher); +}; diff --git a/lib/commons/matches/properties.js b/lib/commons/matches/properties.js new file mode 100644 index 0000000000..c317706e6a --- /dev/null +++ b/lib/commons/matches/properties.js @@ -0,0 +1,25 @@ +/* global matches */ + +/** + * Check if a node matches some attribute(s) + * + * Note: matches.properties(node, matcher) can be indirectly used through + * matches(node, { properties: matcher }) + * + * Example: + * ```js + * matches.properties(node, { + * type: 'text', // Simple string match + * value: value => value.trim() !== '', // None-empty value, using a function matcher + * }) + * ``` + * + * @param {HTMLElement|VirtualNode} node + * @param {Object} matcher + * @returns {Boolean} + */ +matches.properties = function matchesProperties(node, matcher) { + node = node.actualNode || node; + const out = matches.fromFunction(propName => node[propName], matcher); + return out; +}; diff --git a/test/commons/aria/is-aria-role-allowed-on-element.js b/test/commons/aria/is-aria-role-allowed-on-element.js index bb79ad81cc..f6e3c95d39 100644 --- a/test/commons/aria/is-aria-role-allowed-on-element.js +++ b/test/commons/aria/is-aria-role-allowed-on-element.js @@ -125,9 +125,9 @@ describe('aria.isAriaRoleAllowedOnElement', function() { cats: { allowedElements: [ { - tagName: 'INPUT', + nodeName: 'input', attributes: { - TYPE: 'DOG' + value: 'dog' } } ] @@ -136,7 +136,7 @@ describe('aria.isAriaRoleAllowedOnElement', function() { var node = document.createElement('input'); var role = 'cats'; node.setAttribute('role', role); - node.setAttribute('type', 'cats'); + node.setAttribute('value', 'cats'); var actual = axe.commons.aria.isAriaRoleAllowedOnElement(node, role); assert.isFalse(actual); }); diff --git a/test/commons/aria/validate-node-and-attributes.js b/test/commons/aria/validate-node-and-attributes.js deleted file mode 100644 index 708ef185bf..0000000000 --- a/test/commons/aria/validate-node-and-attributes.js +++ /dev/null @@ -1,333 +0,0 @@ -describe('aria.validateNodeAndAttributes', function() { - /** - * @description Convenience method to create a node with supplied attributes and role - * @param {String} tag element tag to construct as node - * @param {Array} attrs list of attributes to add to the created node - * @param {String} role role to add to created node - */ - function getNode(tag, attrs, role) { - var node = document.createElement(tag); - if (attrs) { - Object.keys(attrs).forEach(function(key) { - node.setAttribute(key, attrs[key]); - }); - } - if (role) { - node.setAttribute('role', role); - } - return node; - } - - // Tests for elements that can have No Role: - // defined in axe.commons.aria.lookupTable.elementsAllowedNoRole - describe('validate elements that can have no role (elementsAllowedNoRole)', function() { - var constraints; - - beforeEach(function() { - constraints = axe.commons.aria.lookupTable.elementsAllowedNoRole; - }); - - afterEach(function() { - axe.commons.aria.lookupTable.elementsAllowedNoRole = constraints; - }); - - it('ensure that the lookupTable.elementsAllowedNoRole is invoked', function() { - var overrideInvoked = false; - axe.commons.aria.lookupTable.elementsAllowedNoRole = [ - 'MOOSE', - { - tagName: 'BEAR', - condition: function(node) { - overrideInvoked = true; - assert.isDefined(node); - return true; - } - } - ]; - - var nodeMoose = getNode('moose'); - var actualMoose = axe.commons.aria.validateNodeAndAttributes( - nodeMoose, - axe.commons.aria.lookupTable.elementsAllowedNoRole - ); - assert.isTrue(actualMoose); - - var nodeBear = getNode('bear'); - var actualBear = axe.commons.aria.validateNodeAndAttributes( - nodeBear, - axe.commons.aria.lookupTable.elementsAllowedNoRole - ); - assert.isTrue(overrideInvoked); - assert.isTrue(actualBear); - }); - - // verify tags that are strings - it("returns true for elements ['BASE', 'BODY', 'DATALIST', 'DD', 'CLIPPATH', 'CURSOR', 'META', 'METER', 'PICTURE', 'PROGRESS']", function(done) { - const elTags = [ - 'BASE', - 'BODY', - 'DATALIST', - 'DD', - 'CLIPPATH', - 'CURSOR', - 'META', - 'METER', - 'PICTURE', - 'PROGRESS' - ]; - elTags.forEach(function(el, index) { - var node = getNode(el); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isTrue(actual); - //exit - if (index >= elTags.length - 1) { - done(); - } - }); - }); - - // Verify tags which have object - it('returns true for element AREA with href', function() { - var attrs = { - HREF: '#some-awesome-link' - }; - var node = getNode('area', attrs); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isTrue(actual); - }); - - it('returns false for element AREA with no href', function() { - var node = getNode('area'); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isFalse(actual); - }); - - it('returns true for element INPUT with type COLOR', function() { - var attrs = { - TYPE: 'COLOR' - }; - var node = getNode('input', attrs); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isTrue(actual); - }); - - it('returns true for element INPUT with type DATETIME', function() { - var attrs = { - TYPE: 'DATETIME' - }; - var node = getNode('input', attrs); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isTrue(actual); - }); - - it('returns false for element INPUT with type EMAIL but has LIST attribute', function() { - var attrs = { - TYPE: 'EMAIL', - LIST: 'SOME_VALUE' - }; - var node = getNode('input', attrs); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isFalse(actual); - }); - - it('returns true for element INPUT with type EMAIL and no LIST attribute', function() { - var attrs = { - TYPE: 'EMAIL' - }; - var node = getNode('input', attrs); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isTrue(actual); - }); - - it('returns false for custom element that is not included in elementsAllowedNoRole', function() { - var attrs = { - DATA: 'YO' - }; - var node = getNode('myElement', attrs); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isFalse(actual); - }); - - it('returns false for element SELECT with type MULTIPLE and SIZE 1', function() { - var attrs = { - TYPE: 'MULTIPLE', - SIZE: 1 - }; - var node = getNode('select', attrs); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isFalse(actual); - }); - - it('returns true for element SELECT with type MULTIPLE and SIZE 5', function() { - var attrs = { - TYPE: 'MULTIPLE', - SIZE: 5 - }; - var node = getNode('select', attrs); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isTrue(actual); - }); - }); - - // Tests for elements that can have No Role: - // defined in axe.commons.aria.lookupTable.elementsAllowedAnyRole - describe('validate elements that can have any role (elementsAllowedAnyRole)', function() { - var constraints = axe.commons.aria.lookupTable.elementsAllowedAnyRole; - - beforeEach(function() { - constraints = axe.commons.aria.lookupTable.elementsAllowedAnyRole; - }); - - afterEach(function() { - axe.commons.aria.lookupTable.elementsAllowedAnyRole = constraints; - }); - - it('ensure that the lookupTable.elementsAllowedAnyRole is invoked', function() { - var overrideInvoked = false; - axe.commons.aria.lookupTable.elementsAllowedAnyRole = [ - 'LION', - { - tagName: 'TIGER', - condition: function(node) { - overrideInvoked = true; - assert.isDefined(node); - return false; - } - } - ]; - - var nodeLion = getNode('lion'); - var actualLion = axe.commons.aria.validateNodeAndAttributes( - nodeLion, - axe.commons.aria.lookupTable.elementsAllowedAnyRole - ); - assert.isTrue(actualLion); - - var nodeTiger = getNode('tiger'); - var actualTiger = axe.commons.aria.validateNodeAndAttributes( - nodeTiger, - axe.commons.aria.lookupTable.elementsAllowedAnyRole - ); - assert.isTrue(overrideInvoked); - assert.isFalse(actualTiger); - }); - - // verify tags that are strings - it("returns true for elements ['ABBR', 'CANVAS', 'DIV', 'PRE', 'DEL', 'Q', 'SUB', 'WBR']", function(done) { - const elTags = ['ABBR', 'CANVAS', 'DIV', 'PRE', 'DEL', 'Q', 'SUB', 'WBR']; - elTags.forEach(function(el, index) { - var node = getNode(el); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isTrue(actual); - //exit - if (index >= elTags.length - 1) { - done(); - } - }); - }); - - // Verify tags which have object - it('returns false for element A with href', function() { - var attrs = { - HREF: '#i-cannot-have-a-link' - }; - var node = getNode('a', attrs); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isFalse(actual); - }); - - it('returns true for element A without href', function() { - var node = getNode('a'); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isTrue(actual); - }); - - it('returns false for element BODY', function() { - var node = getNode('body'); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - constraints - ); - assert.isFalse(actual); - }); - }); - - // Tests for elements that are alowed for a given role. - // these elements are defined as attribute allowedElements in role hash - describe('validate allowedElements for a given role in lookupTable.role', function() { - // There are not a lot of tests here as most of the allowedElemets - // are covered in other encapsulating sets like isAriaAllowedOnElement - // The below tests aim to by-pass the isAriaAllowedOnElement method - // and test the validateNodeAndAttributes function - it('returns true for element SECTION with role alert', function() { - var allowedElements = ['SECTION']; - var role = 'alert'; - var node = getNode('section', undefined, role); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - allowedElements - ); - assert.isTrue(actual); - }); - - it('retutn true for INPUT of type image with role menuitem', function() { - var allowedElements = [ - { - tagName: 'INPUT', - attributes: { - TYPE: 'IMAGE' - } - } - ]; - var attrs = { - TYPE: 'IMAGE' - }; - var role = 'menuitem'; - var node = getNode('input', attrs, role); - var actual = axe.commons.aria.validateNodeAndAttributes( - node, - allowedElements - ); - assert.isTrue(actual); - }); - }); -}); diff --git a/test/commons/matches/attributes.js b/test/commons/matches/attributes.js new file mode 100644 index 0000000000..a60671d87b --- /dev/null +++ b/test/commons/matches/attributes.js @@ -0,0 +1,65 @@ +describe('matches.attributes', function() { + var attributes = axe.commons.matches.attributes; + var fixture = document.querySelector('#fixture'); + beforeEach(function() { + fixture.innerHTML = ''; + }); + + it('returns true if all attributes match', function() { + fixture.innerHTML = ''; + assert.isTrue( + attributes(fixture.firstChild, { + foo: 'baz', + bar: 'foo', + baz: 'bar' + }) + ); + }); + + it('returns false if some attributes do not match', function() { + fixture.innerHTML = ''; + assert.isFalse( + attributes(fixture.firstChild, { + foo: 'baz', + bar: 'foo', + baz: 'baz' + }) + ); + }); + + it('returns false if any attributes are missing', function() { + fixture.innerHTML = ''; + assert.isFalse( + attributes(fixture.firstChild, { + foo: 'baz', + bar: 'foo', + baz: 'bar' + }) + ); + }); + + it('works with virtual nodes', function() { + fixture.innerHTML = ''; + assert.isTrue( + attributes( + { + actualNode: fixture.firstChild + }, + { + foo: 'bar', + bar: 'foo' + } + ) + ); + assert.isFalse( + attributes( + { + actualNode: fixture.firstChild + }, + { + baz: 'baz' + } + ) + ); + }); +}); diff --git a/test/commons/matches/condition.js b/test/commons/matches/condition.js new file mode 100644 index 0000000000..96f689ec56 --- /dev/null +++ b/test/commons/matches/condition.js @@ -0,0 +1,28 @@ +describe('matches.condition', function() { + var condition = axe.commons.matches.condition; + + it('passes the first argument to the condition', function() { + var count = 0; + condition('foo', function(foo) { + assert.equal('foo', foo); + count++; + }); + assert.equal(count, 1); + }); + + it('returns true if the condition returns a truthy value', function() { + assert.isTrue( + condition('foo', function() { + return 123; + }) + ); + }); + + it('returns false if the condition returns a falsey value', function() { + assert.isFalse( + condition('foo', function() { + return 0; + }) + ); + }); +}); diff --git a/test/commons/matches/from-definition.js b/test/commons/matches/from-definition.js new file mode 100644 index 0000000000..6cc9df5b77 --- /dev/null +++ b/test/commons/matches/from-definition.js @@ -0,0 +1,249 @@ +describe('matches.fromDefinition', function() { + var fromDefinition = axe.commons.matches.fromDefinition; + var fixture = document.querySelector('#fixture'); + beforeEach(function() { + fixture.innerHTML = ''; + }); + + it('applies a css selector when the matcher is a string', function() { + fixture.innerHTML = '
foo
'; + assert.isTrue(fromDefinition(fixture.firstChild, '#fixture > div')); + assert.isFalse(fromDefinition(fixture.firstChild, '#fixture > span')); + }); + + it('matches a definition with a `nodeName` property', function() { + fixture.innerHTML = '
foo
'; + const matchers = [ + 'div', + ['div', 'span'], + /div/, + function(nodeName) { + return nodeName === 'div'; + } + ]; + matchers.forEach(function(matcher) { + assert.isTrue( + fromDefinition(fixture.firstChild, { + nodeName: matcher + }) + ); + }); + assert.isFalse( + fromDefinition(fixture.firstChild, { + nodeName: 'span' + }) + ); + }); + + it('matches a definition with an `attributes` property', function() { + fixture.innerHTML = '
foo
'; + const matchers = [ + 'bar', + ['bar', 'baz'], + /bar/, + function(attributeName) { + return attributeName === 'bar'; + } + ]; + matchers.forEach(function(matcher) { + assert.isTrue( + fromDefinition(fixture.firstChild, { + attributes: { + foo: matcher + } + }) + ); + }); + assert.isFalse( + fromDefinition(fixture.firstChild, { + attributes: { + foo: 'baz' + } + }) + ); + }); + + it('matches a definition with a `properties` property', function() { + fixture.innerHTML = ''; + const matchers = [ + 'text', + ['text', 'password'], + /text/, + function(type) { + return type === 'text'; + } + ]; + matchers.forEach(function(matcher) { + assert.isTrue( + fromDefinition(fixture.firstChild, { + properties: { + type: matcher + } + }) + ); + }); + assert.isFalse( + fromDefinition(fixture.firstChild, { + properties: { + type: 'password' + } + }) + ); + }); + + it('returns true when all matching properties return true', function() { + fixture.innerHTML = ''; + assert.isTrue( + fromDefinition(fixture.firstChild, { + nodeName: 'input', + properties: { + type: 'text', + value: 'bar' + }, + attributes: { + 'aria-disabled': 'true' + } + }) + ); + }); + + it('returns false when some matching properties return false', function() { + fixture.innerHTML = ''; + assert.isFalse( + fromDefinition(fixture.firstChild, { + nodeName: 'input', + attributes: { + 'aria-disabled': 'false' + } + }) + ); + }); + + describe('with virtual nodes', function() { + it('matches using a string', function() { + fixture.innerHTML = '
foo
'; + var node = { actualNode: fixture.firstChild }; + assert.isTrue(fromDefinition(node, 'div')); + assert.isFalse(fromDefinition(node, 'span')); + }); + + it('matches nodeName', function() { + fixture.innerHTML = '
foo
'; + var node = { actualNode: fixture.firstChild }; + assert.isTrue( + fromDefinition(node, { + nodeName: 'div' + }) + ); + assert.isFalse( + fromDefinition(node, { + nodeName: 'span' + }) + ); + }); + + it('matches attributes', function() { + fixture.innerHTML = '
foo
'; + var node = { actualNode: fixture.firstChild }; + assert.isTrue( + fromDefinition(node, { + attributes: { + foo: 'bar' + } + }) + ); + assert.isFalse( + fromDefinition(node, { + attributes: { + foo: 'baz' + } + }) + ); + }); + + it('matches properties', function() { + fixture.innerHTML = ''; + var node = { actualNode: fixture.firstChild }; + assert.isTrue( + fromDefinition(node, { + properties: { + value: 'foo' + } + }) + ); + assert.isFalse( + fromDefinition(node, { + properties: { + value: 'bar' + } + }) + ); + }); + }); + + describe('with a `condition` property', function() { + it('calls condition and uses its return value as a matcher', function() { + fixture.innerHTML = '
foo
'; + assert.isTrue( + fromDefinition(fixture.firstChild, { + condition: function(node) { + assert.deepEqual(node, fixture.firstChild); + node.setAttribute('foo', 'bar'); + return true; + } + }) + ); + assert.isFalse( + fromDefinition(fixture.firstChild, { + condition: function() { + return false; + } + }) + ); + assert.equal(fixture.firstChild.getAttribute('foo'), 'bar'); + }); + + it('uses the return value as a matcher', function() { + var returnVal = 'true'; + function condition() { + return returnVal; + } + assert.isTrue( + fromDefinition(fixture, { + condition: condition // Truthy test + }) + ); + + returnVal = 0; // Falsey test + assert.isFalse( + fromDefinition(fixture, { + condition: condition + }) + ); + }); + }); + + describe('with an `array` of definitions', function() { + it('returns true if any definition in the array matches', function() { + fixture.innerHTML = '
foo
'; + assert.isTrue( + fromDefinition(fixture.firstChild, [ + { nodeName: 'span' }, + { nodeName: 'div' }, + { nodeName: 'h1' } + ]) + ); + }); + + it('returns false if none definition in the array matches', function() { + fixture.innerHTML = ''; + assert.isFalse( + fromDefinition(fixture.firstChild, [ + { nodeName: 'span' }, + { nodeName: 'div' }, + { nodeName: 'h1' } + ]) + ); + }); + }); +}); diff --git a/test/commons/matches/from-function.js b/test/commons/matches/from-function.js new file mode 100644 index 0000000000..67b18cb6a3 --- /dev/null +++ b/test/commons/matches/from-function.js @@ -0,0 +1,81 @@ +describe('matches.fromFunction', function() { + var fromFunction = axe.commons.matches.fromFunction; + function noop() {} + + it('throws an error when the matcher is a number', function() { + assert.throws(function() { + fromFunction(noop, 123); + }); + }); + + it('throws an error when the matcher is a string', function() { + assert.throws(function() { + fromFunction(noop, 'foo'); + }); + }); + + it('throws an error when the matcher is an array', function() { + assert.throws(function() { + fromFunction(noop, ['foo']); + }); + }); + + it('throws an error when the matcher is a RegExp', function() { + assert.throws(function() { + fromFunction(noop, /foo/); + }); + }); + + describe('with object matches', function() { + var keyMap = {}; + function getValue(key) { + return key; + } + + it('passes every object key to the getValue function once', function() { + var keys = ['foo', 'bar', 'baz']; + function getValue(key) { + var index = keys.indexOf(key); + assert.notEqual(index, -1); + keys.splice(index, 1); + return key; + } + + fromFunction(getValue, { + foo: 'foo', + bar: 'bar', + baz: 'baz' + }); + assert.lengthOf(keys, 0); + }); + + it('returns true if every value is matched', function() { + keyMap = { + foo: 'foo', + bar: 'bar', + baz: 'baz' + }; + assert.isTrue(fromFunction(getValue, keyMap)); + }); + + it('returns false if any value is not matched', function() { + keyMap = { + foo: 'foo', + bar: 'bar', + baz: 'baz' + }; + assert.isFalse( + fromFunction(function(key) { + if (key === 'bar') { + return 'mismatch'; + } + return key; + }, keyMap) + ); + }); + + it('returns true if there are no keys', function() { + assert.isTrue(fromFunction(getValue, {})); + }); + }); +}); diff --git a/test/commons/matches/from-primative.js b/test/commons/matches/from-primative.js new file mode 100644 index 0000000000..e7173789ba --- /dev/null +++ b/test/commons/matches/from-primative.js @@ -0,0 +1,80 @@ +describe('matches.fromPrimative', function() { + var fromPrimative = axe.commons.matches.fromPrimative; + + it('returns true when strictly equal', function() { + assert.isTrue(fromPrimative('foo', 'foo')); + assert.isTrue(fromPrimative(null, null)); + assert.isTrue(fromPrimative(true, true)); + assert.isTrue(fromPrimative(123, 123)); + assert.isTrue(fromPrimative(undefined, undefined)); + }); + + it('returns false when not strictly equal', function() { + assert.isFalse(fromPrimative('foo', 'bar')); + assert.isFalse(fromPrimative(null, undefined)); + assert.isFalse(fromPrimative(false, null)); + assert.isFalse(fromPrimative(true, false)); + assert.isFalse(fromPrimative(123, 456)); + assert.isFalse(fromPrimative(undefined, null)); + }); + + describe('with array matchers', function() { + it('returns true if the string is included', function() { + assert.isTrue(fromPrimative('bar', ['foo', 'bar', 'baz'])); + }); + it('returns false if the string is not included', function() { + assert.isFalse(fromPrimative('foo bar', ['foo', 'bar', 'baz'])); + }); + it('returns false when passed `undefined`', function() { + assert.isFalse(fromPrimative(undefined, ['foo', 'bar', 'baz'])); + }); + }); + + describe('with function matchers', function() { + it('returns true if the function returns a truthy value', function() { + assert.isTrue( + fromPrimative('foo', function(val) { + assert.equal(val, 'foo'); + return true; + }) + ); + assert.isTrue( + fromPrimative('foo', function() { + return 123; + }) + ); + assert.isTrue( + fromPrimative('foo', function() { + return {}; + }) + ); + }); + it('returns false if the function returns a falsey value', function() { + assert.isFalse( + fromPrimative('foo', function(val) { + assert.equal(val, 'foo'); + return false; + }) + ); + assert.isFalse( + fromPrimative('foo', function() { + return 0; + }) + ); + assert.isFalse( + fromPrimative('foo', function() { + return undefined; + }) + ); + }); + }); + + describe('with RegExp matchers', function() { + it('returns true if the regexp matches', function() { + assert.isTrue(fromPrimative('bar', /^(foo|bar|baz)$/)); + }); + it('returns false if the regexp does not match', function() { + assert.isFalse(fromPrimative('foobar', /^(foo|bar|baz)$/)); + }); + }); +}); diff --git a/test/commons/matches/node-name.js b/test/commons/matches/node-name.js new file mode 100644 index 0000000000..6955f63107 --- /dev/null +++ b/test/commons/matches/node-name.js @@ -0,0 +1,68 @@ +describe('matches.nodeName', function() { + var matchNodeName = axe.commons.matches.nodeName; + var fixture = document.querySelector('#fixture'); + beforeEach(function() { + fixture.innerHTML = ''; + }); + + it('returns true if the nodeName is the same as the matcher', function() { + fixture.innerHTML = '

foo

'; + assert.isTrue(matchNodeName(fixture.firstChild, 'h1')); + }); + + it('returns true if the nodename is included in an array', function() { + fixture.innerHTML = '

foo

'; + assert.isTrue(matchNodeName(fixture.firstChild, ['h3', 'h2', 'h1'])); + }); + + it('returns true if the nodeName matches a regexp', function() { + fixture.innerHTML = '

foo

'; + assert.isTrue(matchNodeName(fixture.firstChild, /^h[0-6]$/)); + }); + + it('returns true if the nodeName matches with a function', function() { + fixture.innerHTML = '

foo

'; + assert.isTrue( + matchNodeName(fixture.firstChild, function(nodeName) { + return nodeName === 'h1'; + }) + ); + }); + + it('returns false if the nodeName does not match', function() { + fixture.innerHTML = '
foo
'; + assert.isFalse(matchNodeName(fixture.firstChild, 'h1')); + assert.isFalse(matchNodeName(fixture.firstChild, ['h3', 'h2', 'h1'])); + assert.isFalse(matchNodeName(fixture.firstChild, /^h[0-6]$/)); + assert.isFalse( + matchNodeName(fixture.firstChild, function(nodeName) { + return nodeName === 'h1'; + }) + ); + }); + + it('is case sensitive for XHTML', function() { + var elm = { + // Mock DOM node + nodeName: 'H1', + ownerDocument: document + }; + assert.isFalse(matchNodeName(elm, 'h1', { isXHTML: true })); + }); + + it('is case insensitive for HTML, but not for XHTML', function() { + var elm = { + // Mock DOM node + nodeName: 'H1', + ownerDocument: document + }; + assert.isTrue(matchNodeName(elm, 'h1', { isXHTML: false })); + }); + + it('works with virtual nodes', function() { + fixture.innerHTML = '

foo

'; + const virtualNode = { actualNode: fixture.firstChild }; + assert.isTrue(matchNodeName(virtualNode, 'h1')); + assert.isFalse(matchNodeName(virtualNode, 'div')); + }); +}); diff --git a/test/commons/matches/properties.js b/test/commons/matches/properties.js new file mode 100644 index 0000000000..0e5fbd0e3b --- /dev/null +++ b/test/commons/matches/properties.js @@ -0,0 +1,82 @@ +describe('matches.properties', function() { + var properties = axe.commons.matches.properties; + + it('returns true if all properties match', function() { + assert.isTrue( + properties( + { + foo: 'baz', + bar: 'foo', + baz: 'bar' + }, + { + foo: 'baz', + baz: 'bar' + } + ) + ); + }); + + it('returns false if some properties do not match', function() { + assert.isFalse( + properties( + { + foo: 'baz', + bar: 'foo', + baz: 'bar' + }, + { + foo: 'baz', + bar: 'foo', + baz: 'baz' + } + ) + ); + }); + + it('returns false if any properties are missing', function() { + assert.isFalse( + properties( + { + foo: 'baz', + baz: 'bar' + }, + { + foo: 'baz', + bar: 'foo', + baz: 'bar' + } + ) + ); + }); + + it('works with virtual nodes', function() { + assert.isTrue( + properties( + { + actualNode: { + foo: 'bar', + bar: 'foo' + } + }, + { + foo: 'bar', + bar: 'foo' + } + ) + ); + assert.isFalse( + properties( + { + actualNode: { + foo: 'bar', + bar: 'foo' + } + }, + { + baz: 'baz' + } + ) + ); + }); +});