diff --git a/lib/checks/aria/aria-allowed-role-evaluate.js b/lib/checks/aria/aria-allowed-role-evaluate.js index 5ca99e9fc9..40e0df04b6 100644 --- a/lib/checks/aria/aria-allowed-role-evaluate.js +++ b/lib/checks/aria/aria-allowed-role-evaluate.js @@ -1,4 +1,4 @@ -import { isVisible } from '../../commons/dom'; +import { isVisibleForScreenreader } from '../../commons/dom'; import { getElementUnallowedRoles } from '../../commons/aria'; /** @@ -29,7 +29,7 @@ function ariaAllowedRoleEvaluate(node, options = {}, virtualNode) { const unallowedRoles = getElementUnallowedRoles(virtualNode, allowImplicit); if (unallowedRoles.length) { this.data(unallowedRoles); - if (!isVisible(virtualNode, true)) { + if (!isVisibleForScreenreader(virtualNode)) { // flag hidden elements for review return undefined; } diff --git a/lib/checks/aria/aria-errormessage-evaluate.js b/lib/checks/aria/aria-errormessage-evaluate.js index bacb1c9c7c..482a7d6491 100644 --- a/lib/checks/aria/aria-errormessage-evaluate.js +++ b/lib/checks/aria/aria-errormessage-evaluate.js @@ -1,7 +1,7 @@ import standards from '../../standards'; import { idrefs } from '../../commons/dom'; import { tokenList } from '../../core/utils'; -import { isVisible } from '../../commons/dom'; +import { isVisibleForScreenreader } from '../../commons/dom'; /** * Check if `aria-errormessage` references an element that also uses a technique to announce the message (aria-live, aria-describedby, etc.). * @@ -55,7 +55,7 @@ function ariaErrormessageEvaluate(node, options, virtualNode) { } if (idref) { - if (!isVisible(idref, true)) { + if (!isVisibleForScreenreader(idref)) { this.data({ messageKey: 'hidden', values: tokenList(attr) diff --git a/lib/checks/color/color-contrast-evaluate.js b/lib/checks/color/color-contrast-evaluate.js index e10b0bc6bf..5895a6250e 100644 --- a/lib/checks/color/color-contrast-evaluate.js +++ b/lib/checks/color/color-contrast-evaluate.js @@ -1,4 +1,4 @@ -import { isVisible } from '../../commons/dom'; +import { isVisibleOnScreen } from '../../commons/dom'; import { visibleVirtual, hasUnicode, @@ -29,7 +29,7 @@ export default function colorContrastEvaluate(node, options, virtualNode) { pseudoSizeThreshold } = options; - if (!isVisible(node, false)) { + if (!isVisibleOnScreen(node)) { this.data({ messageKey: 'hidden' }); return true; } diff --git a/lib/checks/generic/has-descendant-evaluate.js b/lib/checks/generic/has-descendant-evaluate.js index b4e50a549a..62824a54f1 100644 --- a/lib/checks/generic/has-descendant-evaluate.js +++ b/lib/checks/generic/has-descendant-evaluate.js @@ -1,5 +1,5 @@ import { querySelectorAllFilter } from '../../core/utils'; -import { isVisible, isModalOpen } from '../../commons/dom'; +import { isVisibleForScreenreader, isModalOpen } from '../../commons/dom'; function hasDescendant(node, options, virtualNode) { if (!options || !options.selector || typeof options.selector !== 'string') { @@ -15,7 +15,7 @@ function hasDescendant(node, options, virtualNode) { const matchingElms = querySelectorAllFilter( virtualNode, options.selector, - vNode => isVisible(vNode.actualNode, true) + vNode => isVisibleForScreenreader(vNode) ); this.relatedNodes(matchingElms.map(vNode => vNode.actualNode)); return matchingElms.length > 0; diff --git a/lib/checks/generic/page-no-duplicate-evaluate.js b/lib/checks/generic/page-no-duplicate-evaluate.js index 5d87ec0277..93cfac680d 100644 --- a/lib/checks/generic/page-no-duplicate-evaluate.js +++ b/lib/checks/generic/page-no-duplicate-evaluate.js @@ -1,6 +1,6 @@ import cache from '../../core/base/cache'; import { querySelectorAllFilter } from '../../core/utils'; -import { isVisible, findUpVirtual } from '../../commons/dom'; +import { isVisibleForScreenreader, findUpVirtual } from '../../commons/dom'; function pageNoDuplicateEvaluate(node, options, virtualNode) { if (!options || !options.selector || typeof options.selector !== 'string') { @@ -18,7 +18,7 @@ function pageNoDuplicateEvaluate(node, options, virtualNode) { cache.set(key, true); let elms = querySelectorAllFilter(axe._tree[0], options.selector, elm => - isVisible(elm.actualNode, true) + isVisibleForScreenreader(elm) ); // Filter elements that, within certain contexts, don't map their role. diff --git a/lib/checks/keyboard/accesskeys-evaluate.js b/lib/checks/keyboard/accesskeys-evaluate.js index 36dcef1b9a..97eb36169d 100644 --- a/lib/checks/keyboard/accesskeys-evaluate.js +++ b/lib/checks/keyboard/accesskeys-evaluate.js @@ -1,7 +1,7 @@ -import { isVisible } from '../../commons/dom'; +import { isVisibleOnScreen } from '../../commons/dom'; function accesskeysEvaluate(node) { - if (isVisible(node, false)) { + if (isVisibleOnScreen(node)) { this.data(node.getAttribute('accesskey')); this.relatedNodes([node]); } diff --git a/lib/checks/label/explicit-evaluate.js b/lib/checks/label/explicit-evaluate.js index e118360e49..b056b8e41d 100644 --- a/lib/checks/label/explicit-evaluate.js +++ b/lib/checks/label/explicit-evaluate.js @@ -1,4 +1,4 @@ -import { getRootNode, isVisible } from '../../commons/dom'; +import { getRootNode, isVisibleOnScreen } from '../../commons/dom'; import { accessibleText } from '../../commons/text'; import { escapeSelector } from '../../core/utils'; @@ -16,7 +16,7 @@ function explicitEvaluate(node, options, virtualNode) { try { return labels.some(label => { // defer to hidden-explicit-label check for better messaging - if (!isVisible(label)) { + if (!isVisibleOnScreen(label)) { return true; } else { return !!accessibleText(label); diff --git a/lib/checks/label/hidden-explicit-label-evaluate.js b/lib/checks/label/hidden-explicit-label-evaluate.js index 06c2678acf..e69fb09a99 100644 --- a/lib/checks/label/hidden-explicit-label-evaluate.js +++ b/lib/checks/label/hidden-explicit-label-evaluate.js @@ -1,4 +1,4 @@ -import { getRootNode, isVisible } from '../../commons/dom'; +import { getRootNode, isVisibleForScreenreader } from '../../commons/dom'; import { accessibleTextVirtual } from '../../commons/text'; import { escapeSelector } from '../../core/utils'; @@ -12,7 +12,7 @@ function hiddenExplicitLabelEvaluate(node, options, virtualNode) { const id = escapeSelector(node.getAttribute('id')); const label = root.querySelector(`label[for="${id}"]`); - if (label && !isVisible(label, true)) { + if (label && !isVisibleForScreenreader(label)) { let name; try { name = accessibleTextVirtual(virtualNode).trim(); diff --git a/lib/checks/label/multiple-label-evaluate.js b/lib/checks/label/multiple-label-evaluate.js index baae68963b..a2f4964d5c 100644 --- a/lib/checks/label/multiple-label-evaluate.js +++ b/lib/checks/label/multiple-label-evaluate.js @@ -1,4 +1,4 @@ -import { getRootNode, isVisible, idrefs } from '../../commons/dom'; +import { getRootNode, isVisibleOnScreen, isVisibleForScreenreader, idrefs } from '../../commons/dom'; import { escapeSelector } from '../../core/utils'; function multipleLabelEvaluate(node) { @@ -10,7 +10,7 @@ function multipleLabelEvaluate(node) { if (labels.length) { // filter out CSS hidden labels because they're fine - labels = labels.filter(label => isVisible(label)); + labels = labels.filter(label => isVisibleOnScreen(label)); } while (parent) { @@ -27,7 +27,7 @@ function multipleLabelEvaluate(node) { // more than 1 CSS visible label if (labels.length > 1) { - const ATVisibleLabels = labels.filter(label => isVisible(label, true)); + const ATVisibleLabels = labels.filter(label => isVisibleForScreenreader(label)); // more than 1 AT visible label will fail IOS/Safari/VO even with aria-labelledby if (ATVisibleLabels.length > 1) { return undefined; diff --git a/lib/checks/lists/only-dlitems-evaluate.js b/lib/checks/lists/only-dlitems-evaluate.js index 98663e3f72..985d1a7841 100644 --- a/lib/checks/lists/only-dlitems-evaluate.js +++ b/lib/checks/lists/only-dlitems-evaluate.js @@ -1,4 +1,4 @@ -import { isVisible } from '../../commons/dom'; +import { isVisibleForScreenreader } from '../../commons/dom'; import { getRole, getExplicitRole } from '../../commons/aria'; function onlyDlitemsEvaluate(node, options, virtualNode) { @@ -23,7 +23,7 @@ function onlyDlitemsEvaluate(node, options, virtualNode) { const { actualNode } = childNode; const tagName = actualNode.nodeName.toUpperCase(); - if (actualNode.nodeType === 1 && isVisible(actualNode, true, false)) { + if (actualNode.nodeType === 1 && isVisibleForScreenreader(actualNode)) { const explicitRole = getExplicitRole(actualNode); if ((tagName !== 'DT' && tagName !== 'DD') || explicitRole) { diff --git a/lib/checks/lists/only-listitems-evaluate.js b/lib/checks/lists/only-listitems-evaluate.js index 6b0d926a50..8e1b7788e3 100644 --- a/lib/checks/lists/only-listitems-evaluate.js +++ b/lib/checks/lists/only-listitems-evaluate.js @@ -1,4 +1,4 @@ -import { isVisible } from '../../commons/dom'; +import { isVisibleForScreenreader } from '../../commons/dom'; import { getRole } from '../../commons/aria'; function onlyListitemsEvaluate(node, options, virtualNode) { @@ -17,7 +17,7 @@ function onlyListitemsEvaluate(node, options, virtualNode) { return; } - if (actualNode.nodeType !== 1 || !isVisible(actualNode, true, false)) { + if (actualNode.nodeType !== 1 || !isVisibleForScreenreader(actualNode)) { return; } diff --git a/lib/checks/navigation/heading-order-evaluate.js b/lib/checks/navigation/heading-order-evaluate.js index 4585e92ad1..c3600cedb9 100644 --- a/lib/checks/navigation/heading-order-evaluate.js +++ b/lib/checks/navigation/heading-order-evaluate.js @@ -1,6 +1,6 @@ import cache from '../../core/base/cache'; import { querySelectorAllFilter, getAncestry } from '../../core/utils'; -import { isVisible } from '../../commons/dom'; +import { isVisibleForScreenreader } from '../../commons/dom'; import { getRole } from '../../commons/aria'; function getLevel(vNode) { @@ -54,8 +54,10 @@ function headingOrderEvaluate() { // @see https://github.com/dequelabs/axe-core/issues/728 const selector = 'h1, h2, h3, h4, h5, h6, [role=heading], iframe, frame'; // TODO: es-modules_tree - const vNodes = querySelectorAllFilter(axe._tree[0], selector, vNode => - isVisible(vNode.actualNode, true) + const vNodes = querySelectorAllFilter( + axe._tree[0], + selector, + isVisibleForScreenreader ); headingOrder = vNodes.map(vNode => { diff --git a/lib/checks/navigation/region-evaluate.js b/lib/checks/navigation/region-evaluate.js index 48b86e92f1..a4b34387ae 100644 --- a/lib/checks/navigation/region-evaluate.js +++ b/lib/checks/navigation/region-evaluate.js @@ -46,7 +46,7 @@ function findRegionlessElms(virtualNode, options) { ['iframe', 'frame'].includes(virtualNode.props.nodeName) || (dom.isSkipLink(virtualNode.actualNode) && dom.getElementByReference(virtualNode.actualNode, 'href')) || - !dom.isVisible(node, true) + !dom.isVisibleForScreenreader(node) ) { // Mark each parent node as having region descendant let vNode = virtualNode; diff --git a/lib/checks/navigation/skip-link-evaluate.js b/lib/checks/navigation/skip-link-evaluate.js index 292b4e3651..b691810ce7 100644 --- a/lib/checks/navigation/skip-link-evaluate.js +++ b/lib/checks/navigation/skip-link-evaluate.js @@ -1,9 +1,9 @@ -import { getElementByReference, isVisible } from '../../commons/dom'; +import { getElementByReference, isVisibleForScreenreader } from '../../commons/dom'; function skipLinkEvaluate(node) { const target = getElementByReference(node, 'href'); if (target) { - return isVisible(target, true) || undefined; + return isVisibleForScreenreader(target) || undefined; } return false; } diff --git a/lib/checks/shared/is-on-screen-evaluate.js b/lib/checks/shared/is-on-screen-evaluate.js index e289511436..1c525f0532 100644 --- a/lib/checks/shared/is-on-screen-evaluate.js +++ b/lib/checks/shared/is-on-screen-evaluate.js @@ -1,8 +1,8 @@ -import { isVisible, isOffscreen } from '../../commons/dom'; +import { isVisibleOnScreen, isOffscreen } from '../../commons/dom'; function isOnScreenEvaluate(node) { // From a visual perspective - return isVisible(node, false) && !isOffscreen(node); + return isVisibleOnScreen(node) && !isOffscreen(node); } export default isOnScreenEvaluate; diff --git a/lib/commons/dom/focus-disabled.js b/lib/commons/dom/focus-disabled.js index 108ec7b301..34e75bc0ad 100644 --- a/lib/commons/dom/focus-disabled.js +++ b/lib/commons/dom/focus-disabled.js @@ -1,6 +1,6 @@ import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; import { getNodeFromTree } from '../../core/utils'; -import isHiddenWithCSS from './is-hidden-with-css'; +import isHiddenForEveryone from './is-hidden-for-everyone'; // Source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/disabled const allowedDisabledNodeNames = [ 'button', @@ -74,7 +74,7 @@ function focusDisabled(el) { if (!vNode.actualNode) { return false; } - return isHiddenWithCSS(vNode.actualNode); + return isHiddenForEveryone(vNode); } return false; diff --git a/lib/commons/dom/get-rect-stack.js b/lib/commons/dom/get-rect-stack.js index 6febf1a404..e2872886d2 100644 --- a/lib/commons/dom/get-rect-stack.js +++ b/lib/commons/dom/get-rect-stack.js @@ -1,4 +1,4 @@ -import isVisible from './is-visible'; +import isVisibleOnScreen from './is-visible-on-screen'; import VirtualNode from '../../core/base/virtual-node/virtual-node'; import { getNodeFromTree, getScroll, isShadowRoot } from '../../core/utils'; @@ -80,7 +80,7 @@ export function createGrid( // (we don't do this before so we can calculate stacking context // of parents with 0 width/height) const rect = vNode.boundingClientRect; - if (rect.width !== 0 && rect.height !== 0 && isVisible(node)) { + if (rect.width !== 0 && rect.height !== 0 && isVisibleOnScreen(node)) { addNodeToGrid(grid, vNode); } diff --git a/lib/commons/dom/has-lang-text.js b/lib/commons/dom/has-lang-text.js index 6dceb6f440..11c37d020d 100644 --- a/lib/commons/dom/has-lang-text.js +++ b/lib/commons/dom/has-lang-text.js @@ -1,6 +1,6 @@ import { hasChildTextNodes } from './has-content-virtual'; import isVisualContent from './is-visual-content'; -import isHiddenWithCSS from './is-hidden-with-css'; +import isHiddenForEveryone from './is-hidden-for-everyone'; /** * Check that a node has text, or an accessible name which language is defined by the @@ -23,6 +23,6 @@ export default function hasLangText(virtualNode) { child => !child.attr('lang') && // non-empty lang hasLangText(child) && // has text - !isHiddenWithCSS(child) // Not hidden for anyone + !isHiddenForEveryone(child) // Not hidden for anyone ); } diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index 0e23bfa913..b9be84acbe 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -23,6 +23,7 @@ export { default as insertedIntoFocusOrder } from './inserted-into-focus-order'; export { default as isCurrentPageLink } from './is-current-page-link'; export { default as isFocusable } from './is-focusable'; export { default as isHiddenWithCSS } from './is-hidden-with-css'; +export { default as isHiddenForEveryone } from './is-hidden-for-everyone'; export { default as isHTML5 } from './is-html5'; export { default as isInTabOrder } from './is-in-tab-order'; export { default as isInTextBlock } from './is-in-text-block'; @@ -33,6 +34,8 @@ export { default as isNode } from './is-node'; export { default as isOffscreen } from './is-offscreen'; export { default as isOpaque } from './is-opaque'; export { default as isSkipLink } from './is-skip-link'; +export { default as isVisibleForScreenreader } from './is-visible-for-screenreader'; +export { default as isVisibleOnScreen } from './is-visible-on-screen'; export { default as isVisible } from './is-visible'; export { default as isVisualContent } from './is-visual-content'; export { default as reduceToElementsBelowFloating } from './reduce-to-elements-below-floating'; diff --git a/lib/commons/dom/is-hidden-for-everyone.js b/lib/commons/dom/is-hidden-for-everyone.js new file mode 100644 index 0000000000..a2f03c4947 --- /dev/null +++ b/lib/commons/dom/is-hidden-for-everyone.js @@ -0,0 +1,75 @@ +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree } from '../../core/utils'; +import memoize from '../../core/utils/memoize'; +import { + nativelyHidden, + displayHidden, + visibilityHidden +} from './visibility-methods'; + +const hiddenMethods = [displayHidden, visibilityHidden]; + +/** + * Determine if an element is hidden from screenreaders and visual users + * @method isHiddenForEveryone + * @memberof axe.commons.dom + * @param {VirtualNode} vNode The Virtual Node + * @param {Object} [options] + * @param {Boolean} [options.skipAncestors] If the ancestor tree should be not be used + * @param {Boolean} [options.isAncestor] If this function is being called on an ancestor for the target node + * @return {Boolean} The element's visibility state + */ +export default function isHiddenForEveryone( + vNode, + { skipAncestors, isAncestor = false } = {} +) { + vNode = vNode instanceof AbstractVirtualNode ? vNode : getNodeFromTree(vNode); + + if (skipAncestors) { + return isHiddenSelf(vNode, isAncestor); + } + + return isHiddenAncestors(vNode, isAncestor); +} + +/** + * Check the element for visibility state + */ +const isHiddenSelf = memoize(function isHiddenSelfMemoized(vNode, isAncestor) { + if (nativelyHidden(vNode)) { + return true; + } + + if (!vNode.actualNode) { + return false; + } + + if (hiddenMethods.some(method => method(vNode, { isAncestor }))) { + return true; + } + + // detached node + if (!vNode.actualNode.isConnected) { + return true; + } + + return false; +}); + +/** + * Check the element and ancestors for visibility state + */ +const isHiddenAncestors = memoize(function isHiddenAncestorsMemoized( + vNode, + isAncestor +) { + if (isHiddenSelf(vNode, isAncestor)) { + return true; + } + + if (!vNode.parent) { + return false; + } + + return isHiddenAncestors(vNode.parent, true); +}); diff --git a/lib/commons/dom/is-hidden-with-css.js b/lib/commons/dom/is-hidden-with-css.js index fc5a7af241..847c961ce9 100644 --- a/lib/commons/dom/is-hidden-with-css.js +++ b/lib/commons/dom/is-hidden-with-css.js @@ -10,6 +10,7 @@ import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-n * @param {HTMLElement} el The HTML Element * @param {Boolean} descendentVisibilityValue (Optional) immediate descendant visibility value used for recursive computation * @return {Boolean} the element's hidden status + * @deprecated use isHiddenForEveryone */ function isHiddenWithCSS(node, descendentVisibilityValue) { const vNode = diff --git a/lib/commons/dom/is-modal-open.js b/lib/commons/dom/is-modal-open.js index 7c71b4d5e6..c9feb673fb 100644 --- a/lib/commons/dom/is-modal-open.js +++ b/lib/commons/dom/is-modal-open.js @@ -1,4 +1,4 @@ -import isVisible from './is-visible'; +import isVisibleOnScreen from './is-visible-on-screen'; import getViewportSize from './get-viewport-size'; import cache from '../../core/base/cache'; import { querySelectorAllFilter } from '../../core/utils'; @@ -42,7 +42,7 @@ function isModalOpen(options) { // TODO: es-module-_tree axe._tree[0], 'dialog, [role=dialog], [aria-modal=true]', - vNode => isVisible(vNode.actualNode) + isVisibleOnScreen ); if (definiteModals.length) { diff --git a/lib/commons/dom/is-offscreen.js b/lib/commons/dom/is-offscreen.js index ed25204f19..ffad2c0470 100644 --- a/lib/commons/dom/is-offscreen.js +++ b/lib/commons/dom/is-offscreen.js @@ -1,6 +1,7 @@ import getComposedParent from './get-composed-parent'; import getElementCoordinates from './get-element-coordinates'; import getViewportSize from './get-viewport-size'; +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; function noParentScrolled(element, offset) { element = getComposedParent(element); @@ -22,9 +23,22 @@ function noParentScrolled(element, offset) { * @memberof axe.commons.dom * @instance * @param {Element} element - * @return {Boolean} + * @param {Object} [options] + * @param {Boolean} [options.isAncestor] If this function is being called on an ancestor of the target node + * @return {Boolean|undefined} */ -function isOffscreen(element) { +function isOffscreen(element, { isAncestor } = {}) { + if (isAncestor) { + return false; + } + + element = + element instanceof AbstractVirtualNode ? element.actualNode : element; + + if (!element) { + return undefined; + } + let leftBoundary; const docElement = document.documentElement; const styl = window.getComputedStyle(element); diff --git a/lib/commons/dom/is-visible-for-screenreader.js b/lib/commons/dom/is-visible-for-screenreader.js new file mode 100644 index 0000000000..7d1d732001 --- /dev/null +++ b/lib/commons/dom/is-visible-for-screenreader.js @@ -0,0 +1,42 @@ +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree } from '../../core/utils'; +import memoize from '../../core/utils/memoize'; +import isHiddenForEveryone from './is-hidden-for-everyone'; +import { ariaHidden, areaHidden } from './visibility-methods'; + +/** + * Determine if an element is visible to a screen reader + * @method isVisibleForScreenreader + * @memberof axe.commons.dom + * @param {VirtualNode} vNode The Virtual Node + * @return {Boolean} True if the element is visible to a screen reader + */ +export default function isVisibleForScreenreader(vNode) { + vNode = vNode instanceof AbstractVirtualNode ? vNode : getNodeFromTree(vNode); + return isVisibleForScreenreaderVirtual(vNode); +} + +/** + * Check the element and ancestors + */ +const isVisibleForScreenreaderVirtual = memoize( + function isVisibleForScreenreaderMemoized(vNode, isAncestor) { + if (ariaHidden(vNode)) { + return false; + } + + if (vNode.actualNode && vNode.props.nodeName === 'area') { + return !areaHidden(vNode, isVisibleForScreenreaderVirtual); + } + + if (isHiddenForEveryone(vNode, { skipAncestors: true, isAncestor })) { + return false; + } + + if (!vNode.parent) { + return true; + } + + return isVisibleForScreenreaderVirtual(vNode.parent, true); + } +); diff --git a/lib/commons/dom/is-visible-on-screen.js b/lib/commons/dom/is-visible-on-screen.js new file mode 100644 index 0000000000..3411533c40 --- /dev/null +++ b/lib/commons/dom/is-visible-on-screen.js @@ -0,0 +1,58 @@ +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree } from '../../core/utils'; +import memoize from '../../core/utils/memoize'; +import isHiddenForEveryone from './is-hidden-for-everyone'; +import { + opacityHidden, + scrollHidden, + overflowHidden, + clipHidden, + areaHidden +} from './visibility-methods'; +import isOffscreen from './is-offscreen'; + +const hiddenMethods = [ + opacityHidden, + scrollHidden, + overflowHidden, + clipHidden, + isOffscreen +]; + +/** + * Determine if an element is visible on screen + * @method isVisibleOnScreen + * @memberof axe.commons.dom + * @param {VirtualNode} vNode The Virtual Node + * @return {Boolean} True if the element is visible on screen + */ +export default function isVisibleOnScreen(vNode) { + vNode = vNode instanceof AbstractVirtualNode ? vNode : getNodeFromTree(vNode); + return isVisibleOnScreenVirtual(vNode); +} + +const isVisibleOnScreenVirtual = memoize(function isVisibleOnScreenMemoized( + vNode, + isAncestor +) { + if (vNode.actualNode && vNode.props.nodeName === 'area') { + return !areaHidden(vNode, isVisibleOnScreenVirtual); + } + + if (isHiddenForEveryone(vNode, { skipAncestors: true, isAncestor })) { + return false; + } + + if ( + vNode.actualNode && + hiddenMethods.some(method => method(vNode, { isAncestor })) + ) { + return false; + } + + if (!vNode.parent) { + return true; + } + + return isVisibleOnScreenVirtual(vNode.parent, true); +}); diff --git a/lib/commons/dom/is-visible.js b/lib/commons/dom/is-visible.js index 8cb847719d..94b6d9f2e7 100644 --- a/lib/commons/dom/is-visible.js +++ b/lib/commons/dom/is-visible.js @@ -105,6 +105,7 @@ function isAreaVisible(el, screenReader, recursed) { * @param {Boolean} screenReader When provided, will evaluate visibility from the perspective of a screen reader * @param {Boolean} recursed * @return {Boolean} The element's visibilty status + * @deprecated use isVisibleOnScreen or isVisibleForScreenreader if `screenReader: true` was passed */ function isVisible(el, screenReader, recursed) { if (!el) { diff --git a/lib/commons/dom/visibility-methods.js b/lib/commons/dom/visibility-methods.js new file mode 100644 index 0000000000..41b35293f9 --- /dev/null +++ b/lib/commons/dom/visibility-methods.js @@ -0,0 +1,186 @@ +import { + getScroll, + closest, + getRootNode, + querySelectorAll, + escapeSelector +} from '../../core/utils'; + +const clipRegex = + /rect\s*\(([0-9]+)px,?\s*([0-9]+)px,?\s*([0-9]+)px,?\s*([0-9]+)px\s*\)/; +const clipPathRegex = /(\w+)\((\d+)/; + +/** + * Determine if an element is natively hidden + * @param {VirtualNode} vNode + * @return {Boolean} + */ +export function nativelyHidden(vNode) { + return ['style', 'script', 'noscript', 'template'].includes( + vNode.props.nodeName + ); +} + +/** + * Determine if an element is hidden using the display property + * @param {VirtualNode} vNode + * @return {Boolean} + */ +export function displayHidden(vNode) { + // Firefox's user-agent always sets `area` element + // to `display:none` so we can't rely on it to + // check for hidden + if (vNode.props.nodeName === 'area') { + return false; + } + + return vNode.getComputedStylePropertyValue('display') === 'none'; +} + +/** + * Determine if an element is hidden using the visibility property. Visibility is only applicable for the node itself (and not any ancestors) + * @param {VirtualNode} vNode + * @param {Object} [options] + * @param {Boolean} [options.isAncestor] If this function is being called on an ancestor for the target node + * @return {Boolean} + */ +export function visibilityHidden(vNode, { isAncestor } = {}) { + // because the parent can be hidden, and the child visible we + // have to ignore visibility on ancestors. we don't need to look + // at the parent either, because visibility inherits + return ( + !isAncestor && + ['hidden', 'collapse'].includes( + vNode.getComputedStylePropertyValue('visibility') + ) + ); +} + +/** + * Determine if an element is hidden using the aria-hidden attribute + * @param {VirtualNode} vNode + * @return {Boolean} + */ +export function ariaHidden(vNode) { + return vNode.attr('aria-hidden') === 'true'; +} + +/** + * Determine if an element is hidden by making the opacity 0 + * @param {VirtualNode} vNode + * @return {Boolean} + */ +export function opacityHidden(vNode) { + return vNode.getComputedStylePropertyValue('opacity') === '0'; +} + +/** + * Determine if an element is hidden by using scroll and dimensions + * @param {VirtualNode} vNode + * @return {Boolean} + */ +export function scrollHidden(vNode) { + const scroll = getScroll(vNode.actualNode); + const elHeight = parseInt(vNode.getComputedStylePropertyValue('height')); + const elWidth = parseInt(vNode.getComputedStylePropertyValue('width')); + + return !!scroll && (elHeight === 0 || elWidth === 0); +} + +/** + * Determine if an element is hidden by using overflow: hidden and dimensions + * @param {VirtualNode} vNode + * @return {Boolean} + */ +export function overflowHidden(vNode) { + const elHeight = parseInt(vNode.getComputedStylePropertyValue('height')); + const elWidth = parseInt(vNode.getComputedStylePropertyValue('width')); + + return ( + vNode.getComputedStylePropertyValue('position') === 'absolute' && + (elHeight < 2 || elWidth < 2) && + vNode.getComputedStylePropertyValue('overflow') === 'hidden' + ); +} + +/** + * Determines if an element is hidden with a clip or clip-path technique + * @param {VirtualNode} vNode + * @return {Boolean} + */ +export function clipHidden(vNode) { + const matchesClip = vNode + .getComputedStylePropertyValue('clip') + .match(clipRegex); + const matchesClipPath = vNode + .getComputedStylePropertyValue('clip-path') + .match(clipPathRegex); + if (matchesClip && matchesClip.length === 5) { + const position = vNode.getComputedStylePropertyValue('position'); + // clip is only applied to absolutely positioned elements + if (['fixed', 'absolute'].includes(position)) { + return ( + matchesClip[3] - matchesClip[1] <= 0 && + matchesClip[2] - matchesClip[4] <= 0 + ); + } + } + if (matchesClipPath) { + const type = matchesClipPath[1]; + const value = parseInt(matchesClipPath[2], 10); + + switch (type) { + case 'inset': + return value >= 50; + case 'circle': + return value === 0; + default: + } + } + + return false; +} + +/** + * Check `AREA` element is hidden + * - validate if it is a child of `map` + * - ensure `map` is referred by `img` using the `usemap` attribute + * @param {VirtualNode} vNode + * @param {Function} visibleFunction Function used to check if the image element is visible + * @retruns {Boolean} + */ +export function areaHidden(vNode, visibleFunction) { + /** + * Note: + * - Verified that `map` element cannot refer to `area` elements across different document trees + * - Verified that `map` element does not get affected by altering `display` property + */ + const mapEl = closest(vNode, 'map'); + if (!mapEl) { + return true; + } + + const mapElName = mapEl.attr('name'); + if (!mapElName) { + return true; + } + + /** + * `map` element has to be in light DOM + */ + const mapElRootNode = getRootNode(vNode.actualNode); + if (!mapElRootNode || mapElRootNode.nodeType !== 9) { + return true; + } + + const refs = querySelectorAll( + // TODO: es-module-_tree + axe._tree, + `img[usemap="#${escapeSelector(mapElName)}"]` + ); + if (!refs || !refs.length) { + return true; + } + + return refs.some(ref => !visibleFunction(ref)); +} diff --git a/lib/commons/text/accessible-text-virtual.js b/lib/commons/text/accessible-text-virtual.js index cd423a2f7a..49ea9a621c 100644 --- a/lib/commons/text/accessible-text-virtual.js +++ b/lib/commons/text/accessible-text-virtual.js @@ -5,7 +5,7 @@ import formControlValue from './form-control-value'; import subtreeText from './subtree-text'; import titleText from './title-text'; import sanitize from './sanitize'; -import isVisible from '../dom/is-visible'; +import isVisibleForScreenreader from '../dom/is-visible-for-screenreader'; import isIconLigature from '../text/is-icon-ligature'; /** @@ -93,7 +93,7 @@ function shouldIgnoreHidden(virtualNode, context) { return false; } - return !isVisible(virtualNode, true); + return !isVisibleForScreenreader(virtualNode); } /** @@ -137,7 +137,7 @@ function prepareContext(virtualNode, context) { context.includeHidden === undefined ) { context = { - includeHidden: !isVisible(virtualNode, true), + includeHidden: !isVisibleForScreenreader(virtualNode), ...context }; } diff --git a/lib/commons/text/form-control-value.js b/lib/commons/text/form-control-value.js index 2a4b672649..eac24b770a 100644 --- a/lib/commons/text/form-control-value.js +++ b/lib/commons/text/form-control-value.js @@ -9,7 +9,7 @@ import isAriaListbox from '../forms/is-aria-listbox'; import isAriaCombobox from '../forms/is-aria-combobox'; import isAriaRange from '../forms/is-aria-range'; import getOwnedVirtual from '../aria/get-owned-virtual'; -import isHiddenWithCSS from '../dom/is-hidden-with-css'; +import isHiddenForEveryone from '../dom/is-hidden-for-everyone'; import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; import { getNodeFromTree, querySelectorAll } from '../../core/utils'; import log from '../../core/log'; @@ -130,7 +130,7 @@ function ariaTextboxValue(node) { if (!isAriaTextbox(vNode)) { return ''; } - if (!actualNode || (actualNode && !isHiddenWithCSS(actualNode))) { + if (!actualNode || (actualNode && !isHiddenForEveryone(actualNode))) { return visibleVirtual(vNode, true); } else { return actualNode.textContent; diff --git a/lib/commons/text/visible-text-nodes.js b/lib/commons/text/visible-text-nodes.js index b82b2b0d3e..19a1067ab8 100644 --- a/lib/commons/text/visible-text-nodes.js +++ b/lib/commons/text/visible-text-nodes.js @@ -1,4 +1,4 @@ -import isVisible from '../dom/is-visible'; +import isVisibleOnScreen from '../dom/is-visible-on-screen'; /** * Returns an array of visible text virtual nodes @@ -11,7 +11,7 @@ import isVisible from '../dom/is-visible'; * @deprecated */ function visibleTextNodes(vNode) { - const parentVisible = isVisible(vNode.actualNode); + const parentVisible = isVisibleOnScreen(vNode); let nodes = []; vNode.children.forEach(child => { if (child.actualNode.nodeType === 3) { diff --git a/lib/commons/text/visible-virtual.js b/lib/commons/text/visible-virtual.js index 73a578b38a..3febc49743 100644 --- a/lib/commons/text/visible-virtual.js +++ b/lib/commons/text/visible-virtual.js @@ -1,5 +1,6 @@ import sanitize from './sanitize'; -import isVisible from '../dom/is-visible'; +import isVisibleOnScreen from '../dom/is-visible-on-screen'; +import isVisibleForScreenreader from '../dom/is-visible-for-screenreader'; import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; import { getNodeFromTree } from '../../core/utils'; @@ -20,12 +21,13 @@ import { getNodeFromTree } from '../../core/utils'; function visibleVirtual(element, screenReader, noRecursing) { const vNode = element instanceof AbstractVirtualNode ? element : getNodeFromTree(element); + const visibleMethod = screenReader ? isVisibleForScreenreader : isVisibleOnScreen // if the element does not have an actual node treat it as if // it is visible const visible = !element.actualNode || - (element.actualNode && isVisible(element.actualNode, screenReader)); + (element.actualNode && visibleMethod(element)); const result = vNode.children .map(child => { diff --git a/lib/core/base/context.js b/lib/core/base/context.js index 0b366ce431..4e593175fd 100644 --- a/lib/core/base/context.js +++ b/lib/core/base/context.js @@ -1,6 +1,5 @@ import createFrameContext from './create-frame-context'; import { - isHidden, findBy, getNodeFromTree, getFlattenedTree, @@ -10,6 +9,7 @@ import { respondable, clone } from '../utils'; +import { isVisibleForScreenreader } from '../../commons/dom' /** * Pushes a unique frame onto `frames` array, filtering any hidden iframes @@ -18,7 +18,7 @@ import { * @param {HTMLElement} frame The frame to push onto Context */ function pushUniqueFrame(context, frame) { - if (isHidden(frame) || findBy(context.frames, 'node', frame)) { + if (!isVisibleForScreenreader(frame) || findBy(context.frames, 'node', frame)) { return; } context.frames.push(createFrameContext(frame, context)); diff --git a/lib/core/base/rule.js b/lib/core/base/rule.js index abda44ade1..74a7b347bc 100644 --- a/lib/core/base/rule.js +++ b/lib/core/base/rule.js @@ -8,9 +8,9 @@ import { queue, DqElement, select, - isHidden, assert } from '../utils'; +import { isVisibleForScreenreader } from '../../commons/dom'; import constants from '../constants'; import log from '../log'; @@ -130,8 +130,8 @@ Rule.prototype.matches = function matches() { Rule.prototype.gather = function gather(context, options = {}) { const markStart = 'mark_gather_start_' + this.id; const markEnd = 'mark_gather_end_' + this.id; - const markHiddenStart = 'mark_isHidden_start_' + this.id; - const markHiddenEnd = 'mark_isHidden_end_' + this.id; + const markHiddenStart = 'mark_isVisibleForScreenreader_start_' + this.id; + const markHiddenEnd = 'mark_isVisibleForScreenreader_end_' + this.id; if (options.performanceTimer) { performanceTimer.mark(markStart); @@ -144,13 +144,13 @@ Rule.prototype.gather = function gather(context, options = {}) { } elements = elements.filter(element => { - return !isHidden(element.actualNode); + return isVisibleForScreenreader(element); }); if (options.performanceTimer) { performanceTimer.mark(markHiddenEnd); performanceTimer.measure( - 'rule_' + this.id + '#gather_axe.utils.isHidden', + 'rule_' + this.id + '#gather_axe.utils.isVisibleForScreenreader', markHiddenStart, markHiddenEnd ); diff --git a/lib/core/core.js b/lib/core/core.js index 2aded83b4a..b7690fe860 100644 --- a/lib/core/core.js +++ b/lib/core/core.js @@ -51,6 +51,17 @@ import { getNodesMatchingExpression } from './utils/selector-cache'; import { convertSelector } from './utils/matches'; +import { + nativelyHidden, + displayHidden, + visibilityHidden, + ariaHidden, + opacityHidden, + scrollHidden, + overflowHidden, + clipHidden, + areaHidden +} from '../commons/dom/visibility-methods'; axe.constants = constants; axe.log = log; @@ -78,8 +89,22 @@ axe._thisWillBeDeletedDoNotUse.public = { axe._thisWillBeDeletedDoNotUse.utils = axe._thisWillBeDeletedDoNotUse.utils || {}; axe._thisWillBeDeletedDoNotUse.utils.cacheNodeSelectors = cacheNodeSelectors; -axe._thisWillBeDeletedDoNotUse.utils.getNodesMatchingExpression = getNodesMatchingExpression; +axe._thisWillBeDeletedDoNotUse.utils.getNodesMatchingExpression = + getNodesMatchingExpression; axe._thisWillBeDeletedDoNotUse.utils.convertSelector = convertSelector; +axe._thisWillBeDeletedDoNotUse.commons = + axe._thisWillBeDeletedDoNotUse.commons || {}; +axe._thisWillBeDeletedDoNotUse.commons.dom = + axe._thisWillBeDeletedDoNotUse.commons.dom || {}; +axe._thisWillBeDeletedDoNotUse.commons.dom.nativelyHidden = nativelyHidden; +axe._thisWillBeDeletedDoNotUse.commons.dom.displayHidden = displayHidden; +axe._thisWillBeDeletedDoNotUse.commons.dom.visibilityHidden = visibilityHidden; +axe._thisWillBeDeletedDoNotUse.commons.dom.ariaHidden = ariaHidden; +axe._thisWillBeDeletedDoNotUse.commons.dom.opacityHidden = opacityHidden; +axe._thisWillBeDeletedDoNotUse.commons.dom.scrollHidden = scrollHidden; +axe._thisWillBeDeletedDoNotUse.commons.dom.overflowHidden = overflowHidden; +axe._thisWillBeDeletedDoNotUse.commons.dom.clipHidden = clipHidden; +axe._thisWillBeDeletedDoNotUse.commons.dom.areaHidden = areaHidden; axe.imports = imports; diff --git a/lib/core/utils/is-hidden.js b/lib/core/utils/is-hidden.js index 94ce47f264..61f2bf1a11 100644 --- a/lib/core/utils/is-hidden.js +++ b/lib/core/utils/is-hidden.js @@ -7,6 +7,7 @@ import getNodeFromTree from './get-node-from-tree'; * @param {HTMLElement} el The HTMLElement * @param {Boolean} recursed * @return {Boolean} The element's visibilty status + * @deprecated use isVisibleToScreenreader */ function isHidden(el, recursed) { const node = getNodeFromTree(el); diff --git a/lib/core/utils/pollyfills.js b/lib/core/utils/pollyfills.js index 2afe6d6cd5..fe1de7f354 100644 --- a/lib/core/utils/pollyfills.js +++ b/lib/core/utils/pollyfills.js @@ -6,8 +6,8 @@ - Array.prototype.find */ if (typeof Object.assign !== 'function') { - (function() { - Object.assign = function(target) { + (function () { + Object.assign = function (target) { if (target === undefined || target === null) { throw new TypeError('Cannot convert undefined or null to object'); } @@ -30,7 +30,7 @@ if (typeof Object.assign !== 'function') { if (!Array.prototype.find) { Object.defineProperty(Array.prototype, 'find', { - value: function(predicate) { + value: function (predicate) { if (this === null) { throw new TypeError('Array.prototype.find called on null or undefined'); } @@ -55,7 +55,7 @@ if (!Array.prototype.find) { if (!Array.prototype.findIndex) { Object.defineProperty(Array.prototype, 'findIndex', { - value: function(predicate, thisArg) { + value: function (predicate, thisArg) { if (this === null) { throw new TypeError('Array.prototype.find called on null or undefined'); } @@ -82,7 +82,7 @@ export function pollyfillElementsFromPoint() { if (document.elementsFromPoint) return document.elementsFromPoint; if (document.msElementsFromPoint) return document.msElementsFromPoint; - var usePointer = (function() { + var usePointer = (function () { var element = document.createElement('x'); element.style.cssText = 'pointer-events:auto'; return element.style.pointerEvents === 'auto'; @@ -96,7 +96,7 @@ export function pollyfillElementsFromPoint() { ? '* { pointer-events: all }' : '* { visibility: visible }'; - return function(x, y) { + return function (x, y) { var current, i, d; var elements = []; var previousPointerEvents = []; @@ -153,7 +153,7 @@ if (typeof window.addEventListener === 'function') { if (!Array.prototype.includes) { Object.defineProperty(Array.prototype, 'includes', { - value: function(searchElement) { + value: function (searchElement) { var O = Object(this); var len = parseInt(O.length, 10) || 0; if (len === 0) { @@ -190,7 +190,7 @@ if (!Array.prototype.includes) { // Reference: http://es5.github.io/#x15.4.4.17 if (!Array.prototype.some) { Object.defineProperty(Array.prototype, 'some', { - value: function(fun) { + value: function (fun) { if (this == null) { throw new TypeError('Array.prototype.some called on null or undefined'); } @@ -216,14 +216,14 @@ if (!Array.prototype.some) { if (!Array.from) { Object.defineProperty(Array, 'from', { - value: (function() { + value: (function () { var toStr = Object.prototype.toString; - var isCallable = function(fn) { + var isCallable = function (fn) { return ( typeof fn === 'function' || toStr.call(fn) === '[object Function]' ); }; - var toInteger = function(value) { + var toInteger = function (value) { var number = Number(value); if (isNaN(number)) { return 0; @@ -234,7 +234,7 @@ if (!Array.from) { return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)); }; var maxSafeInteger = Math.pow(2, 53) - 1; - var toLength = function(value) { + var toLength = function (value) { var len = toInteger(value); return Math.min(Math.max(len, 0), maxSafeInteger); }; @@ -307,7 +307,7 @@ if (!Array.from) { } if (!String.prototype.includes) { - String.prototype.includes = function(search, start) { + String.prototype.includes = function (search, start) { if (typeof start !== 'number') { start = 0; } @@ -329,7 +329,7 @@ if (!Array.prototype.flat) { return depth ? Array.prototype.reduce.call( this, - function(acc, cur) { + function (acc, cur) { if (Array.isArray(cur)) { acc.push.apply(acc, flat.call(cur, depth - 1)); } else { @@ -345,3 +345,19 @@ if (!Array.prototype.flat) { writable: true }); } + +// linked from MDN docs on isConnected +// @see https://gist.github.com/eligrey/f109a6d0bf4efe3461201c3d7b745e8f +if (window.Node && !('isConnected' in Node.prototype)) { + Object.defineProperty(Node.prototype, 'isConnected', { + get() { + return ( + !this.ownerDocument || + !( + this.ownerDocument.compareDocumentPosition(this) & + this.DOCUMENT_POSITION_DISCONNECTED + ) + ); + } + }); +} diff --git a/lib/rules/autocomplete-matches.js b/lib/rules/autocomplete-matches.js index 88cb1454ac..9d0657ba31 100644 --- a/lib/rules/autocomplete-matches.js +++ b/lib/rules/autocomplete-matches.js @@ -1,6 +1,6 @@ import { sanitize } from '../commons/text'; import standards from '../standards'; -import { isVisible } from '../commons/dom'; +import { isVisibleForScreenreader, isVisibleOnScreen } from '../commons/dom'; function autocompleteMatches(node, virtualNode) { const autocomplete = virtualNode.attr('autocomplete'); @@ -46,8 +46,8 @@ function autocompleteMatches(node, virtualNode) { if ( tabIndex === '-1' && virtualNode.actualNode && - !isVisible(virtualNode.actualNode, false) && - !isVisible(virtualNode.actualNode, true) + !isVisibleOnScreen(virtualNode) && + !isVisibleForScreenreader(virtualNode) ) { return false; } diff --git a/lib/rules/is-visible-matches.js b/lib/rules/is-visible-matches.js index 6625f7b3bf..9ecb7c827d 100644 --- a/lib/rules/is-visible-matches.js +++ b/lib/rules/is-visible-matches.js @@ -1,5 +1,5 @@ -import { isVisible } from '../commons/dom'; +import { isVisibleOnScreen } from '../commons/dom'; export default function hasVisibleTextMatches(node) { - return isVisible(node, false); + return isVisibleOnScreen(node); } diff --git a/lib/rules/landmark-unique-matches.js b/lib/rules/landmark-unique-matches.js index af5409c408..9f1d87c423 100644 --- a/lib/rules/landmark-unique-matches.js +++ b/lib/rules/landmark-unique-matches.js @@ -1,4 +1,4 @@ -import { findUpVirtual, isVisible } from '../commons/dom'; +import { findUpVirtual, isVisibleForScreenreader } from '../commons/dom'; import { getRole } from '../commons/aria'; import { getAriaRolesByType } from '../commons/standards'; import { accessibleTextVirtual } from '../commons/text'; @@ -44,7 +44,7 @@ function landmarkUniqueMatches(node, virtualNode) { return landmarkRoles.indexOf(role) >= 0 || role === 'region'; } - return isLandmarkVirtual(virtualNode) && isVisible(node, true); + return isLandmarkVirtual(virtualNode) && isVisibleForScreenreader(node); } export default landmarkUniqueMatches; diff --git a/lib/rules/link-in-text-block-matches.js b/lib/rules/link-in-text-block-matches.js index 3ef3a50519..6ef7a0c3cc 100644 --- a/lib/rules/link-in-text-block-matches.js +++ b/lib/rules/link-in-text-block-matches.js @@ -1,5 +1,5 @@ import { sanitize } from '../commons/text'; -import { isVisible, isInTextBlock } from '../commons/dom'; +import { isVisibleOnScreen, isInTextBlock } from '../commons/dom'; function linkInTextBlockMatches(node) { var text = sanitize(node.textContent); @@ -11,7 +11,7 @@ function linkInTextBlockMatches(node) { if (!text) { return false; } - if (!isVisible(node, false)) { + if (!isVisibleOnScreen(node)) { return false; } diff --git a/test/checks/keyboard/accesskeys.js b/test/checks/keyboard/accesskeys.js index cd770f55df..78cf520ec7 100644 --- a/test/checks/keyboard/accesskeys.js +++ b/test/checks/keyboard/accesskeys.js @@ -2,7 +2,7 @@ describe('accesskeys', function() { 'use strict'; var fixture = document.getElementById('fixture'); - + var fixtureSetup = axe.testUtils.fixtureSetup; var checkContext = axe.testUtils.MockCheckContext(); afterEach(function() { @@ -12,6 +12,7 @@ describe('accesskeys', function() { it('should return true and record accesskey', function() { fixture.innerHTML = '
'; + fixtureSetup(); var node = fixture.querySelector('#target'); assert.isTrue(checks.accesskeys.evaluate.call(checkContext, node)); diff --git a/test/checks/label/multiple-label.js b/test/checks/label/multiple-label.js index 55a4118559..ae0e5d673a 100644 --- a/test/checks/label/multiple-label.js +++ b/test/checks/label/multiple-label.js @@ -1,18 +1,19 @@ -describe('multiple-label', function() { +describe('multiple-label', function () { 'use strict'; var fixture = document.getElementById('fixture'); var shadowSupported = axe.testUtils.shadowSupport.v1; var checkContext = axe.testUtils.MockCheckContext(); + var fixtureSetup = axe.testUtils.fixtureSetup; - afterEach(function() { - fixture.innerHTML = ''; + afterEach(function () { checkContext.reset(); }); - it('should return undefined if there are multiple implicit labels', function() { - fixture.innerHTML = - ''; + it('should return undefined if there are multiple implicit labels', function () { + fixtureSetup( + '' + ); var target = fixture.querySelector('#target'); var l1 = fixture.querySelector('#l1'); var l2 = fixture.querySelector('#l2'); @@ -24,9 +25,8 @@ describe('multiple-label', function() { assert.deepEqual(checkContext._relatedNodes, [l1, l2]); }); - it('should return false if there is only one implicit label', function() { - fixture.innerHTML = - ''; + it('should return false if there is only one implicit label', function () { + fixtureSetup(''); var target = fixture.querySelector('#target'); var l1 = fixture.querySelector('#l1'); assert.isFalse( @@ -37,12 +37,13 @@ describe('multiple-label', function() { assert.deepEqual(checkContext._relatedNodes, [l1]); }); - it('should return undefined if there are multiple explicit labels', function() { - fixture.innerHTML = + it('should return undefined if there are multiple explicit labels', function () { + fixtureSetup( '' + - '' + - '' + - ''; + '' + + '' + + '' + ); var target = fixture.querySelector('#target'); var l1 = fixture.querySelector('#l1'); var l2 = fixture.querySelector('#l2'); @@ -55,9 +56,10 @@ describe('multiple-label', function() { assert.deepEqual(checkContext._relatedNodes, [l1, l2, l3]); }); - it('should return false if there is only one explicit label', function() { - fixture.innerHTML = - ''; + it('should return false if there is only one explicit label', function () { + fixtureSetup( + '' + ); var target = fixture.querySelector('#target'); var l1 = fixture.querySelector('#l1'); assert.isFalse( @@ -68,11 +70,12 @@ describe('multiple-label', function() { assert.deepEqual(checkContext._relatedNodes, [l1]); }); - it('should return false if there are multiple explicit labels but one is hidden', function() { - fixture.innerHTML = + it('should return false if there are multiple explicit labels but one is hidden', function () { + fixtureSetup( '' + - '' + - ''; + '' + + '' + ); var target = fixture.querySelector('#test-input2'); var l1 = fixture.querySelector('#l1'); assert.isFalse( @@ -83,12 +86,13 @@ describe('multiple-label', function() { assert.deepEqual(checkContext._relatedNodes, [l1]); }); - it('should return undefined if there are multiple explicit labels but some are hidden', function() { - fixture.innerHTML = + it('should return undefined if there are multiple explicit labels but some are hidden', function () { + fixtureSetup( '' + - '' + - '' + - ''; + '' + + '' + + '' + ); var target = fixture.querySelector('#me'); var l1 = fixture.querySelector('#l1'); var l3 = fixture.querySelector('#l3'); @@ -100,9 +104,10 @@ describe('multiple-label', function() { assert.deepEqual(checkContext._relatedNodes, [l1, l3]); }); - it('should return undefined if there are implicit and explicit labels', function() { - fixture.innerHTML = - ''; + it('should return undefined if there are implicit and explicit labels', function () { + fixtureSetup( + '' + ); var target = fixture.querySelector('#target'); var l1 = fixture.querySelector('#l1'); var l2 = fixture.querySelector('#l2'); @@ -114,9 +119,10 @@ describe('multiple-label', function() { assert.deepEqual(checkContext._relatedNodes, [l1, l2]); }); - it('should return false if there an implicit label uses for attribute', function() { - fixture.innerHTML = - ''; + it('should return false if there an implicit label uses for attribute', function () { + fixtureSetup( + '' + ); var target = fixture.querySelector('#target'); assert.isFalse( axe.testUtils @@ -125,14 +131,16 @@ describe('multiple-label', function() { ); }); - it('should return undefined given multiple labels and no aria-labelledby', function() { - fixture.innerHTML = ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; + it('should return undefined given multiple labels and no aria-labelledby', function () { + fixtureSetup( + '' + + '' + + '' + + '' + + '' + + '' + + '' + ); var target = fixture.querySelector('#A'); assert.isUndefined( axe.testUtils @@ -141,14 +149,16 @@ describe('multiple-label', function() { ); }); - it('should return undefined given multiple labels, one label AT visible, and no aria-labelledby', function() { - fixture.innerHTML = ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; + it('should return undefined given multiple labels, one label AT visible, and no aria-labelledby', function () { + fixtureSetup( + '' + + '' + + '' + + '' + + '' + + '' + + '' + ); var target = fixture.querySelector('#B'); assert.isUndefined( axe.testUtils @@ -157,10 +167,12 @@ describe('multiple-label', function() { ); }); - it('should return false given multiple labels, one label AT visible, and aria-labelledby for AT visible', function() { - fixture.innerHTML = ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; + it('should return false given multiple labels, one label AT visible, and aria-labelledby for AT visible', function () { + fixtureSetup( + '' + + '' + + '' + ); var target = fixture.querySelector('#D'); assert.isFalse( axe.testUtils @@ -169,11 +181,12 @@ describe('multiple-label', function() { ); }); - it('should return false given multiple labels, one label AT visible, and aria-labelledby for all', function() { - fixture.innerHTML = ''; - fixture.innerHTML += - ''; - fixture.innerHTML += ''; + it('should return false given multiple labels, one label AT visible, and aria-labelledby for all', function () { + fixtureSetup( + '' + + '' + + '' + ); var target = fixture.querySelector('#F'); assert.isFalse( axe.testUtils @@ -182,10 +195,12 @@ describe('multiple-label', function() { ); }); - it('should return false given multiple labels, one label visible, and no aria-labelledby', function() { - fixture.innerHTML = ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; + it('should return false given multiple labels, one label visible, and no aria-labelledby', function () { + fixtureSetup( + '' + + '' + + '' + ); var target = fixture.querySelector('#I'); assert.isFalse( axe.testUtils @@ -194,15 +209,16 @@ describe('multiple-label', function() { ); }); - it('should return undefined given multiple labels, all visible, aria-labelledby for all', function() { - fixture.innerHTML = - ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; + it('should return undefined given multiple labels, all visible, aria-labelledby for all', function () { + fixtureSetup( + '' + + '' + + '' + + '' + + '' + + '' + + '' + ); var target = fixture.querySelector('#J'); assert.isUndefined( axe.testUtils @@ -211,10 +227,12 @@ describe('multiple-label', function() { ); }); - it('should return undefined given multiple labels, one AT visible, no aria-labelledby', function() { - fixture.innerHTML = ''; - fixture.innerHTML += ''; - fixture.innerHTML += ''; + it('should return undefined given multiple labels, one AT visible, no aria-labelledby', function () { + fixtureSetup( + '' + + '' + + '' + ); var target = fixture.querySelector('#Q'); assert.isUndefined( axe.testUtils @@ -225,13 +243,14 @@ describe('multiple-label', function() { (shadowSupported ? it : xit)( 'should consider labels in the same document/shadow tree', - function() { + function () { fixture.innerHTML = '
'; var target = document.querySelector('#target'); var shadowRoot = target.attachShadow({ mode: 'open' }); shadowRoot.innerHTML = ''; var shadowTarget = target.shadowRoot; + fixtureSetup(); assert.isFalse( axe.testUtils .getCheckEvaluate('multiple-label') @@ -242,7 +261,7 @@ describe('multiple-label', function() { (shadowSupported ? it : xit)( 'should return false for valid multiple labels in the same document/shadow tree', - function() { + function () { fixture.innerHTML = '
'; var target = document.querySelector('#target'); var shadowRoot = target.attachShadow({ mode: 'open' }); @@ -250,6 +269,7 @@ describe('multiple-label', function() { innerHTML += ''; innerHTML += ''; shadowRoot.innerHTML = innerHTML; + fixtureSetup(); var shadowTarget = target.shadowRoot; assert.isFalse( axe.testUtils @@ -261,7 +281,7 @@ describe('multiple-label', function() { (shadowSupported ? it : xit)( 'should return undefined for invalid multiple labels in the same document/shadow tree', - function() { + function () { fixture.innerHTML = '
'; var target = document.querySelector('#target'); var shadowRoot = target.attachShadow({ mode: 'open' }); @@ -269,6 +289,7 @@ describe('multiple-label', function() { innerHTML += ''; innerHTML += ''; shadowRoot.innerHTML = innerHTML; + fixtureSetup(); var shadowTarget = target.shadowRoot; assert.isUndefined( axe.testUtils diff --git a/test/checks/navigation/skip-link.js b/test/checks/navigation/skip-link.js index 59cf3b9db0..ff48fb285c 100644 --- a/test/checks/navigation/skip-link.js +++ b/test/checks/navigation/skip-link.js @@ -3,10 +3,6 @@ describe('skip-link', function() { var fixture = document.getElementById('fixture'); - afterEach(function() { - fixture.innerHTML = ''; - }); - it('should return true if the href points to an element with an ID', function() { fixture.innerHTML = 'Click Here

Introduction

'; @@ -25,6 +21,7 @@ describe('skip-link', function() { it('should return false if the href points to a non-existent element', function() { fixture.innerHTML = 'Click Here

Introduction

'; + axe._tree = axe.utils.getFlattenedTree(fixture); var node = fixture.querySelector('a'); assert.isFalse(axe.testUtils.getCheckEvaluate('skip-link')(node)); }); @@ -33,6 +30,7 @@ describe('skip-link', function() { fixture.innerHTML = 'Click Here' + '

Introduction

'; + axe._tree = axe.utils.getFlattenedTree(fixture); var node = fixture.querySelector('a'); assert.isUndefined(axe.testUtils.getCheckEvaluate('skip-link')(node)); }); @@ -41,6 +39,7 @@ describe('skip-link', function() { fixture.innerHTML = 'Click Here' + '

Introduction

'; + axe._tree = axe.utils.getFlattenedTree(fixture); var node = fixture.querySelector('a'); assert.isUndefined(axe.testUtils.getCheckEvaluate('skip-link')(node)); }); diff --git a/test/checks/shared/is-on-screen.js b/test/checks/shared/is-on-screen.js index e240edcc1a..55c5f00939 100644 --- a/test/checks/shared/is-on-screen.js +++ b/test/checks/shared/is-on-screen.js @@ -1,34 +1,29 @@ describe('is-on-screen', function() { 'use strict'; - var fixture = document.getElementById('fixture'); + var queryFixture = axe.testUtils.queryFixture; it('should return true for visible elements', function() { - fixture.innerHTML = '
elm
'; - var node = fixture.querySelector('#target'); + var vNode = queryFixture('
elm
'); - assert.isTrue(axe.testUtils.getCheckEvaluate('is-on-screen')(node)); + assert.isTrue(axe.testUtils.getCheckEvaluate('is-on-screen')(vNode)); }); it('should return true for aria-hidden=true elements', function() { - fixture.innerHTML = ''; - var node = fixture.querySelector('#target'); + var vNode = queryFixture(''); - assert.isTrue(axe.testUtils.getCheckEvaluate('is-on-screen')(node)); + assert.isTrue(axe.testUtils.getCheckEvaluate('is-on-screen')(vNode)); }); it('should return false for display:none elements', function() { - fixture.innerHTML = ''; - var node = fixture.querySelector('#target'); + var vNode = queryFixture(''); - assert.isFalse(axe.testUtils.getCheckEvaluate('is-on-screen')(node)); + assert.isFalse(axe.testUtils.getCheckEvaluate('is-on-screen')(vNode)); }); it('should return false for off screen elements', function() { - fixture.innerHTML = - '
elm
'; - var node = fixture.querySelector('#target'); + var vNode = queryFixture( '
elm
'); - assert.isFalse(axe.testUtils.getCheckEvaluate('is-on-screen')(node)); + assert.isFalse(axe.testUtils.getCheckEvaluate('is-on-screen')(vNode)); }); }); diff --git a/test/commons/aria/get-role.js b/test/commons/aria/get-role.js index 166f78e3ba..f0436fe9a0 100644 --- a/test/commons/aria/get-role.js +++ b/test/commons/aria/get-role.js @@ -1,75 +1,75 @@ -describe('aria.getRole', function() { +describe('aria.getRole', function () { 'use strict'; var aria = axe.commons.aria; var flatTreeSetup = axe.testUtils.flatTreeSetup; var fixture = document.querySelector('#fixture'); - afterEach(function() { + afterEach(function () { fixture.innerHTML = ''; }); - it('returns valid roles', function() { + it('returns valid roles', function () { var node = document.createElement('div'); node.setAttribute('role', 'button'); flatTreeSetup(node); assert.equal(aria.getRole(node), 'button'); }); - it('handles case sensitivity', function() { + it('handles case sensitivity', function () { var node = document.createElement('div'); node.setAttribute('role', 'BUTTON'); flatTreeSetup(node); assert.equal(aria.getRole(node), 'button'); }); - it('handles whitespacing', function() { + it('handles whitespacing', function () { var node = document.createElement('div'); node.setAttribute('role', ' button '); flatTreeSetup(node); assert.equal(aria.getRole(node), 'button'); }); - it('returns null when there is no role', function() { + it('returns null when there is no role', function () { var node = document.createElement('div'); flatTreeSetup(node); assert.isNull(aria.getRole(node)); }); - it('returns the explit role if it is valid and non-abstract', function() { + it('returns the explit role if it is valid and non-abstract', function () { var node = document.createElement('li'); node.setAttribute('role', 'menuitem'); flatTreeSetup(node); assert.equal(aria.getRole(node), 'menuitem'); }); - it('returns the implicit role if the explicit is invalid', function() { + it('returns the implicit role if the explicit is invalid', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); var node = fixture.querySelector('#target'); assert.equal(aria.getRole(node), 'listitem'); }); - it('ignores fallback roles by default', function() { + it('ignores fallback roles by default', function () { var node = document.createElement('div'); node.setAttribute('role', 'spinbutton button'); flatTreeSetup(node); assert.isNull(aria.getRole(node)); }); - it('accepts virtualNode objects', function() { + it('accepts virtualNode objects', function () { var node = document.createElement('div'); node.setAttribute('role', 'button'); var vNode = flatTreeSetup(node)[0]; assert.equal(aria.getRole(vNode), 'button'); }); - it('returns null if the node is not an element', function() { + it('returns null if the node is not an element', function () { var node = document.createTextNode('foo bar baz'); flatTreeSetup(node); assert.isNull(aria.getRole(node)); }); - it('runs role resolution with role=none', function() { + it('runs role resolution with role=none', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); @@ -77,7 +77,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'listitem'); }); - it('runs role resolution with role=presentation', function() { + it('runs role resolution with role=presentation', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); @@ -85,15 +85,15 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'listitem'); }); - it('handles focusable element with role="none"', function() { - var node = document.createElement('button'); - node.setAttribute('role', 'none'); - flatTreeSetup(node); + it('handles focusable element with role="none"', function () { + fixture.innerHTML = ''; + flatTreeSetup(fixture); + var node = fixture.querySelector('#target'); assert.equal(aria.getRole(node), 'button'); }); - describe('presentational role inheritance', function() { - it('handles presentation role inheritance for ul', function() { + describe('presentational role inheritance', function () { + it('handles presentation role inheritance for ul', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); @@ -101,7 +101,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('handles presentation role inheritance for ol', function() { + it('handles presentation role inheritance for ol', function () { fixture.innerHTML = '
  1. foo
'; flatTreeSetup(fixture); @@ -109,7 +109,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('handles presentation role inheritance for dt', function() { + it('handles presentation role inheritance for dt', function () { fixture.innerHTML = '
foo
bar>
'; flatTreeSetup(fixture); @@ -117,7 +117,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('handles presentation role inheritance for dd', function() { + it('handles presentation role inheritance for dd', function () { fixture.innerHTML = '
foo
bar>
'; flatTreeSetup(fixture); @@ -125,7 +125,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('handles presentation role inheritance for dt with div wrapper', function() { + it('handles presentation role inheritance for dt with div wrapper', function () { fixture.innerHTML = '
foo
bar>
'; flatTreeSetup(fixture); @@ -133,7 +133,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('handles presentation role inheritance for dd with div wrapper', function() { + it('handles presentation role inheritance for dd with div wrapper', function () { fixture.innerHTML = '
foo
bar>
'; flatTreeSetup(fixture); @@ -141,7 +141,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('handles presentation role inheritance for thead', function() { + it('handles presentation role inheritance for thead', function () { fixture.innerHTML = '
higoodbye
hifoo
'; flatTreeSetup(fixture); @@ -149,7 +149,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('handles presentation role inheritance for td', function() { + it('handles presentation role inheritance for td', function () { fixture.innerHTML = '
higoodbye
hifoo
'; flatTreeSetup(fixture); @@ -157,7 +157,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('handles presentation role inheritance for th', function() { + it('handles presentation role inheritance for th', function () { fixture.innerHTML = '
higoodbye
hifoo
'; flatTreeSetup(fixture); @@ -165,7 +165,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('handles presentation role inheritance for tbody', function() { + it('handles presentation role inheritance for tbody', function () { fixture.innerHTML = '
higoodbye
hifoo
'; flatTreeSetup(fixture); @@ -173,7 +173,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('handles presentation role inheritance for tr', function() { + it('handles presentation role inheritance for tr', function () { fixture.innerHTML = '
higoodbye
hifoo
'; flatTreeSetup(fixture); @@ -181,7 +181,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('handles presentation role inheritance for tfoot', function() { + it('handles presentation role inheritance for tfoot', function () { fixture.innerHTML = '
higoodbye
hifoo
'; flatTreeSetup(fixture); @@ -189,7 +189,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('returns implicit role for presentation role inheritance if ancestor is not the required ancestor', function() { + it('returns implicit role for presentation role inheritance if ancestor is not the required ancestor', function () { fixture.innerHTML = '
  • foo
'; flatTreeSetup(fixture); @@ -197,7 +197,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'listitem'); }); - it('does not override explicit role with presentation role inheritance', function() { + it('does not override explicit role with presentation role inheritance', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); @@ -205,7 +205,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'listitem'); }); - it('does not continue presentation role with explicit role in between', function() { + it('does not continue presentation role with explicit role in between', function () { fixture.innerHTML = '
foo
'; flatTreeSetup(fixture); @@ -213,7 +213,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'cell'); }); - it('handles presentation role inheritance with invalid role in between', function() { + it('handles presentation role inheritance with invalid role in between', function () { fixture.innerHTML = '
foo
'; flatTreeSetup(fixture); @@ -221,7 +221,7 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'presentation'); }); - it('does not continue presentation role through nested layers', function() { + it('does not continue presentation role through nested layers', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); @@ -229,32 +229,32 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node), 'listitem'); }); - it('throws an error if the tree is incomplete', function() { + it('throws an error if the tree is incomplete', function () { fixture.innerHTML = ''; var node = fixture.querySelector('#target'); flatTreeSetup(node); - assert.throws(function() { + assert.throws(function () { aria.getRole(node); }); }); }); - describe('noImplicit', function() { - it('returns the implicit role by default', function() { + describe('noImplicit', function () { + it('returns the implicit role by default', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); var node = fixture.querySelector('#target'); assert.equal(aria.getRole(node), 'listitem'); }); - it('returns null rather than the implicit role with `noImplicit: true`', function() { + it('returns null rather than the implicit role with `noImplicit: true`', function () { var node = document.createElement('li'); flatTreeSetup(node); assert.isNull(aria.getRole(node, { noImplicit: true })); }); - it('does not do role resolution if noImplicit: true', function() { + it('does not do role resolution if noImplicit: true', function () { var node = document.createElement('li'); node.setAttribute('role', 'none'); node.setAttribute('aria-label', 'foo'); @@ -262,14 +262,14 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node, { noImplicit: true }), null); }); - it('still returns the explicit role', function() { + it('still returns the explicit role', function () { var node = document.createElement('li'); node.setAttribute('role', 'button'); flatTreeSetup(node); assert.equal(aria.getRole(node, { noImplicit: true }), 'button'); }); - it('returns the implicit role with `noImplicit: false`', function() { + it('returns the implicit role with `noImplicit: false`', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); var node = fixture.querySelector('#target'); @@ -277,22 +277,22 @@ describe('aria.getRole', function() { }); }); - describe('abstracts', function() { - it('ignores abstract roles by default', function() { + describe('abstracts', function () { + it('ignores abstract roles by default', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); var node = fixture.querySelector('#target'); assert.equal(aria.getRole(node), 'listitem'); }); - it('returns abstract roles with `abstracts: true`', function() { + it('returns abstract roles with `abstracts: true`', function () { var node = document.createElement('li'); node.setAttribute('role', 'section'); flatTreeSetup(node); assert.equal(aria.getRole(node, { abstracts: true }), 'section'); }); - it('does not returns abstract roles with `abstracts: false`', function() { + it('does not returns abstract roles with `abstracts: false`', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); var node = fixture.querySelector('#target'); @@ -300,22 +300,22 @@ describe('aria.getRole', function() { }); }); - describe('dpub', function() { - it('ignores DPUB roles by default', function() { + describe('dpub', function () { + it('ignores DPUB roles by default', function () { var node = document.createElement('section'); node.setAttribute('role', 'doc-chapter'); flatTreeSetup(node); assert.isNull(aria.getRole(node)); }); - it('returns DPUB roles with `dpub: true`', function() { + it('returns DPUB roles with `dpub: true`', function () { var node = document.createElement('section'); node.setAttribute('role', 'doc-chapter'); flatTreeSetup(node); assert.equal(aria.getRole(node, { dpub: true }), 'doc-chapter'); }); - it('does not returns DPUB roles with `dpub: false`', function() { + it('does not returns DPUB roles with `dpub: false`', function () { var node = document.createElement('section'); node.setAttribute('role', 'doc-chapter'); flatTreeSetup(node); @@ -323,29 +323,29 @@ describe('aria.getRole', function() { }); }); - describe('fallback', function() { - it('returns the first valid item in the list', function() { + describe('fallback', function () { + it('returns the first valid item in the list', function () { var node = document.createElement('div'); node.setAttribute('role', 'link button'); flatTreeSetup(node); assert.equal(aria.getRole(node, { fallback: true }), 'link'); }); - it('skips over invalid roles', function() { + it('skips over invalid roles', function () { var node = document.createElement('div'); node.setAttribute('role', 'foobar button'); flatTreeSetup(node); assert.equal(aria.getRole(node, { fallback: true }), 'button'); }); - it('returns the null if all roles are invalid and there is no implicit role', function() { + it('returns the null if all roles are invalid and there is no implicit role', function () { var node = document.createElement('div'); node.setAttribute('role', 'foo bar baz'); flatTreeSetup(node); assert.isNull(aria.getRole(node, { fallback: true })); }); - it('respects the defaults', function() { + it('respects the defaults', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); @@ -353,14 +353,14 @@ describe('aria.getRole', function() { assert.equal(aria.getRole(node, { fallback: true }), 'listitem'); }); - it('respect the `noImplicit` option', function() { + it('respect the `noImplicit` option', function () { var node = document.createElement('li'); node.setAttribute('role', 'doc-chapter section'); flatTreeSetup(node); assert.isNull(aria.getRole(node, { fallback: true, noImplicit: true })); }); - it('respect the `abstracts` option', function() { + it('respect the `abstracts` option', function () { var node = document.createElement('li'); node.setAttribute('role', 'doc-chapter section'); flatTreeSetup(node); @@ -370,7 +370,7 @@ describe('aria.getRole', function() { ); }); - it('respect the `dpub` option', function() { + it('respect the `dpub` option', function () { var node = document.createElement('li'); node.setAttribute('role', 'doc-chapter section'); flatTreeSetup(node); @@ -381,8 +381,8 @@ describe('aria.getRole', function() { }); }); - describe('noPresentational is honored', function() { - it('handles no inheritance role = presentation', function() { + describe('noPresentational is honored', function () { + it('handles no inheritance role = presentation', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); @@ -390,7 +390,7 @@ describe('aria.getRole', function() { assert.isNull(aria.getRole(node, { noPresentational: true })); }); - it('handles inheritance role = presentation', function() { + it('handles inheritance role = presentation', function () { fixture.innerHTML = ''; flatTreeSetup(fixture); @@ -398,14 +398,14 @@ describe('aria.getRole', function() { assert.isNull(aria.getRole(node, { noPresentational: true })); }); - it('handles implicit role', function() { + it('handles implicit role', function () { var node = document.createElement('img'); node.setAttribute('alt', ''); flatTreeSetup(node); assert.isNull(aria.getRole(node, { noPresentational: true })); }); - it('handles role = none', function() { + it('handles role = none', function () { var node = document.createElement('div'); node.setAttribute('role', 'none'); flatTreeSetup(node); diff --git a/test/commons/dom/is-hidden-for-everyone.js b/test/commons/dom/is-hidden-for-everyone.js new file mode 100644 index 0000000000..655c08e1fa --- /dev/null +++ b/test/commons/dom/is-hidden-for-everyone.js @@ -0,0 +1,299 @@ +describe('dom.isHiddenForEveryone', function () { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var shadowSupported = axe.testUtils.shadowSupport.v1; + var isHiddenForEveryone = axe.commons.dom.isHiddenForEveryone; + var queryFixture = axe.testUtils.queryFixture; + + function createContentSlotted(mainProps, targetProps) { + var group = document.createElement('div'); + group.innerHTML = + '

'; + return group; + } + + function makeShadowTree(node, mainProps, targetProps) { + var root = node.attachShadow({ mode: 'open' }); + var node = createContentSlotted(mainProps, targetProps); + root.appendChild(node); + } + + it('should return false on static-positioned, visible element', function () { + var vNode = queryFixture('
I am visible
'); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + it('should return true on static-positioned, hidden element', function () { + var vNode = queryFixture( + '' + ); + var actual = isHiddenForEveryone(vNode); + assert.isTrue(actual); + }); + + it('should return false on absolutely positioned elements that are on-screen', function () { + var vNode = queryFixture( + '
I am visible
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + it('should return false for off-screen and aria-hidden element', function () { + var vNode = queryFixture( + '' + ); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + it('should return false on fixed position elements that are on-screen', function () { + var vNode = queryFixture( + '
I am visible
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + it('should return false for off-screen absolutely positioned element', function () { + var vNode = queryFixture( + '
I am visible
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + it('should return false for off-screen fixed positioned element', function () { + var vNode = queryFixture( + '
I am visible
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + it('should return true on detached elements', function () { + var el = document.createElement('div'); + el.innerHTML = 'I am not visible because I am detached!'; + axe.testUtils.flatTreeSetup(el); + var actual = isHiddenForEveryone(el); + assert.isTrue(actual); + }); + + it('should return false on body', function () { + axe.testUtils.flatTreeSetup(document.body); + var actual = isHiddenForEveryone(document.body); + assert.isFalse(actual); + }); + + it('should return false on html', function () { + axe.testUtils.flatTreeSetup(document.documentElement); + var actual = isHiddenForEveryone(document.documentElement); + assert.isFalse(actual); + }); + + it('should return false if static-position but top/left is set', function () { + var vNode = queryFixture( + '
I am visible
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + it('should return false, and not be affected by `aria-hidden`', function () { + var vNode = queryFixture( + '' + ); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + it('should return true for STYLE node', function () { + var vNode = queryFixture( + "" + ); + var actual = isHiddenForEveryone(vNode); + assert.isTrue(actual); + }); + + it('should return true for SCRIPT node', function () { + var vNode = queryFixture( + "" + ); + var actual = isHiddenForEveryone(vNode); + assert.isTrue(actual); + }); + + it('should return true for if parent of element set to `display:none`', function () { + var vNode = queryFixture( + '
' + + '
' + + '

I am not visible

' + + '
' + + '
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isTrue(actual); + }); + + it('should return false for if parent of element set to `display:block`', function () { + var vNode = queryFixture( + '
' + + '
' + + '

I am visible

' + + '
' + + '
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + // `visibility` test + it('should return true for element that has `visibility:hidden`', function () { + var vNode = queryFixture( + '' + ); + var actual = isHiddenForEveryone(vNode); + assert.isTrue(actual); + }); + + it('should return false and compute how `visibility` of self and parent is configured', function () { + var vNode = queryFixture( + '
' + + '
' + + '
I am visible
' + + '
' + + '
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + it('should return false and compute how `visibility` of self and parent is configured', function () { + var vNode = queryFixture( + '
' + + '
' + + '
I am visible
' + + '
' + + '
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + it('should return true and as parent is set to `visibility:hidden`', function () { + var vNode = queryFixture( + '
' + + '
' + + '
I am not visible
' + + '
' + + '
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isTrue(actual); + }); + + // mixing display and visibility + it('should return true and compute using both `display` and `visibility` set on element and parent(s)', function () { + var vNode = queryFixture( + '
' + + '
' + + '
I am not visible
' + + '
' + + '
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isTrue(actual); + }); + + it('should return false and compute using both `display` and `visibility` set on element and parent(s)', function () { + var vNode = queryFixture( + '
' + + '
' + + '
I am visible
' + + '
' + + '
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + + it('should return true and compute using both `display` and `visibility` set on element and parent(s)', function () { + var vNode = queryFixture( + '
' + + '
' + + '' + + '
' + + '
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isTrue(actual); + }); + + it('should return true and compute using both `display` and `visibility` set on element and parent(s)', function () { + var vNode = queryFixture( + '
' + + '
' + + '
I am not visible
' + + '
' + + '
' + ); + var actual = isHiddenForEveryone(vNode); + assert.isTrue(actual); + }); + + (shadowSupported ? it : it.skip)( + 'should return true if `display:none` inside shadowDOM', + function () { + fixture.innerHTML = '
'; + makeShadowTree(fixture.firstChild, 'display:none;', ''); + var tree = axe.utils.getFlattenedTree(fixture.firstChild); + var vNode = axe.utils.querySelectorAll(tree, 'p')[0]; + var actual = isHiddenForEveryone(vNode); + assert.isTrue(actual); + } + ); + + (shadowSupported ? it : xit)( + 'should return true as parent shadowDOM host is set to `visibility:hidden`', + function () { + fixture.innerHTML = '
'; + makeShadowTree(fixture.firstChild, 'visibility:hidden', ''); + var tree = axe.utils.getFlattenedTree(fixture.firstChild); + var vNode = axe.utils.querySelectorAll(tree, 'p')[0]; + var actual = isHiddenForEveryone(vNode); + assert.isTrue(actual); + } + ); + + (shadowSupported ? it : xit)( + 'should return false as parent shadowDOM host set to `visibility:hidden` is overriden', + function () { + fixture.innerHTML = '
'; + makeShadowTree( + fixture.firstChild, + 'visibility:hidden', + 'visibility:visible' + ); + var tree = axe.utils.getFlattenedTree(fixture.firstChild); + var vNode = axe.utils.querySelectorAll(tree, 'p')[0]; + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + } + ); + + describe('SerialVirtualNode', function () { + it('should return false on detached virtual nodes', function () { + var vNode = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + var actual = isHiddenForEveryone(vNode); + assert.isFalse(actual); + }); + }); +}); diff --git a/test/commons/dom/is-offscreen.js b/test/commons/dom/is-offscreen.js index 282f2e79de..e755eda1a2 100644 --- a/test/commons/dom/is-offscreen.js +++ b/test/commons/dom/is-offscreen.js @@ -1,14 +1,14 @@ -describe('dom.isOffscreen', function() { +describe('dom.isOffscreen', function () { 'use strict'; var fixture = document.getElementById('fixture'); var shadowSupport = axe.testUtils.shadowSupport; - afterEach(function() { + afterEach(function () { fixture.innerHTML = ''; document.body.style.direction = 'ltr'; }); - it('should detect elements positioned outside the left edge', function() { + it('should detect elements positioned outside the left edge', function () { fixture.innerHTML = '
Offscreen?
'; var el = document.getElementById('target'); @@ -16,7 +16,7 @@ describe('dom.isOffscreen', function() { assert.isTrue(axe.commons.dom.isOffscreen(el)); }); - it('should detect elements positioned to but not beyond the left edge', function() { + it('should detect elements positioned to but not beyond the left edge', function () { fixture.innerHTML = '
Offscreen?
'; var el = document.getElementById('target'); @@ -24,7 +24,7 @@ describe('dom.isOffscreen', function() { assert.isTrue(axe.commons.dom.isOffscreen(el)); }); - it('should not detect elements at the left edge with a zero width', function() { + it('should not detect elements at the left edge with a zero width', function () { fixture.innerHTML = '
'; var el = document.getElementById('target'); @@ -32,14 +32,14 @@ describe('dom.isOffscreen', function() { assert.isFalse(axe.commons.dom.isOffscreen(el)); }); - it('should detect elements positioned outside the top edge', function() { + it('should detect elements positioned outside the top edge', function () { fixture.innerHTML = '
Offscreen?
'; var el = document.getElementById('target'); assert.isTrue(axe.commons.dom.isOffscreen(el)); }); - it('should never detect elements positioned outside the bottom edge', function() { + it('should never detect elements positioned outside the bottom edge', function () { fixture.innerHTML = '
Offscreen?
'; var el = document.getElementById('target'); @@ -47,7 +47,7 @@ describe('dom.isOffscreen', function() { assert.isFalse(axe.commons.dom.isOffscreen(el)); }); - it('should detect elements positioned that bleed inside the left edge', function() { + it('should detect elements positioned that bleed inside the left edge', function () { fixture.innerHTML = '
Offscreen?
'; var el = document.getElementById('target'); @@ -55,7 +55,7 @@ describe('dom.isOffscreen', function() { assert.isFalse(axe.commons.dom.isOffscreen(el)); }); - it('should detect elements positioned outside the right edge', function() { + it('should detect elements positioned outside the right edge', function () { fixture.innerHTML = '
Offscreen?
'; var el = document.getElementById('target'); @@ -63,7 +63,7 @@ describe('dom.isOffscreen', function() { assert.isFalse(axe.commons.dom.isOffscreen(el)); }); - it('should detect elements positioned outside the top edge', function() { + it('should detect elements positioned outside the top edge', function () { fixture.innerHTML = '
Offscreen?
'; var el = document.getElementById('target'); @@ -71,7 +71,7 @@ describe('dom.isOffscreen', function() { assert.isFalse(axe.commons.dom.isOffscreen(el)); }); - it('should detect elements positioned outside the bottom edge', function() { + it('should detect elements positioned outside the bottom edge', function () { fixture.innerHTML = '
Offscreen?
'; var el = document.getElementById('target'); @@ -79,7 +79,7 @@ describe('dom.isOffscreen', function() { assert.isFalse(axe.commons.dom.isOffscreen(el)); }); - it('should detect elements that are made off-screen by a parent', function() { + it('should detect elements that are made off-screen by a parent', function () { fixture.innerHTML = '
' + '
Offscreen?
' + @@ -90,7 +90,7 @@ describe('dom.isOffscreen', function() { assert.isTrue(axe.commons.dom.isOffscreen(el)); }); - it('should NOT detect elements positioned outside the right edge on LTR documents', function() { + it('should NOT detect elements positioned outside the right edge on LTR documents', function () { fixture.innerHTML = '
Offscreen?
'; var el = document.getElementById('target'); @@ -98,7 +98,7 @@ describe('dom.isOffscreen', function() { assert.isFalse(axe.commons.dom.isOffscreen(el)); }); - it('should detect elements positioned outside the right edge on RTL documents', function() { + it('should detect elements positioned outside the right edge on RTL documents', function () { document.body.style.direction = 'rtl'; fixture.innerHTML = '
Offscreen?
'; @@ -107,7 +107,7 @@ describe('dom.isOffscreen', function() { assert.isTrue(axe.commons.dom.isOffscreen(el)); }); - it('should NOT detect elements positioned outside the left edge on RTL documents', function() { + it('should NOT detect elements positioned outside the left edge on RTL documents', function () { document.body.style.direction = 'rtl'; fixture.innerHTML = '
Offscreen?
'; @@ -116,7 +116,7 @@ describe('dom.isOffscreen', function() { assert.isFalse(axe.commons.dom.isOffscreen(el)); }); - it('should not detect elements positioned because of a scroll', function() { + it('should not detect elements positioned because of a scroll', function () { fixture.innerHTML = '
' + '
goobye
' + @@ -130,9 +130,13 @@ describe('dom.isOffscreen', function() { assert.isFalse(axe.commons.dom.isOffscreen(viz)); }); + it('should return undefined if actual ndoe is undefined', function () { + assert.isUndefined(axe.commons.dom.isOffscreen()); + }); + (shadowSupport.v1 ? it : xit)( 'should detect on screen shadow nodes', - function() { + function () { fixture.innerHTML = '
'; var shadow = fixture.querySelector('div').attachShadow({ mode: 'open' }); shadow.innerHTML = '
Offscreen?
'; @@ -144,7 +148,7 @@ describe('dom.isOffscreen', function() { (shadowSupport.v1 ? it : xit)( 'should detect off screen shadow nodes', - function() { + function () { fixture.innerHTML = '
'; var shadow = fixture.querySelector('div').attachShadow({ mode: 'open' }); shadow.innerHTML = diff --git a/test/commons/dom/is-visible-for-screenreader.js b/test/commons/dom/is-visible-for-screenreader.js new file mode 100644 index 0000000000..9c5e314412 --- /dev/null +++ b/test/commons/dom/is-visible-for-screenreader.js @@ -0,0 +1,234 @@ +describe('dom.isVisibleForScreenreader', function () { + 'use strict'; + + var fixture = document.querySelector('#fixture'); + var queryFixture = axe.testUtils.queryFixture; + var shadowSupported = axe.testUtils.shadowSupport.v1; + var isVisibleForScreenreader = axe.commons.dom.isVisibleForScreenreader; + + function createContentHidden() { + var group = document.createElement('div'); + group.innerHTML = + ''; + return group; + } + + function makeShadowTreeHidden(node) { + var root = node.attachShadow({ mode: 'open' }); + var div = document.createElement('div'); + div.className = 'parent'; + root.appendChild(div); + div.appendChild(createContentHidden()); + } + + it('should return false on detached elements', function () { + var el = document.createElement('div'); + el.innerHTML = 'I am not visible because I am detached!'; + axe.testUtils.flatTreeSetup(el); + assert.isFalse(isVisibleForScreenreader(el)); + }); + + it('should return true on body', function () { + axe.testUtils.flatTreeSetup(document.body); + var actual = isVisibleForScreenreader(document.body); + assert.isTrue(actual); + }); + + it('should return true on html', function () { + axe.testUtils.flatTreeSetup(document.documentElement); + var actual = isVisibleForScreenreader(document.documentElement); + assert.isTrue(actual); + }); + + it('should return true for visible element', function () { + var vNode = queryFixture('
Visible
'); + assert.isTrue(isVisibleForScreenreader(vNode)); + }); + + it('should return true for visible area element', function () { + var vNode = queryFixture( + '' + + '' + + '' + + '' + ); + assert.isTrue(isVisibleForScreenreader(vNode)); + }); + + it('should return false if `aria-hidden` is set', function () { + var vNode = queryFixture( + '' + ); + assert.isFalse(isVisibleForScreenreader(vNode)); + }); + + it('should return false if `display: none` is set', function () { + var vNode = queryFixture( + '' + ); + assert.isFalse(isVisibleForScreenreader(vNode)); + }); + + it('should return false if `aria-hidden` is set on parent', function () { + var vNode = queryFixture( + '' + ); + assert.isFalse(isVisibleForScreenreader(vNode)); + }); + + it('should know how `visibility` works', function () { + var vNode = queryFixture( + '
' + + '
Hi
' + + '
' + ); + assert.isTrue(isVisibleForScreenreader(vNode)); + }); + + it('returns false for `AREA` without closest `MAP` element', function () { + var vNode = queryFixture( + '' + ); + var actual = isVisibleForScreenreader(vNode); + assert.isFalse(actual); + }); + + it('returns false for `AREA` with closest `MAP` with no name attribute', function () { + var vNode = queryFixture( + '' + + '' + + '' + ); + var actual = isVisibleForScreenreader(vNode); + assert.isFalse(actual); + }); + + (shadowSupported ? it : xit)( + 'returns false for `AREA` element that is inside shadowDOM', + function () { + fixture.innerHTML = '
'; + var container = fixture.querySelector('#container'); + var shadow = container.attachShadow({ mode: 'open' }); + shadow.innerHTML = + '' + + '' + + ''; + axe.testUtils.flatTreeSetup(fixture); + + var target = shadow.querySelector('#target'); + var actual = isVisibleForScreenreader(target); + assert.isFalse(actual); + } + ); + + it('returns false for `AREA` with closest `MAP` with name but not referred by an `IMG` usemap attribute', function () { + var vNode = queryFixture( + '' + + '' + + '' + + 'MDN infographic' + ); + var actual = isVisibleForScreenreader(vNode); + assert.isFalse(actual); + }); + + it('returns false for `AREA` with `MAP` and used in `IMG` which is not visible', function () { + var vNode = queryFixture( + '' + + '' + + '' + + 'MDN infographic' + ); + var actual = isVisibleForScreenreader(vNode); + assert.isFalse(actual); + }); + + it('returns true for `AREA` with `MAP` and used in `IMG` which is visible', function () { + var vNode = queryFixture( + '' + + '' + + '' + + 'MDN infographic' + ); + var actual = isVisibleForScreenreader(vNode); + assert.isTrue(actual); + }); + + (shadowSupported ? it : xit)( + 'not hidden: should work when the element is inside shadow DOM', + function () { + var tree, node; + // shadow DOM v1 - note: v0 is compatible with this code, so no need + // to specifically test this + fixture.innerHTML = '
'; + makeShadowTreeHidden(fixture.firstChild); + tree = axe.utils.getFlattenedTree(fixture.firstChild); + node = axe.utils.querySelectorAll(tree, 'input')[0]; + assert.isTrue(isVisibleForScreenreader(node)); + } + ); + + (shadowSupported ? it : xit)( + 'hidden: should work when the element is inside shadow DOM', + function () { + var tree, node; + // shadow DOM v1 - note: v0 is compatible with this code, so no need + // to specifically test this + fixture.innerHTML = '
'; + makeShadowTreeHidden(fixture.firstChild); + tree = axe.utils.getFlattenedTree(fixture.firstChild); + node = axe.utils.querySelectorAll(tree, 'input')[0]; + assert.isFalse(isVisibleForScreenreader(node)); + } + ); + + (shadowSupported ? it : xit)( + 'should work with hidden slotted elements', + function () { + function createContentSlotted() { + var group = document.createElement('div'); + group.innerHTML = + ''; + return group; + } + function makeShadowTree(node) { + var root = node.attachShadow({ mode: 'open' }); + var div = document.createElement('div'); + root.appendChild(div); + div.appendChild(createContentSlotted()); + } + fixture.innerHTML = '

hello

'; + makeShadowTree(fixture.firstChild); + var tree = axe.utils.getFlattenedTree(fixture.firstChild); + var vNode = axe.utils.querySelectorAll(tree, 'a')[0]; + assert.isFalse(isVisibleForScreenreader(vNode)); + } + ); + + describe('SerialVirtualNode', function () { + it('should return false if `aria-hidden` is set', function () { + var vNode = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + 'aria-hidden': true + } + }); + assert.isFalse(isVisibleForScreenreader(vNode)); + }); + + it('should return false if `aria-hidden` is set on parent', function () { + var vNode = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + var parentVNode = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + 'aria-hidden': true + } + }); + parentVNode.children = [vNode]; + vNode.parent = parentVNode; + assert.isFalse(isVisibleForScreenreader(vNode)); + }); + }); +}); diff --git a/test/commons/dom/is-visible-on-screen.js b/test/commons/dom/is-visible-on-screen.js new file mode 100644 index 0000000000..12b3f29ea3 --- /dev/null +++ b/test/commons/dom/is-visible-on-screen.js @@ -0,0 +1,434 @@ +describe('dom.isVisibleOnScreen', function () { + 'use strict'; + + var fixture = document.querySelector('#fixture'); + var queryFixture = axe.testUtils.queryFixture; + var isIE11 = axe.testUtils.isIE11; + var shadowSupported = axe.testUtils.shadowSupport.v1; + var isVisibleOnScreen = axe.commons.dom.isVisibleOnScreen; + + it('should return true on statically-positioned, visible elements', function () { + var vNode = queryFixture('
Hello!
'); + + assert.isTrue(isVisibleOnScreen(vNode)); + }); + + it('should return true on absolutely positioned elements that are on-screen', function () { + var vNode = queryFixture( + '
hi
' + ); + + assert.isTrue(isVisibleOnScreen(vNode)); + }); + + it('should respect position: fixed', function () { + var vNode = queryFixture( + '
StickySticky
' + ); + + assert.isTrue(isVisibleOnScreen(vNode)); + }); + + it('should properly calculate offsets according the offsetParent', function () { + var vNode = queryFixture( + '
' + + '
Hi
' + + '
' + ); + assert.isTrue(isVisibleOnScreen(vNode)); + }); + + it('should return false if moved offscreen with left', function () { + var vNode = queryFixture( + '
Hi
' + ); + assert.isFalse(isVisibleOnScreen(vNode)); + }); + + it('should return false if moved offscreen with top', function () { + var vNode = queryFixture( + '
Hi
' + ); + assert.isFalse(isVisibleOnScreen(vNode)); + }); + + it('should return false on detached elements', function () { + var el = document.createElement('div'); + el.innerHTML = 'I am not visible because I am detached!'; + axe.testUtils.flatTreeSetup(el); + assert.isFalse(isVisibleOnScreen(el)); + }); + + it('should return true on body', function () { + axe.testUtils.flatTreeSetup(document.body); + var actual = isVisibleOnScreen(document.body); + assert.isTrue(actual); + }); + + it('should return true on html', function () { + axe.testUtils.flatTreeSetup(document.documentElement); + var actual = isVisibleOnScreen(document.documentElement); + assert.isTrue(actual); + }); + + it('should return false on STYLE tag', function () { + var vNode = queryFixture( + '' + ); + var actual = isVisibleOnScreen(vNode); + assert.isFalse(actual); + }); + + it('should return false on NOSCRIPT tag', function () { + var vNode = queryFixture( + '' + ); + var actual = isVisibleOnScreen(vNode); + assert.isFalse(actual); + }); + + it('should return false on TEMPLATE tag', function () { + var vNode = queryFixture( + '' + ); + var actual = isVisibleOnScreen(vNode); + assert.isFalse(actual); + }); + + it('should return true if positioned statically but top/left is set', function () { + var vNode = queryFixture( + '
Hi
' + ); + assert.isTrue(isVisibleOnScreen(vNode)); + }); + + it('should not be affected by `aria-hidden`', function () { + var vNode = queryFixture( + '' + ); + + assert.isTrue(isVisibleOnScreen(vNode)); + }); + + it('should not calculate position on parents', function () { + var vNode = queryFixture( + '
' + + '
Hi
' + + '
' + ); + + assert.isTrue(isVisibleOnScreen(vNode)); + }); + + it('should know how `visibility` works', function () { + var vNode = queryFixture( + '
' + + '
Hi
' + + '
' + ); + + assert.isTrue(isVisibleOnScreen(vNode)); + }); + + it('should detect clip rect hidden text technique', function () { + var clip = + 'clip: rect(1px 1px 1px 1px);' + + 'clip: rect(1px, 1px, 1px, 1px);' + + 'width: 1px; height: 1px;' + + 'position: absolute;' + + 'overflow: hidden;'; + + var vNode = queryFixture('
Hi
'); + + assert.isFalse(isVisibleOnScreen(vNode)); + }); + + it('should detect clip rect hidden text technique using position: fixed', function () { + var clip = + 'clip: rect(1px 1px 1px 1px);' + + 'clip: rect(1px, 1px, 1px, 1px);' + + 'width: 1px; height: 1px;' + + 'position: fixed;' + + 'overflow: hidden;'; + + var vNode = queryFixture('
Hi
'); + + assert.isFalse(isVisibleOnScreen(vNode)); + }); + + it('should detect when clip is not applied because of positioning', function () { + var clip = + 'clip: rect(1px 1px 1px 1px);' + + 'clip: rect(1px, 1px, 1px, 1px);' + + 'position: relative;' + + 'overflow: hidden;'; + + var vNode = queryFixture('
Hi
'); + + assert.isTrue(isVisibleOnScreen(vNode)); + }); + + it('should detect clip rect hidden text technique on parent', function () { + var clip = + 'clip: rect(1px 1px 1px 1px);' + + 'clip: rect(1px, 1px, 1px, 1px);' + + 'width: 1px; height: 1px;' + + 'position: absolute;' + + 'overflow: hidden;'; + + var vNode = queryFixture( + '
' + '
Hi
' + '
' + ); + + assert.isFalse(isVisibleOnScreen(vNode)); + }); + + it('should detect when clip is not applied because of positioning on parent', function () { + var clip = + 'clip: rect(1px 1px 1px 1px);' + + 'clip: rect(1px, 1px, 1px, 1px);' + + 'position: relative;' + + 'overflow: hidden;'; + + var vNode = queryFixture( + '
' + '
Hi
' + '
' + ); + + assert.isTrue(isVisibleOnScreen(vNode)); + }); + + it('should detect poorly hidden clip rects', function () { + var clip = + 'clip: rect(5px 1px 1px 5px);' + + 'clip: rect(5px, 1px, 1px, 5px);' + + 'width: 1px; height: 1px;' + + 'position: absolute;' + + 'overflow: hidden;'; + + var vNode = queryFixture('
Hi
'); + + assert.isFalse(isVisibleOnScreen(vNode)); + }); + + it('should return false for display: none', function () { + var vNode = queryFixture( + '' + ); + + assert.isFalse(isVisibleOnScreen(vNode)); + }); + + it('should return false for opacity: 0', function () { + var vNode = queryFixture( + '
Hello!
' + ); + + assert.isFalse(isVisibleOnScreen(vNode)); + }); + + it('should return false for 0 height scrollable region', function () { + var vNode = queryFixture( + '
Hello!
' + ); + + assert.isFalse(isVisibleOnScreen(vNode)); + }); + + it('should return false for 0 width scrollable region', function () { + var vNode = queryFixture( + '
Hello!
' + ); + + assert.isFalse(isVisibleOnScreen(vNode)); + }); + + it('returns false for `AREA` without closest `MAP` element', function () { + var vNode = queryFixture( + '' + ); + var actual = isVisibleOnScreen(vNode); + assert.isFalse(actual); + }); + + it('returns false for `AREA` with closest `MAP` with no name attribute', function () { + var vNode = queryFixture( + '' + + '' + + '' + ); + var actual = isVisibleOnScreen(vNode); + assert.isFalse(actual); + }); + + (shadowSupported ? it : xit)( + 'returns false for `AREA` element that is inside shadowDOM', + function () { + fixture.innerHTML = '
'; + var container = fixture.querySelector('#container'); + var shadow = container.attachShadow({ mode: 'open' }); + shadow.innerHTML = + '' + + '' + + ''; + axe.testUtils.flatTreeSetup(fixture); + + var target = shadow.querySelector('#target'); + var actual = isVisibleOnScreen(target); + assert.isFalse(actual); + } + ); + + it('returns false for `AREA` with closest `MAP` with name but not referred by an `IMG` usemap attribute', function () { + var vNode = queryFixture( + '' + + '' + + '' + + 'MDN infographic' + ); + var actual = isVisibleOnScreen(vNode); + assert.isFalse(actual); + }); + + it('returns false for `AREA` with `MAP` and used in `IMG` which is not visible', function () { + var vNode = queryFixture( + '' + + '' + + '' + + 'MDN infographic' + ); + var actual = isVisibleOnScreen(vNode); + assert.isFalse(actual); + }); + + it('returns true for `AREA` with `MAP` and used in `IMG` which is visible', function () { + var vNode = queryFixture( + '' + + '' + + '' + + 'MDN infographic' + ); + var actual = isVisibleOnScreen(vNode); + assert.isTrue(actual); + }); + + // IE11 either only supports clip paths defined by url() or not at all, + // MDN and caniuse.com give different results... + (isIE11 ? it.skip : it)( + 'should detect clip-path hidden text technique', + function () { + var vNode = queryFixture( + '
Hi
' + ); + + assert.isFalse(isVisibleOnScreen(vNode)); + } + ); + + (isIE11 ? it.skip : it)( + 'should detect clip-path hidden text technique on parent', + function () { + var vNode = queryFixture( + '
' + + '
Hi
' + + '
' + ); + + assert.isFalse(isVisibleOnScreen(vNode)); + } + ); + + (shadowSupported ? it : xit)( + 'should correctly handle visible slotted elements', + function () { + function createContentSlotted() { + var group = document.createElement('div'); + group.innerHTML = '
Stuff
'; + return group; + } + function makeShadowTree(node) { + var root = node.attachShadow({ mode: 'open' }); + var div = document.createElement('div'); + root.appendChild(div); + div.appendChild(createContentSlotted()); + } + fixture.innerHTML = '
hello
'; + makeShadowTree(fixture.firstChild); + var tree = axe.utils.getFlattenedTree(fixture.firstChild); + var el = axe.utils.querySelectorAll(tree, 'a')[0]; + assert.isTrue(isVisibleOnScreen(el.actualNode)); + } + ); + (shadowSupported ? it : xit)( + 'should correctly handle hidden slotted elements', + function () { + function createContentSlotted() { + var group = document.createElement('div'); + group.innerHTML = + ''; + return group; + } + function makeShadowTree(node) { + var root = node.attachShadow({ mode: 'open' }); + var div = document.createElement('div'); + root.appendChild(div); + div.appendChild(createContentSlotted()); + } + fixture.innerHTML = '

hello

'; + makeShadowTree(fixture.firstChild); + var tree = axe.utils.getFlattenedTree(fixture.firstChild); + var el = axe.utils.querySelectorAll(tree, 'a')[0]; + assert.isFalse(isVisibleOnScreen(el.actualNode)); + } + ); + it('should return false if element is visually hidden using position absolute, overflow hidden, and a very small height', function () { + var vNode = queryFixture( + '
StickySticky
' + ); + + assert.isFalse(isVisibleOnScreen(vNode)); + }); + + describe('SerialVirtualNode', function () { + it('should return true on statically-positioned, visible elements', function () { + var vNode = new axe.SerialVirtualNode({ + nodeName: 'div' + }); + assert.isTrue(isVisibleOnScreen(vNode)); + }); + + it('should return false on STYLE tag', function () { + var vNode = new axe.SerialVirtualNode({ + nodeName: 'style' + }); + var actual = isVisibleOnScreen(vNode); + assert.isFalse(actual); + }); + + it('should return false on NOSCRIPT tag', function () { + var vNode = new axe.SerialVirtualNode({ + nodeName: 'noscript' + }); + var actual = isVisibleOnScreen(vNode); + assert.isFalse(actual); + }); + + it('should return false on TEMPLATE tag', function () { + var vNode = new axe.SerialVirtualNode({ + nodeName: 'template' + }); + var actual = isVisibleOnScreen(vNode); + assert.isFalse(actual); + }); + + it('should not be affected by `aria-hidden`', function () { + var vNode = new axe.SerialVirtualNode({ + nodeName: 'div', + attributes: { + 'aria-hidden': true + } + }); + assert.isTrue(isVisibleOnScreen(vNode)); + }); + }); +}); diff --git a/test/commons/dom/visibility-methods.js b/test/commons/dom/visibility-methods.js new file mode 100644 index 0000000000..f5d2e12b08 --- /dev/null +++ b/test/commons/dom/visibility-methods.js @@ -0,0 +1,390 @@ +describe('dom.visibility-methods', function () { + var queryFixture = axe.testUtils.queryFixture; + var shadowCheckSetup = axe.testUtils.shadowCheckSetup; + var nativelyHidden = + axe._thisWillBeDeletedDoNotUse.commons.dom.nativelyHidden; + var displayHidden = axe._thisWillBeDeletedDoNotUse.commons.dom.displayHidden; + var visibilityHidden = + axe._thisWillBeDeletedDoNotUse.commons.dom.visibilityHidden; + var ariaHidden = axe._thisWillBeDeletedDoNotUse.commons.dom.ariaHidden; + var opacityHidden = axe._thisWillBeDeletedDoNotUse.commons.dom.opacityHidden; + var scrollHidden = axe._thisWillBeDeletedDoNotUse.commons.dom.scrollHidden; + var overflowHidden = + axe._thisWillBeDeletedDoNotUse.commons.dom.overflowHidden; + var clipHidden = axe._thisWillBeDeletedDoNotUse.commons.dom.clipHidden; + var areaHidden = axe._thisWillBeDeletedDoNotUse.commons.dom.areaHidden; + + describe('nativelyHidden', function () { + var nativelyHiddenElements = ['style', 'script', 'noscript', 'template']; + + it('should return true for hidden elements', function () { + nativelyHiddenElements.forEach(function (nodeName) { + var vNode = new axe.VirtualNode(document.createElement(nodeName)); + assert.isTrue(nativelyHidden(vNode), (nodeName = ' is not hidden')); + }); + }); + + it('should return false for visible elements', function () { + Object.keys(axe._audit.standards.htmlElms) + .filter(function (nodeName) { + return !nativelyHiddenElements.includes(nodeName); + }) + .forEach(function (nodeName) { + var vNode = new axe.VirtualNode(document.createElement(nodeName)); + assert.isFalse(nativelyHidden(vNode), nodeName + ' is not visible'); + }); + }); + }); + + describe('displayHidden', function () { + it('should return true for element with "display:none`', function () { + var vNode = queryFixture( + '' + ); + assert.isTrue(displayHidden(vNode)); + }); + + it('should return false for element with "display:block`', function () { + var vNode = queryFixture( + '
' + ); + assert.isFalse(displayHidden(vNode)); + }); + + it('should return false for element without display', function () { + var vNode = queryFixture('
'); + assert.isFalse(displayHidden(vNode)); + }); + + it('should return false for area element', function () { + var vNode = queryFixture( + '' + + '' + + '' + + '' + ); + assert.isFalse(displayHidden(vNode)); + }); + }); + + describe('visibilityHidden', function () { + it('should return true for element with "visibility:hidden`', function () { + var vNode = queryFixture( + '' + ); + assert.isTrue(visibilityHidden(vNode)); + }); + + it('should return true for element with "visibility:collapse`', function () { + var vNode = queryFixture( + '
' + ); + assert.isTrue(visibilityHidden(vNode)); + }); + + it('should return false for element with "visibility:visible`', function () { + var vNode = queryFixture( + '
' + ); + assert.isFalse(visibilityHidden(vNode)); + }); + + it('should return false for element without visibility', function () { + var vNode = queryFixture('
'); + assert.isFalse(visibilityHidden(vNode)); + }); + + it('should return false for if passed "isAncestor:true`', function () { + var vNode = queryFixture( + '' + ); + assert.isFalse(visibilityHidden(vNode, { isAncestor: true })); + }); + }); + + describe('ariaHidden', function () { + it('should return true for element with "aria-hidden=true`', function () { + var vNode = queryFixture(''); + assert.isTrue(ariaHidden(vNode)); + }); + + it('should return false for element with "aria-hidden=false`', function () { + var vNode = queryFixture('
'); + assert.isFalse(ariaHidden(vNode)); + }); + + it('should return false for element without aria-hidden', function () { + var vNode = queryFixture('
'); + assert.isFalse(ariaHidden(vNode)); + }); + }); + + describe('opacityHidden', function () { + it('should return true for element with "opacity:0`', function () { + var vNode = queryFixture('
'); + assert.isTrue(opacityHidden(vNode)); + }); + + it('should return false for element with "opacity:0.1`', function () { + var vNode = queryFixture('
'); + assert.isFalse(opacityHidden(vNode)); + }); + + it('should return false for element without opacity', function () { + var vNode = queryFixture('
'); + assert.isFalse(opacityHidden(vNode)); + }); + }); + + describe('scrollHidden', function () { + it('should return true for element with scroll and "width:0`', function () { + var vNode = queryFixture( + '
scroll hidden
' + ); + assert.isTrue(scrollHidden(vNode)); + }); + + it('should return true for element with scroll and "height:0`', function () { + var vNode = queryFixture( + '
scroll hidden
' + ); + assert.isTrue(scrollHidden(vNode)); + }); + + it('should return false for element with scroll and width > 0', function () { + var vNode = queryFixture( + '
scroll hidden
' + ); + assert.isFalse(scrollHidden(vNode)); + }); + + it('should return false for element with scroll and height > 0', function () { + var vNode = queryFixture( + '
scroll hidden
' + ); + assert.isFalse(scrollHidden(vNode)); + }); + + it('should return false for element without scroll', function () { + var vNode = queryFixture('
scroll hidden
'); + assert.isFalse(scrollHidden(vNode)); + }); + }); + + describe('overflowHidden', function () { + it('should return true for element with "overflow:hidden" and "width:0`', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isTrue(overflowHidden(vNode)); + }); + + it('should return true for element with "overflow:hidden" and "height:0`', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isTrue(overflowHidden(vNode)); + }); + + it('should return true for element with "overflow:hidden" and "width:1`', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isTrue(overflowHidden(vNode)); + }); + + it('should return true for element with "overflow:hidden" and "height:1`', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isTrue(overflowHidden(vNode)); + }); + + it('should return true for element with "overflow:hidden" and width > 1', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isFalse(overflowHidden(vNode)); + }); + + it('should return true for element with "overflow:hidden" and height > 1', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isFalse(overflowHidden(vNode)); + }); + + it('should return false for element with "position:fixed`', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isFalse(overflowHidden(vNode)); + }); + + it('should return false for element with "position:relative`', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isFalse(overflowHidden(vNode)); + }); + + it('should return false for element without position', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isFalse(overflowHidden(vNode)); + }); + + it('should return false for element with "overflow:visible`', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isFalse(overflowHidden(vNode)); + }); + + it('should return false for element with "overflow:auto`', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isFalse(overflowHidden(vNode)); + }); + + it('should return false for element without overflow', function () { + var vNode = queryFixture( + '
overflow hidden
' + ); + assert.isFalse(overflowHidden(vNode)); + }); + }); + + describe('clipHidden', function () { + it('should return true for element with clip-path and inset >= 50%', function () { + var vNode = queryFixture( + '
Hello world
' + ); + assert.isTrue(clipHidden(vNode)); + }); + + it('should return false for element with clip-path and inset < 50%', function () { + var vNode = queryFixture( + '
Hello world
' + ); + assert.isFalse(clipHidden(vNode)); + }); + + it('should return true for element with clip-path and circle=0', function () { + var vNode = queryFixture( + '
Hello world
' + ); + assert.isTrue(clipHidden(vNode)); + }); + + it('should return false for element with clip-path and circle > 0', function () { + var vNode = queryFixture( + '
Hello world
' + ); + assert.isFalse(clipHidden(vNode)); + }); + + it('should return true for element with clip and "position:absolute`', function () { + var vNode = queryFixture( + '
Hello world
' + ); + assert.isTrue(clipHidden(vNode)); + }); + + it('should return true for element with clip and "position:fixed"', function () { + var vNode = queryFixture( + '
Hello world
' + ); + assert.isTrue(clipHidden(vNode)); + }); + + it('should return true for element with clip using commas', function () { + var vNode = queryFixture( + '
Hello world
' + ); + assert.isTrue(clipHidden(vNode)); + }); + + it('should return true for poorly hidden clip rects', function () { + var vNode = queryFixture( + '
Hello world
' + ); + assert.isTrue(clipHidden(vNode)); + }); + + it('should return false for element with clip and "position:relative"', function () { + var vNode = queryFixture( + '
Hello world
' + ); + assert.isFalse(clipHidden(vNode)); + }); + + it('should return false for element with clip and without position', function () { + var vNode = queryFixture( + '
Hello world
' + ); + assert.isFalse(clipHidden(vNode)); + }); + + it('should return false for element without clip or clip-path', function () { + var vNode = queryFixture('
Hello world
'); + assert.isFalse(clipHidden(vNode)); + }); + + it('should return false for element with visible clip', function () { + var vNode = queryFixture( + '
Hello world
' + ); + assert.isFalse(clipHidden(vNode)); + }); + }); + + describe('areaHidden', function () { + it('should return true for element without map parent', function () { + var vNode = queryFixture(''); + assert.isTrue(areaHidden(vNode)); + }); + + it('should return true for map without a name', function () { + var vNode = queryFixture(''); + assert.isTrue(areaHidden(vNode)); + }); + + it('should return true if map is in shadowDOM', function () { + var vNode = shadowCheckSetup( + '
', + '' + )[2]; + assert.isTrue(areaHidden(vNode)); + }); + + it('should return true if img does not use map', function () { + var vNode = queryFixture(''); + assert.isTrue(areaHidden(vNode)); + }); + + it('should return true if img uses map but is hidden', function () { + var vNode = queryFixture( + '' + ); + assert.isTrue( + areaHidden(vNode, function () { + return false; + }) + ); + }); + + it('should return false if img uses map and is visible', function () { + var vNode = queryFixture( + '' + ); + assert.isFalse( + areaHidden(vNode, function () { + return true; + }) + ); + }); + }); +}); diff --git a/test/rule-matches/is-visible-matches.js b/test/rule-matches/is-visible-matches.js index a08410496c..7dc3a87822 100644 --- a/test/rule-matches/is-visible-matches.js +++ b/test/rule-matches/is-visible-matches.js @@ -1,31 +1,32 @@ -describe('is-visible-matches', function() { +describe('is-visible-matches', function () { 'use strict'; var rule; var fixture = document.getElementById('fixture'); + var fixtureSetup = axe.testUtils.fixtureSetup; - beforeEach(function() { + beforeEach(function () { fixture.innerHTML = ''; rule = axe.utils.getRule('avoid-inline-spacing'); }); - it('returns true for visible elements', function() { - fixture.innerHTML = '

Hello world

'; + it('returns true for visible elements', function () { + fixtureSetup('

Hello world

'); assert.isTrue(rule.matches(fixture.firstChild)); }); - it('returns false for elements with hidden', function() { - fixture.innerHTML = '' + it('returns false for elements with hidden', function () { + fixtureSetup(''); assert.isFalse(rule.matches(fixture.firstChild)); }); - it('returns true for visible elements with aria-hidden="true"', function() { - fixture.innerHTML = '' + it('returns true for visible elements with aria-hidden="true"', function () { + fixtureSetup(''); assert.isTrue(rule.matches(fixture.firstChild)); }); - it('returns false for opacity:0 elements with accessible text', function() { - fixture.innerHTML = '

Hello world

'; + it('returns false for opacity:0 elements with accessible text', function () { + fixtureSetup('

Hello world

'); assert.isFalse(rule.matches(fixture.firstChild)); }); });