diff --git a/lib/commons/color/get-background-color.js b/lib/commons/color/get-background-color.js index ac93dd301d..4fc036cee0 100644 --- a/lib/commons/color/get-background-color.js +++ b/lib/commons/color/get-background-color.js @@ -9,39 +9,9 @@ * @memberof axe.commons.color * @param {Element} elm Element to determine background color * @param {Array} [bgElms=[]] elements to inspect - * @param {Boolean} [noScroll=false] should scroll * @returns {Color} */ -color.getBackgroundColor = function getBackgroundColor( - elm, - bgElms = [], - noScroll = false -) { - if (noScroll !== true) { - /** - * Avoid scrolling overflow:hidden containers, by only aligning to top, - * when not doing so would move the center point above the viewport top. - */ - const clientHeight = elm.getBoundingClientRect().height; - const alignToTop = clientHeight - 2 >= window.innerHeight * 2; - elm.scrollIntoView(alignToTop); - - // ensure element is scrolled into view horizontally - let center, scrollParent; - do { - const rect = elm.getBoundingClientRect(); - - // 'x' does not exist in IE11 - const x = 'x' in rect ? rect.x : rect.left; - center = x + rect.width / 2; - - if (center < 0) { - scrollParent = getScrollParent(elm); - scrollParent.scrollLeft = 0; - } - } while (center < 0 && scrollParent !== document.documentElement); - } - +color.getBackgroundColor = function getBackgroundColor(elm, bgElms = []) { let bgColors = []; let elmStack = color.getBackgroundStack(elm); @@ -101,7 +71,6 @@ color.getBackgroundStack = function getBackgroundStack(elm) { if (elmStack === null) { return null; } - elmStack = includeMissingElements(elmStack, elm); elmStack = dom.reduceToElementsBelowFloating(elmStack, elm); elmStack = sortPageBackground(elmStack); @@ -133,15 +102,6 @@ color.filteredRectStack = function filteredRectStack(elm) { const boundingStack = rectStack.shift(); let isSame; - // Safari v12.1 does not include labels as part of elementsFromPoint() - // if they wrap an input element (UNLESS the label has a background - // color). this results in two different rectStacks (since - // elm.getClientRects() returns two rects for the element) which - // returns null as isSame is false. we can fix this by adding in the - // missing label to the boundingStack before checking for isSame - // @see https://bugs.webkit.org/show_bug.cgi?id=197743 - includeMissingElements(boundingStack, elm); - // iterating over arrays of DOMRects rectStack.forEach((rectList, index) => { if (index === 0) { @@ -179,19 +139,7 @@ color.filteredRectStack = function filteredRectStack(elm) { * @return {Array} */ color.getRectStack = function(elm) { - const boundingCoords = axe.commons.color.centerPointOfRect( - elm.getBoundingClientRect() - ); - - if (!boundingCoords) { - return null; - } - - let boundingStack = dom.shadowElementsFromPoint( - boundingCoords.x, - boundingCoords.y - ); - + const boundingStack = axe.commons.dom.getElementStack(elm); let rects = Array.from(elm.getClientRects()); // If the element does not have multiple rects, like for display:block, return a single stack if (!rects || rects.length <= 1) { @@ -199,17 +147,7 @@ color.getRectStack = function(elm) { } // Handle inline elements spanning multiple lines to be evaluated - let filteredArr = rects - .filter(rect => { - // exclude manual line breaks in Chrome/Safari - return rect.width && rect.width > 0; - }) - .map(rect => { - const coords = axe.commons.color.centerPointOfRect(rect); - if (coords) { - return dom.shadowElementsFromPoint(coords.x, coords.y); - } - }); + let filteredArr = axe.commons.dom.getClientElementStack(elm); if (filteredArr.some(stack => stack === undefined)) { // Can be happen when one or more of the rects sits outside the viewport @@ -263,66 +201,6 @@ function sortPageBackground(elmStack) { return bgNodes; } -/** - * Include nodes missing from initial gathering because - * document.elementsFromPoint misses some elements we need - * i.e. TR is missing from table elementStack and leaves out bgColor - * https://github.com/dequelabs/axe-core/issues/273 - * @private - * @param {Array} elmStack - * @param {Element} elm - * @returns {Array} - */ -function includeMissingElements(elmStack, elm) { - /*eslint max-depth:["error",7]*/ - const nodeName = elm.nodeName.toUpperCase(); - const elementMap = { - TD: ['TR', 'THEAD', 'TBODY', 'TFOOT'], - TH: ['TR', 'THEAD', 'TBODY', 'TFOOT'], - INPUT: ['LABEL'] - }; - const tagArray = elmStack.map(elm => { - return elm.nodeName.toUpperCase(); - }); - let bgNodes = elmStack; - for (let candidate in elementMap) { - // check that TR or LABEL has paired nodeName from elementMap, but don't expect elm to be that candidate - if (tagArray.includes(candidate)) { - for ( - let candidateIndex = 0; - candidateIndex < elementMap[candidate].length; - candidateIndex++ - ) { - // look up the tree for a matching candidate - let ancestorMatch = axe.commons.dom.findUp( - elm, - elementMap[candidate][candidateIndex] - ); - if (ancestorMatch && elmStack.indexOf(ancestorMatch) === -1) { - // found an ancestor not in elmStack, and it overlaps - let overlaps = axe.commons.dom.visuallyOverlaps( - elm.getBoundingClientRect(), - ancestorMatch - ); - if (overlaps) { - // if target is in the elementMap, use its position. - bgNodes.splice(tagArray.indexOf(candidate) + 1, 0, ancestorMatch); - } - } - // nodeName matches value - // (such as LABEL, when matching itself. It should be in the list, but Phantom skips it) - if ( - nodeName === elementMap[candidate][candidateIndex] && - tagArray.indexOf(nodeName) === -1 - ) { - bgNodes.splice(tagArray.indexOf(candidate) + 1, 0, elm); - } - } - } - } - return bgNodes; -} - /** * Determine if element is partially overlapped, triggering a Can't Tell result * @private @@ -393,36 +271,6 @@ function contentOverlapping(targetElement, bgNode) { return false; } -/** - * Return the scrolling parent element - * @see https://stackoverflow.com/questions/35939886/find-first-scrollable-parent#42543908 - * @param {Element} element - * @param {Boolean} includeHidden - * @return {Element} - */ -function getScrollParent(element, includeHidden) { - var style = getComputedStyle(element); - var excludeStaticParent = style.position === 'absolute'; - var overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/; - - if (style.position === 'fixed') { - return document.documentElement; - } - for (var parent = element; (parent = parent.parentElement); ) { - style = getComputedStyle(parent); - if (excludeStaticParent && style.position === 'static') { - continue; - } - if ( - overflowRegex.test(style.overflow + style.overflowY + style.overflowX) - ) { - return parent; - } - } - - return document.documentElement; -} - /** * Determines whether an element has a fully opaque background, whether solid color or an image * @param {Element} node diff --git a/lib/commons/dom/get-element-stack.js b/lib/commons/dom/get-element-stack.js index 2ce44bc9ed..2d955828ca 100644 --- a/lib/commons/dom/get-element-stack.js +++ b/lib/commons/dom/get-element-stack.js @@ -1,7 +1,7 @@ -/* global dom */ +/* global dom, VirtualNode */ // split the page cells to group elements by the position -const gridSize = 200; // arbitrary size, increase to reduce memory (less cells) use but increase time (more nodes per grid to check collision) +const gridSize = 200; // arbitrary size, increase to reduce memory use (less cells) but increase time (more nodes per grid to check collision) /** * Determine if node produces a stacking context. @@ -11,31 +11,20 @@ const gridSize = 200; // arbitrary size, increase to reduce memory (less cells) * @param {VirtualNode} vNode * @return {Boolean} */ -function isStackingContext(vNode) { - const node = vNode.actualNode; +function isStackingContext(vNode, parentVNode) { + const position = vNode.getComputedStylePropertyValue('position'); + const zIndex = vNode.getComputedStylePropertyValue('z-index'); - //the root element (HTML) - if ( - !node || - node.nodeName === 'HTML' || - node.nodeName === '#document-fragment' - ) { - return true; - } + // the root element (HTML) is skipped since we always start with a + // stacking order of [0] // position: fixed or sticky - if ( - vNode.getComputedStylePropertyValue('position') === 'fixed' || - vNode.getComputedStylePropertyValue('position') === 'sticky' - ) { + if (position === 'fixed' || position === 'sticky') { return true; } // positioned (absolutely or relatively) with a z-index value other than "auto", - if ( - vNode.getComputedStylePropertyValue('z-index') !== 'auto' && - vNode.getComputedStylePropertyValue('position') !== 'static' - ) { + if (zIndex !== 'auto' && position !== 'static') { return true; } @@ -56,34 +45,26 @@ function isStackingContext(vNode) { } // elements with a mix-blend-mode value other than "normal" - if ( - vNode.getComputedStylePropertyValue('mix-blend-mode') && - vNode.getComputedStylePropertyValue('mix-blend-mode') !== 'normal' - ) { + const mixBlendMode = vNode.getComputedStylePropertyValue('mix-blend-mode'); + if (mixBlendMode && mixBlendMode !== 'normal') { return true; } // elements with a filter value other than "none" - if ( - vNode.getComputedStylePropertyValue('filter') && - vNode.getComputedStylePropertyValue('filter') !== 'none' - ) { + const filter = vNode.getComputedStylePropertyValue('filter'); + if (filter && filter !== 'none') { return true; } // elements with a perspective value other than "none" - if ( - vNode.getComputedStylePropertyValue('perspective') && - vNode.getComputedStylePropertyValue('perspective') !== 'none' - ) { + const perspective = vNode.getComputedStylePropertyValue('perspective'); + if (perspective && perspective !== 'none') { return true; } // element with a clip-path value other than "none" - if ( - vNode.getComputedStylePropertyValue('clip-path') && - vNode.getComputedStylePropertyValue('clip-path') !== 'none' - ) { + const clipPath = vNode.getComputedStylePropertyValue('clip-path'); + if (clipPath && clipPath !== 'none') { return true; } @@ -92,7 +73,6 @@ function isStackingContext(vNode) { vNode.getComputedStylePropertyValue('-webkit-mask') || vNode.getComputedStylePropertyValue('mask') || 'none'; - if (mask !== 'none') { return true; } @@ -102,7 +82,6 @@ function isStackingContext(vNode) { vNode.getComputedStylePropertyValue('-webkit-mask-image') || vNode.getComputedStylePropertyValue('mask-image') || 'none'; - if (maskImage !== 'none') { return true; } @@ -112,7 +91,6 @@ function isStackingContext(vNode) { vNode.getComputedStylePropertyValue('-webkit-mask-border') || vNode.getComputedStylePropertyValue('mask-border') || 'none'; - if (maskBorder !== 'none') { return true; } @@ -123,10 +101,8 @@ function isStackingContext(vNode) { } // transform or opacity in will-change even if you don't specify values for these attributes directly - if ( - vNode.getComputedStylePropertyValue('will-change') === 'transform' || - vNode.getComputedStylePropertyValue('will-change') === 'opacity' - ) { + const willChange = vNode.getComputedStylePropertyValue('will-change'); + if (willChange === 'transform' || willChange === 'opacity') { return true; } @@ -146,11 +122,8 @@ function isStackingContext(vNode) { } // a flex item or gird item with a z-index value other than "auto", that is the parent element display: flex|inline-flex|grid|inline-grid, - if ( - vNode.getComputedStylePropertyValue('z-index') !== 'auto' && - vNode.parent - ) { - const parentDsiplay = vNode.parent.getComputedStylePropertyValue('display'); + if (zIndex !== 'auto' && parentVNode) { + const parentDsiplay = parentVNode.getComputedStylePropertyValue('display'); if ( [ 'flex', @@ -192,9 +165,7 @@ function getPositionOrder(vNode) { } // 3. the in-flow, non-inline-level, non-positioned descendants. - if (vNode.getComputedStylePropertyValue('position') === 'static') { - return 0; - } + return 0; } // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0. @@ -210,15 +181,6 @@ function getPositionOrder(vNode) { */ function visuallySort(a, b) { /*eslint no-bitwise: 0 */ - - // 1. The root element forms the root stacking context. - if (a.actualNode.nodeName.toLowerCase() === 'html') { - return 1; - } - if (b.actualNode.nodeName.toLowerCase() === 'html') { - return -1; - } - for (let i = 0; i < a._stackingOrder.length; i++) { if (typeof b._stackingOrder[i] === 'undefined') { return -1; @@ -236,9 +198,55 @@ function visuallySort(a, b) { } // nodes are the same stacking order - const docPosition = a.actualNode.compareDocumentPosition(b.actualNode); - const DOMOrder = docPosition & 4 ? 1 : -1; - const isDescendant = docPosition & 8 || docPosition & 16; + let aNode = a.actualNode; + let bNode = b.actualNode; + + // elements don't correctly calculate document position when comparing + // across shadow boundaries, so we need to compare the position of a + // shared host instead + + // elements have different hosts + if (aNode.getRootNode && aNode.getRootNode() !== bNode.getRootNode()) { + // keep track of all parent hosts and find the one both nodes share + const boundaries = []; + while (aNode) { + boundaries.push({ + root: aNode.getRootNode(), + node: aNode + }); + aNode = aNode.getRootNode().host; + } + + while ( + bNode && + !boundaries.find(boundary => boundary.root === bNode.getRootNode()) + ) { + bNode = bNode.getRootNode().host; + } + + // bNode is a node that shares a host with some part of the a parent + // shadow tree, find the aNode that shares the same host as bNode + aNode = boundaries.find(boundary => boundary.root === bNode.getRootNode()) + .node; + + // sort child of shadow to it's host node by finding which element is + // the child of the host and sorting it before the host + if (aNode === bNode) { + return a.actualNode.getRootNode() !== aNode.getRootNode() ? -1 : 1; + } + } + + const { + DOCUMENT_POSITION_FOLLOWING, + DOCUMENT_POSITION_CONTAINS, + DOCUMENT_POSITION_CONTAINED_BY + } = window.Node; + + const docPosition = aNode.compareDocumentPosition(bNode); + const DOMOrder = docPosition & DOCUMENT_POSITION_FOLLOWING ? 1 : -1; + const isDescendant = + docPosition & DOCUMENT_POSITION_CONTAINS || + docPosition & DOCUMENT_POSITION_CONTAINED_BY; const aPosition = getPositionOrder(a); const bPosition = getPositionOrder(b); @@ -256,17 +264,13 @@ function visuallySort(a, b) { * @param {VirtualNode} * @return {Number[]} */ -function getStackingOrder(vNode) { - const stackingOrder = vNode.parent - ? vNode.parent._stackingOrder.slice() - : [0]; - - if (vNode.getComputedStylePropertyValue('z-index') !== 'auto') { - stackingOrder[stackingOrder.length - 1] = parseInt( - vNode.getComputedStylePropertyValue('z-index') - ); +function getStackingOrder(vNode, parentVNode) { + const stackingOrder = parentVNode._stackingOrder.slice(); + const zIndex = vNode.getComputedStylePropertyValue('z-index'); + if (zIndex !== 'auto') { + stackingOrder[stackingOrder.length - 1] = parseInt(zIndex); } - if (isStackingContext(vNode)) { + if (isStackingContext(vNode, parentVNode)) { stackingOrder.push(0); } @@ -278,24 +282,25 @@ function getStackingOrder(vNode) { * @param {VirtualNode} * @return {VirtualNode|null} */ -function findScrollRegionParent(vNode) { +function findScrollRegionParent(vNode, parentVNode) { let scrollRegionParent = null; - let vNodeParent = vNode.parent; let checkedNodes = [vNode]; - while (vNodeParent) { - if (vNodeParent._scrollRegionParent) { - scrollRegionParent = vNodeParent._scrollRegionParent; + while (parentVNode) { + if (parentVNode._scrollRegionParent) { + scrollRegionParent = parentVNode._scrollRegionParent; break; } - if (axe.utils.getScroll(vNodeParent.actualNode)) { - scrollRegionParent = vNodeParent; + if (axe.utils.getScroll(parentVNode.actualNode)) { + scrollRegionParent = parentVNode; break; } - checkedNodes.push(vNodeParent); - vNodeParent = vNodeParent.parent; + checkedNodes.push(parentVNode); + parentVNode = axe.utils.getNodeFromTree( + parentVNode.actualNode.parentElement || parentVNode.actualNode.parentNode + ); } // cache result of parent scroll region so we don't have to look up the entire @@ -306,21 +311,6 @@ function findScrollRegionParent(vNode) { return scrollRegionParent; } -/** - * Get the DOMRect x or y value. IE11 (and Phantom) does not support x/y - * on DOMRect. - * @param {DOMRect} - * @param {String} pos 'x' or 'y' - * @return {Number} - */ -function getDomPosition(rect, pos) { - if (pos === 'x') { - return 'x' in rect ? rect.x : rect.left; - } - - return 'y' in rect ? rect.y : rect.top; -} - /** * Add a node to every cell of the grid it intersects with. * @param {Grid} @@ -332,97 +322,119 @@ function addNodeToGrid(grid, vNode) { vNode._grid = grid; vNode.clientRects.forEach(rect => { - const startRow = Math.floor(getDomPosition(rect, 'y') / gridSize); - const startCol = Math.floor(getDomPosition(rect, 'x') / gridSize); + const x = rect.left; + const y = rect.top; - const endRow = Math.floor( - (getDomPosition(rect, 'y') + rect.height) / gridSize - ); - const endCol = Math.floor( - (getDomPosition(rect, 'x') + rect.width) / gridSize - ); + // "| 0" is a faster way to do Math.floor + // @see https://jsperf.com/math-floor-vs-math-round-vs-parseint/152 + const startRow = (y / gridSize) | 0; + const startCol = (x / gridSize) | 0; + const endRow = ((y + rect.height) / gridSize) | 0; + const endCol = ((x + rect.width) / gridSize) | 0; for (let row = startRow; row <= endRow; row++) { grid.cells[row] = grid.cells[row] || []; for (let col = startCol; col <= endCol; col++) { grid.cells[row][col] = grid.cells[row][col] || []; - - if (!grid.cells[row][col].includes(vNode)) { - grid.cells[row][col].push(vNode); - } + grid.cells[row][col].push(vNode); } } }); } /** - * Setup the 2d grid and add every element to it. + * Setup the 2d grid and add every element to it, even elements not + * included in the flat tree */ -function createGrid() { - const rootGrid = { +function createGrid( + root = document.body, + rootGrid = { container: null, cells: [] - }; - - axe.utils - .querySelectorAll(axe._tree[0], '*') - .filter(vNode => vNode.actualNode.parentElement !== document.head) - .forEach(vNode => { - if (vNode.actualNode.nodeType !== window.Node.ELEMENT_NODE) { - return; - } + }, + parentVNode = null +) { + // by not starting at the htmlElement we don't have to pass a custom + // filter function into the treeWalker to filter out head elements, + // which would be called for every node + if (!parentVNode) { + let vNode = axe.utils.getNodeFromTree(document.documentElement); + if (!vNode) { + vNode = new VirtualNode(document.documentElement); + } - vNode._stackingOrder = getStackingOrder(vNode); + vNode._stackingOrder = [0]; + addNodeToGrid(rootGrid, vNode); - // filter out any elements with 0 width or height - // (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) { - return; - } + if (axe.utils.getScroll(vNode.actualNode)) { + const subGrid = { + container: vNode, + cells: [] + }; + vNode._subGrid = subGrid; + } + } - const scrollRegionParent = findScrollRegionParent(vNode); - const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid; + // IE11 requires the first 3 parameters + // @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker + const treeWalker = document.createTreeWalker( + root, + window.NodeFilter.SHOW_ELEMENT, + null, + false + ); + let node = parentVNode ? treeWalker.nextNode() : treeWalker.currentNode; + while (node) { + let vNode = axe.utils.getNodeFromTree(node); + + // an svg in IE11 does not have a parentElement but instead has a + // parentNode. but parentNode could be a shadow root so we need to + // verity it's in the tree first + if (node.parentElement) { + parentVNode = axe.utils.getNodeFromTree(node.parentElement); + } else if (node.parentNode && axe.utils.getNodeFromTree(node.parentNode)) { + parentVNode = axe.utils.getNodeFromTree(node.parentNode); + } - if (axe.utils.getScroll(vNode.actualNode)) { - const subGrid = { - container: vNode, - cells: [] - }; - vNode._subGrid = subGrid; - } + if (!vNode) { + vNode = new VirtualNode(node, parentVNode); + } - addNodeToGrid(grid, vNode); - }); -} + vNode._stackingOrder = getStackingOrder(vNode, parentVNode); -/** - * Return all elements that are at the center point of the passed in virtual node. - * @method getElementStack - * @memberof axe.commons.dom - * @param {VirtualNode} vNode - * @param {Boolean} [recursed] If the function has been called recursively - * @return {VirtualNode[]} - */ -dom.getElementStack = function(vNode, recursed = false) { - if (!axe._cache.get('gridCreated')) { - createGrid(); - axe._cache.set('gridCreated', true); - } + const scrollRegionParent = findScrollRegionParent(vNode, parentVNode); + const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid; - const grid = vNode._grid; + if (axe.utils.getScroll(vNode.actualNode)) { + const subGrid = { + container: vNode, + cells: [] + }; + vNode._subGrid = subGrid; + } - if (!grid) { - return []; - } + // filter out any elements with 0 width or height + // (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) { + addNodeToGrid(grid, vNode); + } - const boundingRect = vNode.boundingClientRect; + // add shadow root elements to the grid + if (axe.utils.isShadowRoot(node)) { + createGrid(node.shadowRoot, grid, vNode); + } + + node = treeWalker.nextNode(); + } +} +function getRectStack(grid, rect, recursed = false) { // use center point of rect - let x = getDomPosition(boundingRect, 'x') + boundingRect.width / 2; - let y = getDomPosition(boundingRect, 'y') + boundingRect.height / 2; + let x = rect.left + rect.width / 2; + let y = rect.top + rect.height / 2; // NOTE: there is a very rare edge case in Chrome vs Firefox that can // return different results of `document.elementsFromPoint`. If the center @@ -430,36 +442,91 @@ dom.getElementStack = function(vNode, recursed = false) { // Chrome appears to round the number up and return the element while Firefox // keeps the number as is and won't return the element. In this case, we // went with pixel perfect collision rather than rounding - const row = Math.floor(y / gridSize); - const col = Math.floor(x / gridSize); + const row = (y / gridSize) | 0; + const col = (x / gridSize) | 0; let stack = grid.cells[row][col].filter(gridCellNode => { - return gridCellNode.clientRects.find(rect => { - let pointX = x; - let pointY = y; - - let rectWidth = rect.width; - let rectHeight = rect.height; - let rectX = getDomPosition(rect, 'x'); - let rectY = getDomPosition(rect, 'y'); + return gridCellNode.clientRects.find(clientRect => { + let rectX = clientRect.left; + let rectY = clientRect.top; // perform an AABB (axis-aligned bounding box) collision check for the // point inside the rect return ( - pointX < rectX + rectWidth && - pointX > rectX && - pointY < rectY + rectHeight && - pointY > rectY + x < rectX + clientRect.width && + x > rectX && + y < rectY + clientRect.height && + y > rectY ); }); }); - if (grid.container) { - stack = dom.getElementStack(grid.container, true).concat(stack); + const gridContainer = grid.container; + if (gridContainer) { + stack = getRectStack( + gridContainer._grid, + gridContainer.boundingClientRect, + true + ).concat(stack); } if (!recursed) { - stack.sort(visuallySort); + stack = stack + .sort(visuallySort) + .map(vNode => vNode.actualNode) + // always make sure html is the last element + .concat(document.documentElement) + // remove duplicates caused by adding client rects of the same node + .filter((node, index, array) => array.indexOf(node) === index); } return stack; +} + +/** + * Return all elements that are at the center bounding rect of the passed in node. + * @method getElementStack + * @memberof axe.commons.dom + * @param {Node} node + * @return {Node[]} + */ +dom.getElementStack = function(node) { + if (!axe._cache.get('gridCreated')) { + createGrid(); + axe._cache.set('gridCreated', true); + } + + const vNode = axe.utils.getNodeFromTree(node); + const grid = vNode._grid; + + if (!grid) { + return []; + } + + return getRectStack(grid, vNode.boundingClientRect); +}; + +/** + * Return all elements that are at the center of each client rect of the passed in node. + * @method getClientElementStack + * @memberof axe.commons.dom + * @param {Node} node + * @return {Array} + */ +dom.getClientElementStack = function(node) { + if (!axe._cache.get('gridCreated')) { + createGrid(); + axe._cache.set('gridCreated', true); + } + + const vNode = axe.utils.getNodeFromTree(node); + const grid = vNode._grid; + + if (!grid) { + return []; + } + + const clientRects = vNode.clientRects; + return clientRects.map(rect => { + return getRectStack(grid, rect); + }); }; diff --git a/lib/commons/dom/visually-contains.js b/lib/commons/dom/visually-contains.js index da9cd6f248..c5aa2ea3d8 100644 --- a/lib/commons/dom/visually-contains.js +++ b/lib/commons/dom/visually-contains.js @@ -1,35 +1,51 @@ /* global dom */ +/** + * Return the ancestor node that is a scroll region. + * @param {VirtualNode} + * @return {VirtualNode|null} + */ +function getScrollAncestor(node) { + const vNode = axe.utils.getNodeFromTree(node); + let ancestor = vNode.parent; + + while (ancestor) { + if (axe.utils.getScroll(ancestor.actualNode)) { + return ancestor.actualNode; + } + + ancestor = ancestor.parent; + } +} + /** * Checks whether a parent element visually contains its child, either directly or via scrolling. * Assumes that |parent| is an ancestor of |node|. - * @method visuallyContains - * @memberof axe.commons.dom - * @instance * @param {Element} node * @param {Element} parent * @return {boolean} True if node is visually contained within parent */ -dom.visuallyContains = function(node, parent) { - var rectBound = node.getBoundingClientRect(); - var margin = 0.01; - var rect = { +function contains(node, parent) { + const rectBound = node.getBoundingClientRect(); + const margin = 0.01; + const rect = { top: rectBound.top + margin, bottom: rectBound.bottom - margin, left: rectBound.left + margin, right: rectBound.right - margin }; - var parentRect = parent.getBoundingClientRect(); - var parentTop = parentRect.top; - var parentLeft = parentRect.left; - var parentScrollArea = { + + const parentRect = parent.getBoundingClientRect(); + const parentTop = parentRect.top; + const parentLeft = parentRect.left; + const parentScrollArea = { top: parentTop - parent.scrollTop, bottom: parentTop - parent.scrollTop + parent.scrollHeight, left: parentLeft - parent.scrollLeft, right: parentLeft - parent.scrollLeft + parent.scrollWidth }; - var style = window.getComputedStyle(parent); + const style = window.getComputedStyle(parent); // if parent element is inline, scrollArea will be too unpredictable if (style.getPropertyValue('display') === 'inline') { @@ -58,4 +74,38 @@ dom.visuallyContains = function(node, parent) { } return true; +} + +/** + * Checks whether a parent element visually contains its child, either directly or via scrolling. + * Assumes that |parent| is an ancestor of |node|. + * @method visuallyContains + * @memberof axe.commons.dom + * @instance + * @param {Element} node + * @param {Element} parent + * @return {boolean} True if node is visually contained within parent + */ +dom.visuallyContains = function visuallyContains(node, parent) { + const parentScrollAncestor = getScrollAncestor(parent); + + // if the elements share a common scroll parent, we can check if the + // parent visually contains the node. otherwise we need to check each + // scroll parent in between the the node and the parent since if the + // element is off screen due to the scroll, it won't be visually contained + // by the parent + do { + const nextScrollAncestor = getScrollAncestor(node); + + if ( + nextScrollAncestor === parentScrollAncestor || + nextScrollAncestor === parent + ) { + return contains(node, parent); + } + + node = nextScrollAncestor; + } while (node); + + return false; }; diff --git a/test/checks/color/link-in-text-block.js b/test/checks/color/link-in-text-block.js index f82cf7f7e2..a0a7eaa7fb 100644 --- a/test/checks/color/link-in-text-block.js +++ b/test/checks/color/link-in-text-block.js @@ -85,6 +85,7 @@ describe('link-in-text-block', function() { linkId + '">link' + '

'; + axe.testUtils.flatTreeSetup(fixture); return document.getElementById(linkId); } @@ -132,6 +133,7 @@ describe('link-in-text-block', function() { ' inside block

inside block' + ' outside block '; + axe.testUtils.flatTreeSetup(fixture); var orig = axe.commons.color.elementIsDistinct; var linkElm = document.getElementById('link'); var parentElm = document.getElementById('parent'); @@ -155,6 +157,7 @@ describe('link-in-text-block', function() { var linkElm = shadow.querySelector('a'); fixture.appendChild(parentElm); + axe.testUtils.flatTreeSetup(fixture); var orig = axe.commons.color.elementIsDistinct; axe.commons.color.elementIsDistinct = function(arg1, arg2) { assert.deepEqual(arg1, linkElm); @@ -176,6 +179,7 @@ describe('link-in-text-block', function() { shadow.innerHTML = '

'; fixture.appendChild(div); + axe.testUtils.flatTreeSetup(fixture); var linkElm = div.querySelector('a'); var parentElm = shadow.querySelector('p'); diff --git a/test/commons/color/get-background-color.js b/test/commons/color/get-background-color.js index e18327b8a1..6b74f5bbd3 100644 --- a/test/commons/color/get-background-color.js +++ b/test/commons/color/get-background-color.js @@ -8,7 +8,6 @@ describe('color.getBackgroundColor', function() { afterEach(function() { document.getElementById('fixture').innerHTML = ''; axe.commons.color.incompleteData.clear(); - document.body.scrollTop = 0; axe._tree = undefined; }); @@ -20,7 +19,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(128, 0, 0, 1); assert.closeTo(actual.red, expected.red, 0.5); @@ -44,7 +43,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var pos = fixture.querySelector('#pos'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(64, 64, 0, 1); @@ -64,7 +63,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var under = fixture.querySelector('#under'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(64, 64, 0, 1); assert.closeTo(actual.red, expected.red, 0.5); @@ -90,7 +89,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var under = fixture.querySelector('#under'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(64, 64, 0, 1); @@ -109,7 +108,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(64, 64, 0, 1); assert.closeTo(actual.red, expected.red, 0.5); @@ -127,7 +126,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(64, 64, 0, 1); assert.equal(actual.red, expected.red); @@ -146,7 +145,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); assert.isNull(actual); assert.deepEqual(bgNodes, [target, parent]); @@ -156,7 +155,7 @@ describe('color.getBackgroundColor', function() { it('should return white if transparency goes all the way up to document', function() { fixture.innerHTML = '
'; var target = fixture.querySelector('#target'); - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target); var expected = new axe.commons.color.Color(255, 255, 255, 1); assert.equal(actual.red, expected.red); @@ -172,7 +171,7 @@ describe('color.getBackgroundColor', function() { '
'; var target = fixture.querySelector('#target'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); assert.isNull(actual); assert.deepEqual(bgNodes, [target]); @@ -183,7 +182,7 @@ describe('color.getBackgroundColor', function() { fixture.innerHTML = '
' + '
Hello
'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] @@ -196,7 +195,7 @@ describe('color.getBackgroundColor', function() { fixture.innerHTML = '
' + '
Hello
'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] @@ -212,7 +211,7 @@ describe('color.getBackgroundColor', function() { ''; var target = fixture.querySelector('#target'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(0, 128, 0, 1); assert.equal(actual.red, expected.red); @@ -228,7 +227,7 @@ describe('color.getBackgroundColor', function() { '
' + '

Text oh heyyyy and here\'s
a link

' + ''; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] @@ -249,7 +248,7 @@ describe('color.getBackgroundColor', function() { '
' + '

Text oh heyyyy and here\'s
a link

' + ''; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] @@ -266,6 +265,7 @@ describe('color.getBackgroundColor', function() { '
' + '
Text
' + '
'; + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] @@ -281,6 +281,7 @@ describe('color.getBackgroundColor', function() { '
' + '
Text
' + ''; + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] @@ -304,7 +305,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'), parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); assert.equal(actual.red, expected.red); @@ -326,7 +327,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'), parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); assert.equal(actual.red, expected.red); @@ -348,7 +349,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'), parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); assert.equal(actual.red, expected.red); @@ -370,7 +371,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'), parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); assert.equal(actual.red, expected.red); @@ -392,7 +393,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'), parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); assert.equal(actual.red, expected.red); @@ -414,7 +415,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'), parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); assert.equal(actual.red, expected.red); @@ -433,7 +434,7 @@ describe('color.getBackgroundColor', function() { var bgNodes = []; var target = fixture.querySelector('#target'); var parent = fixture.querySelector('#parent'); - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(255, 255, 255, 1); assert.equal(actual.red, expected.red); @@ -450,7 +451,7 @@ describe('color.getBackgroundColor', function() { ''; var target = fixture.querySelector('#target'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(0, 0, 0, 1); assert.equal(actual.red, expected.red); @@ -468,7 +469,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(255, 255, 255, 1); assert.equal(actual.red, expected.red); @@ -494,7 +495,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'), parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(243, 243, 243, 1); assert.equal(actual.red, expected.red); @@ -516,7 +517,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(255, 255, 255, 1); @@ -537,7 +538,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var parent = fixture.querySelector('#parent'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes); var expected = new axe.commons.color.Color(255, 255, 255, 1); @@ -560,7 +561,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var shifted = fixture.querySelector('#shifted'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, bgNodes, false); var expected = new axe.commons.color.Color(0, 0, 0, 1); @@ -586,7 +587,7 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var outcome = axe.commons.color.getBackgroundColor(target, bgNodes, false); assert.isNull(outcome); assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'bgImage'); @@ -606,66 +607,19 @@ describe('color.getBackgroundColor', function() { var target = fixture.querySelector('#target'); var bgNodes = []; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var outcome = axe.commons.color.getBackgroundColor(target, bgNodes, false); assert.isNull(outcome); assert.equal(axe.commons.color.incompleteData.get('bgColor'), 'imgNode'); }); - it('does not change the scroll when scroll is disabled', function() { - fixture.innerHTML = - '
' + - '
' + - '
'; - var targetEl = fixture.querySelector('#target'); - var bgNodes = []; - window.scroll(0, 0); - - axe.testUtils.flatTreeSetup(fixture.firstChild); - axe.commons.color.getBackgroundColor(targetEl, bgNodes, true); - - assert.equal(window.pageYOffset, 0); - }); - - it('scrolls into view when scroll is enabled', function() { - fixture.innerHTML = - '
' + - '
' + - '
'; - var targetEl = fixture.querySelector('#target'); - var bgNodes = []; - window.scroll(0, 0); - - axe.testUtils.flatTreeSetup(fixture.firstChild); - axe.commons.color.getBackgroundColor(targetEl, bgNodes, false); - - assert.notEqual(window.pageYOffset, 0); - }); - - it('scrolls horizontally into view when scroll is enabled', function() { - fixture.innerHTML = - '
long text to test scrolling
'; - var targetEl = fixture.querySelector('#target'); - var bgNodes = []; - window.scroll(100, 0); - - axe.testUtils.flatTreeSetup(fixture.firstChild); - axe.commons.color.getBackgroundColor(targetEl, bgNodes, false); - - assert.equal(document.documentElement.scrollLeft, 0); - }); - it('returns elements with negative z-index', function() { fixture.innerHTML = '
' + '
Some text
'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] @@ -687,7 +641,7 @@ describe('color.getBackgroundColor', function() { var orig = document.body.style.background; document.body.style.background = '#FFF'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] @@ -708,7 +662,7 @@ describe('color.getBackgroundColor', function() { var orig = document.body.style.background; document.body.style.background = '#F00'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] @@ -727,7 +681,7 @@ describe('color.getBackgroundColor', function() { var orig = document.body.style.background; document.body.style.background = '#F00'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] @@ -746,7 +700,7 @@ describe('color.getBackgroundColor', function() { var orig = document.documentElement.style.background; document.documentElement.style.background = '#0F0'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor( document.getElementById('target'), [] @@ -760,46 +714,20 @@ describe('color.getBackgroundColor', function() { assert.closeTo(actual.alpha, expected.alpha, 0.1); }); - it('avoids scrolling elements with overflow:hidden', function() { - fixture.innerHTML = - '
' + - '
' + - '
' + - '
Some text here
' + - '
' + - '
' + - '
' + - '
R_20
' + - '
'; - - // This shouldn't cause a scroll - var target1 = document.getElementById('tgt1'); - axe.testUtils.flatTreeSetup(fixture.firstChild); - axe.commons.color.getBackgroundColor(target1, []); - - // Otherwise this would not be on the black bg anymore: - var target2 = document.getElementById('tgt2'); - var actual = axe.commons.color.getBackgroundColor(target2, []); - - assert.closeTo(actual.red, 0, 0.5); - assert.closeTo(actual.green, 0, 0.5); - assert.closeTo(actual.blue, 0, 0.5); - assert.closeTo(actual.alpha, 1, 0.1); - }); - - it('does returns null for inline elements that do not fit the viewport', function() { + it('should return background color for inline elements that do not fit the viewport', function() { var html = ''; - for (var i = 0; i < 1000; i++) { + for (var i = 0; i < 300; i++) { html += 'foo
'; } fixture.innerHTML = '' + html + ''; - axe.testUtils.flatTreeSetup(fixture.firstChild); - var outcome = axe.commons.color.getBackgroundColor(fixture.firstChild, []); - assert.isNull(outcome); - assert.equal( - axe.commons.color.incompleteData.get('bgColor'), - 'outsideViewport' - ); + axe.testUtils.flatTreeSetup(fixture); + var actual = axe.commons.color.getBackgroundColor(fixture, []); + var expected = new axe.commons.color.Color(255, 255, 255, 1); + + assert.closeTo(actual.red, expected.red, 0.5); + assert.closeTo(actual.green, expected.green, 0.5); + assert.closeTo(actual.blue, expected.blue, 0.5); + assert.closeTo(actual.alpha, expected.alpha, 0.1); }); it('should return the body bgColor when content does not overlap', function() { @@ -807,7 +735,7 @@ describe('color.getBackgroundColor', function() { '
' + '
Text' + '
'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var target = fixture.querySelector('#target'); var actual = axe.commons.color.getBackgroundColor(target, []); @@ -827,6 +755,7 @@ describe('color.getBackgroundColor', function() { document.body.style.height = '1px'; document.body.style.background = '#000'; + axe.testUtils.flatTreeSetup(fixture); var target = fixture.querySelector('#target'); var actual = axe.commons.color.getBackgroundColor(target, []); @@ -851,6 +780,7 @@ describe('color.getBackgroundColor', function() { document.body.style.background = '#0f0'; document.documentElement.style.background = '#f00'; + axe.testUtils.flatTreeSetup(fixture); var target = fixture.querySelector('#target'); var actual = axe.commons.color.getBackgroundColor(target, []); @@ -872,7 +802,7 @@ describe('color.getBackgroundColor', function() { '
' + 'Text' + '
'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var target = shadow.querySelector('#shadowTarget'); var actual = axe.commons.color.getBackgroundColor(target, []); @@ -892,7 +822,7 @@ describe('color.getBackgroundColor', function() { var shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = 'Text'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var target = shadow.querySelector('#shadowTarget'); var actual = axe.commons.color.getBackgroundColor(target, [], false); @@ -914,7 +844,7 @@ describe('color.getBackgroundColor', function() { '
'; var target = shadow.querySelector('#target'); - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, []); var expected = new axe.commons.color.Color(0, 0, 0, 1); @@ -934,10 +864,10 @@ describe('color.getBackgroundColor', function() { var shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = '
Text
'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var target = shadow.querySelector('#shadowTarget'); - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(target, []); assert.equal(actual.red, 255); assert.equal(actual.green, 255); @@ -958,7 +888,7 @@ describe('color.getBackgroundColor', function() { shadow1.innerHTML = '
'; var elm2 = document.querySelector('#elm2'); - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(elm2, []); assert.equal(actual.red, 0); assert.equal(actual.blue, 0); @@ -987,7 +917,7 @@ describe('color.getBackgroundColor', function() { '
'; var elm3 = shadow2.querySelector('#elm3'); - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var actual = axe.commons.color.getBackgroundColor(elm3, []); assert.closeTo(actual.red, 128, 2); assert.closeTo(actual.blue, 128, 2); @@ -1005,7 +935,7 @@ describe('color.getBackgroundColor', function() { var shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = '
Text
More text
'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var target = shadow.querySelector('#shadowTarget'); var actual = axe.commons.color.getBackgroundColor(target, []); assert.equal(actual.red, 0); @@ -1024,7 +954,7 @@ describe('color.getBackgroundColor', function() { var shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = '
Text
More text
'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var target = shadow.querySelector('#shadowTarget'); var actual = axe.commons.color.getBackgroundColor(target, []); assert.isNull(actual); @@ -1039,7 +969,7 @@ describe('color.getBackgroundColor', function() { div.innerHTML = 'Link'; var shadow = div.attachShadow({ mode: 'open' }); shadow.innerHTML = '

'; - axe.testUtils.flatTreeSetup(fixture.firstChild); + axe.testUtils.flatTreeSetup(fixture); var linkElm = div.querySelector('a'); var actual = axe.commons.color.getBackgroundColor(linkElm, []); assert.equal(actual.red, 0); diff --git a/test/commons/color/get-foreground-color.js b/test/commons/color/get-foreground-color.js index d96c580388..e11da14546 100644 --- a/test/commons/color/get-foreground-color.js +++ b/test/commons/color/get-foreground-color.js @@ -19,6 +19,7 @@ describe('color.getForegroundColor', function() { ' background-color: rgba(0, 128, 0, 0.5);">' + 'This is my text' + '
'; + axe.testUtils.flatTreeSetup(fixture); var target = fixture.querySelector('#target'); var actual = axe.commons.color.getForegroundColor(target); var expected = new axe.commons.color.Color(32, 32, 64, 1); @@ -35,6 +36,7 @@ describe('color.getForegroundColor', function() { ' background-color: green; opacity: 0.5;">' + 'This is my text' + ''; + axe.testUtils.flatTreeSetup(fixture); var target = fixture.querySelector('#target'); var actual = axe.commons.color.getForegroundColor(target); var expected = new axe.commons.color.Color(32, 32, 64, 1); @@ -51,6 +53,7 @@ describe('color.getForegroundColor', function() { '
' + 'This is my text' + '
'; + axe.testUtils.flatTreeSetup(fixture); var target = fixture.querySelector('#target'); var actual = axe.commons.color.getForegroundColor(target); var expected = new axe.commons.color.Color(119.5, 119.5, 119.5, 1); @@ -68,6 +71,7 @@ describe('color.getForegroundColor', function() { '
' + 'This is my text' + '
'; + axe.testUtils.flatTreeSetup(fixture); var target = fixture.querySelector('#target'); var actual = axe.commons.color.getForegroundColor(target); var expected = new axe.commons.color.Color(119.5, 119.5, 119.5, 1); @@ -83,6 +87,7 @@ describe('color.getForegroundColor', function() { 'background-color: #800000; background-image: url(image.png);">' + '
' + '
'; + axe.testUtils.flatTreeSetup(fixture); var target = fixture.querySelector('#target'); var actual = axe.commons.color.getForegroundColor(target); assert.equal(axe.commons.color.incompleteData.get('fgColor'), 'bgImage'); @@ -94,6 +99,7 @@ describe('color.getForegroundColor', function() { '
' + '
' + '
'; + axe.testUtils.flatTreeSetup(fixture); var target = fixture.querySelector('#target'); var actual = axe.commons.color.getForegroundColor(target); var expected = new axe.commons.color.Color(0, 0, 128, 1); @@ -123,6 +129,7 @@ describe('color.getForegroundColor', function() { shadow.innerHTML = '
'; + axe.testUtils.flatTreeSetup(fixture); var target = shadow.querySelector('#target'); var actual = axe.commons.color.getForegroundColor(target); var expected = new axe.commons.color.Color(0, 0, 128, 1); diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index 52adc87cce..f2321900f4 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -3,7 +3,18 @@ describe('dom.getElementStack', function() { var fixture = document.getElementById('fixture'); var getElementStack = axe.commons.dom.getElementStack; - var queryFixture = axe.testUtils.queryFixture; + var getClientElementStack = axe.commons.dom.getClientElementStack; + var shadowSupported = axe.testUtils.shadowSupport.v1; + + function mapToIDs(stack) { + return stack + .map(function(node) { + return node.id; + }) + .filter(function(id) { + return !!id; + }); + } afterEach(function() { fixture.innerHTML = ''; @@ -11,343 +22,459 @@ describe('dom.getElementStack', function() { describe('stack order', function() { it('should return stack in DOM order of non-positioned elements', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '

Hello World

' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + '

Hello World

' + + '
' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '2', '1', 'fixture']); }); it('should not return elements outside of the stack', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - 'Foo' + - '

Hello World

' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + 'Foo' + + '

Hello World

' + + '
' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '2', '1', 'fixture']); }); it('should should handle positioned elements without z-index', function() { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index - var vNode = queryFixture( + fixture.innerHTML = '
' + - 'DIV #1
position:absolute;
' + - '
' + - 'DIV #2
position:relative;
' + - '
' + - 'DIV #3
position:relative;
' + - '
' + - 'DIV #4
position:absolute;
' + - '
' + - 'DIV #5
position:static;
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + 'DIV #1
position:absolute;' + + '
' + + 'DIV #2
position:relative;
' + + '
' + + 'DIV #3
position:relative;
' + + '
' + + 'DIV #4
position:absolute;
' + + '
' + + 'DIV #5
position:static;
'; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['4', '3', '2', '1', 'target', 'fixture']); }); it('should handle floating and positioned elements without z-index', function() { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float - var vNode = queryFixture( + fixture.innerHTML = '
' + - 'DIV #1
position:absolute;
' + - '
' + - 'DIV #2
float:left;
' + - '
' + - 'DIV #3
no positioning
' + - '
' + - 'DIV #4
position:absolute;
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + 'DIV #1
position:absolute;' + + '
' + + 'DIV #2
float:left;
' + + '
' + + 'DIV #3
no positioning
' + + '
' + + 'DIV #4
position:absolute;
'; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['4', '1', '2', 'target', 'fixture']); }); it('should handle z-index positioned elements in the same stacking context', function() { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_context_example_1 - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
DIV #1' + - '
position:relative;' + - '
' + - '
DIV #2' + - '
position:absolute;' + - '
z-index:1;' + - '
' + - '
' + - '
' + - '
' + - '
DIV #3' + - '
position:relative;' + - '
' + - '
DIV #4' + - '
position:absolute;' + - '
z-index:2;' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
DIV #1' + + '
position:relative;' + + '
' + + '
DIV #2' + + '
position:absolute;' + + '
z-index:1;' + + '
' + + '' + + '
' + + '
' + + '
DIV #3' + + '
position:relative;' + + '
' + + '
DIV #4' + + '
position:absolute;' + + '
z-index:2;' + + '
' + + '
'; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['4', '2', '3', 'target', 'fixture']); }); it('should handle z-index positioned elements in different stacking contexts', function() { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_context_example_2 - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
DIV #1' + - '
position:relative;' + - '
' + - '
DIV #2' + - '
position:absolute;' + - '
z-index:2;' + - '
' + - '
' + - '
' + - '
' + - '
DIV #3' + - '
position:relative;' + - '
' + - '
DIV #4' + - '
position:absolute;' + - '
z-index:10;' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
DIV #1' + + '
position:relative;' + + '
' + + '
DIV #2' + + '
position:absolute;' + + '
z-index:2;' + + '
' + + '' + + '
' + + '
' + + '
DIV #3' + + '
position:relative;' + + '
' + + '
DIV #4' + + '
position:absolute;' + + '
z-index:10;' + + '
' + + '
'; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['2', '4', '3', 'target', 'fixture']); }); it('should handle complex stacking context', function() { // see https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context - var vNode = queryFixture( + fixture.innerHTML = '
' + - 'Division Element #1
' + - 'position: relative;
' + - 'z-index: 5;' + - '
' + - '
' + - 'Division Element #2
' + - 'position: relative;
' + - 'z-index: 2;' + - '
' + - '
' + - '
' + - 'Division Element #4
' + - 'position: relative;
' + - 'z-index: 6;' + - '
' + - 'Division Element #3
' + - 'position: absolute;
' + - 'z-index: 4;' + - '
' + - 'Division Element #5
' + - 'position: relative;
' + - 'z-index: 1;' + - '
' + - '' + - '
' + - 'Division Element #6
' + - 'position: absolute;
' + - 'z-index: 3;' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + 'Division Element #1
' + + 'position: relative;
' + + 'z-index: 5;' + + '' + + '
' + + 'Division Element #2
' + + 'position: relative;
' + + 'z-index: 2;' + + '
' + + '
' + + '
' + + 'Division Element #4
' + + 'position: relative;
' + + 'z-index: 6;' + + '
' + + 'Division Element #3
' + + 'position: absolute;
' + + 'z-index: 4;' + + '
' + + 'Division Element #5
' + + 'position: relative;
' + + 'z-index: 1;' + + '
' + + '' + + '
' + + 'Division Element #6
' + + 'position: absolute;
' + + 'z-index: 3;' + + '
' + + '
'; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['1', '4', 'target', '5', '3', '2']); }); it('should correctly order children of position elements without z-index', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '1', 'fixture']); }); it('should correctly order children of position elements with z-index', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '1', 'fixture']); }); it('should handle modals on top of the stack', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '

Hello World

' + - '
' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + '

Hello World

' + + '
' + + '' + + '
'; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['3', 'target', '2', '1', 'fixture']); }); it('should handle "pointer-events:none"', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '

Hello World

' + - '
' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + '

Hello World

' + + '
' + + '' + + '
'; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['3', 'target', '2', '1', 'fixture']); }); it('should return elements left out by document.elementsFromPoint', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + '' + + '
' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); }); it('should not return elements that do not fully cover the target', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '

Text oh heyyyy and here\'s
a link

' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + '

Text oh heyyyy and here\'s
a link

' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '1', 'fixture']); }); it('should not return parent elements that do not fully cover the target', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
Text
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
Text
' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target']); }); it('should return elements that partially cover the target', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '
Text
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + '
Text
' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '2', '1', 'fixture']); }); it('should handle negative z-index', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '

Hello World

' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + '

Hello World

' + + '
' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['1', 'fixture', 'target', '2']); }); + + (shadowSupported ? it : xit)( + 'should sort shadow dom elements correctly', + function() { + fixture.innerHTML = '
'; + var container = fixture.querySelector('#container'); + var shadow = container.attachShadow({ mode: 'open' }); + shadow.innerHTML = 'Text'; + axe.testUtils.flatTreeSetup(fixture); + + var target = shadow.querySelector('#shadowTarget'); + var stack = mapToIDs(getElementStack(target)); + assert.deepEqual(stack, ['shadowTarget', 'container', 'fixture']); + } + ); + + (shadowSupported ? it : xit)( + 'should sort nested shadow dom elements correctly', + function() { + fixture.innerHTML = '
'; + var container = fixture.querySelector('#container'); + var shadow = container.attachShadow({ mode: 'open' }); + + shadow.innerHTML = '
'; + var nestedContainer = shadow.querySelector('#shadowContainer'); + var nestedShadow = nestedContainer.attachShadow({ mode: 'open' }); + + nestedShadow.innerHTML = 'Text'; + axe.testUtils.flatTreeSetup(fixture); + + var target = nestedShadow.querySelector('#shadowTarget'); + var stack = mapToIDs(getElementStack(target)); + assert.deepEqual(stack, [ + 'shadowTarget', + 'shadowContainer', + 'container', + 'fixture' + ]); + } + ); + + (shadowSupported ? it : xit)( + 'should sort positioned shadow elements correctly', + function() { + fixture.innerHTML = '
'; + var container = fixture.querySelector('#container'); + var shadow = container.attachShadow({ mode: 'open' }); + + shadow.innerHTML = + '
'; + var nestedContainer = shadow.querySelector('#shadowContainer'); + var nestedShadow = nestedContainer.attachShadow({ mode: 'open' }); + + nestedShadow.innerHTML = 'Text'; + axe.testUtils.flatTreeSetup(fixture); + + var target = nestedShadow.querySelector('#shadowTarget'); + var stack = mapToIDs(getElementStack(target)); + assert.deepEqual(stack, [ + 'container', + 'fixture', + 'shadowTarget', + 'shadowContainer' + ]); + } + ); + + (shadowSupported ? it : xit)( + 'should sort shadow elements with different trees correctly', + function() { + fixture.innerHTML = + '
'; + var container1 = fixture.querySelector('#container1'); + var shadow1 = container1.attachShadow({ mode: 'open' }); + shadow1.innerHTML = 'Text'; + + var container2 = fixture.querySelector('#container2'); + var shadow2 = container2.attachShadow({ mode: 'open' }); + shadow2.innerHTML = 'Container 2 text'; + axe.testUtils.flatTreeSetup(fixture); + + var target = shadow1.querySelector('#shadowTarget'); + var stack = mapToIDs(getElementStack(target)); + assert.deepEqual(stack, [ + '1', + 'container2', + 'shadowTarget', + 'container1', + 'fixture' + ]); + } + ); }); describe('scroll regions', function() { + var origHeight = document.documentElement.style.height; + var origOverflow = document.documentElement.style.overflowY; + + afterEach(function() { + document.documentElement.style.height = origHeight; + document.documentElement.style.overflowY = origOverflow; + }); + it('should return stack of scroll regions', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '
' + - '

Hello World

' + - '
' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); }); it('should return stack when scroll region is larger than parent', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '
' + - '

Hello World

' + - '
' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); }); it('should return stack of recursive scroll regions', function() { - var vNode = queryFixture( + fixture.innerHTML = '
' + - '
' + - '
' + - '
' + - '
' + - '

Hello World

' + - '
' + - '
' + - '
' + - '
' + - '
' - ); - var stack = getElementStack(vNode).map(function(vNode) { - return vNode.actualNode.id; - }); + '
' + + '
' + + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + '
' + + '
' + + ''; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); assert.deepEqual(stack, ['target', '5', '4', '3', '2', '1', 'fixture']); }); + + it('should handle html as a scroll region', function() { + fixture.innerHTML = + '
' + + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + '
'; + document.documentElement.style.height = '5000px'; + document.documentElement.style.overflowY = 'scroll'; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stack = mapToIDs(getElementStack(target)); + assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); + }); + }); + + describe('dom.getClientElementStack', function() { + it('should return array of client rects', function() { + fixture.innerHTML = + '
' + + '' + + 'Hello
World' + + '
' + + '
'; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var stacks = getClientElementStack(target).map(mapToIDs); + assert.deepEqual(stacks, [ + ['2', 'target', '1', 'fixture'], + ['3', 'target', '1', 'fixture'] + ]); + }); }); }); diff --git a/test/commons/dom/shadow-elements-from-point.js b/test/commons/dom/shadow-elements-from-point.js index f11a9d860c..ccd51fd4a6 100644 --- a/test/commons/dom/shadow-elements-from-point.js +++ b/test/commons/dom/shadow-elements-from-point.js @@ -23,6 +23,7 @@ describe('dom.shadowElementsFromPoint', function() { var shadow2 = container2.attachShadow({ mode: 'open' }); shadow2.innerHTML = 'Text'; var shadowSpan = shadow2.querySelector('span'); + axe.testUtils.flatTreeSetup(fixture); container.scrollIntoView(); diff --git a/test/commons/dom/visually-contains.js b/test/commons/dom/visually-contains.js index 0c8e6c2530..3c5bb94718 100644 --- a/test/commons/dom/visually-contains.js +++ b/test/commons/dom/visually-contains.js @@ -2,6 +2,7 @@ describe('dom.visuallyContains', function() { 'use strict'; var fixture = document.getElementById('fixture'); + var queryFixture = axe.testUtils.queryFixture; var shadowSupported = axe.testUtils.shadowSupport.v1; @@ -10,78 +11,124 @@ describe('dom.visuallyContains', function() { }); it('should return true when element is trivially contained', function() { - fixture.innerHTML = + var target = queryFixture( '
' + - '
' + - '
'; - var target = fixture.querySelector('#target'); - assert.isTrue(axe.commons.dom.visuallyContains(target, target.parentNode)); + '
' + + '
' + ); + assert.isTrue( + axe.commons.dom.visuallyContains( + target.actualNode, + target.parent.actualNode + ) + ); }); it('should return false when overflow is hidden', function() { - fixture.innerHTML = + var target = queryFixture( '
' + - '
' + - '
'; - var target = fixture.querySelector('#target'); - var result = axe.commons.dom.visuallyContains(target, target.parentNode); + '
' + + '
' + ); + var result = axe.commons.dom.visuallyContains( + target.actualNode, + target.parent.actualNode + ); assert.isFalse(result); }); it('should return false when absolutely positioned content does not overlap', function() { - fixture.innerHTML = + var target = queryFixture( '
' + - '
Text' + - '
'; - var target = fixture.querySelector('#target'); - var result = axe.commons.dom.visuallyContains(target, target.parentNode); + '
Text' + + '
' + ); + var result = axe.commons.dom.visuallyContains( + target.actualNode, + target.parent.actualNode + ); assert.isFalse(result); }); it('should return false when element is outside of margin', function() { - fixture.innerHTML = + var target = queryFixture( '
' + - '
' + - '
'; - var target = fixture.querySelector('#target'); - assert.isFalse(axe.commons.dom.visuallyContains(target, target.parentNode)); + '
' + + '
' + ); + assert.isFalse( + axe.commons.dom.visuallyContains( + target.actualNode, + target.parent.actualNode + ) + ); }); it('should return false when overflow is visible', function() { - fixture.innerHTML = + var target = queryFixture( '
' + - '
' + - '
'; - var target = fixture.querySelector('#target'); - assert.isFalse(axe.commons.dom.visuallyContains(target, target.parentNode)); + '
' + + '
' + ); + assert.isFalse( + axe.commons.dom.visuallyContains( + target.actualNode, + target.parent.actualNode + ) + ); }); it('should return true when element is scrollable', function() { - fixture.innerHTML = + var target = queryFixture( '
' + - '
' + - '
'; - var target = fixture.querySelector('#target'); - assert.isTrue(axe.commons.dom.visuallyContains(target, target.parentNode)); + '
' + + '
' + ); + assert.isTrue( + axe.commons.dom.visuallyContains( + target.actualNode, + target.parent.actualNode + ) + ); }); it('should return true when element is inline', function() { // result depends on the display property of the element - fixture.innerHTML = - ''; - var target = fixture.querySelector('#target'); - assert.isTrue(axe.commons.dom.visuallyContains(target, target.parentNode)); + var target = queryFixture( + '' + ); + assert.isTrue( + axe.commons.dom.visuallyContains( + target.actualNode, + target.parent.actualNode + ) + ); }); it('should return false when element is partially contained', function() { - fixture.innerHTML = + var target = queryFixture( '
' + - '

Text
more text

'; - var target = fixture.querySelector('#target'); - var result = axe.commons.dom.visuallyContains(target, target.parentNode); + '

Text
more text

' + ); + var result = axe.commons.dom.visuallyContains( + target.actualNode, + target.parent.actualNode + ); assert.isFalse(result); }); + it('should return true when element is contained by scroll region', function() { + var target = queryFixture( + '
' + + '
' + + '
' + + '

Text

' + ); + var parent = fixture.querySelector('#parent'); + var result = axe.commons.dom.visuallyContains(target.actualNode, parent); + assert.isTrue(result); + }); + (shadowSupported ? it : xit)( 'should return true when element is visually contained across shadow boundary', function() { @@ -91,12 +138,32 @@ describe('dom.visuallyContains', function() { var shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = '
'; + axe.testUtils.flatTreeSetup(fixture); var target = shadow.querySelector('#target'); var result = axe.commons.dom.visuallyContains(target, container); assert.isTrue(result); } ); + (shadowSupported ? it : xit)( + 'should return true when element is contained by scroll region across shadow boundary', + function() { + fixture.innerHTML = + '
' + + '
' + + '
' + + '
'; + var container = fixture.querySelector('#container'); + var shadow = container.attachShadow({ mode: 'open' }); + shadow.innerHTML = '

Text

'; + axe.testUtils.flatTreeSetup(fixture); + var target = shadow.querySelector('#target'); + var parent = fixture.querySelector('#parent'); + var result = axe.commons.dom.visuallyContains(target, parent); + assert.isTrue(result); + } + ); + (shadowSupported ? it : xit)( 'should return false when element is not visually contained across shadow boundary', function() { @@ -106,6 +173,7 @@ describe('dom.visuallyContains', function() { var shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = '
'; + axe.testUtils.flatTreeSetup(fixture); var target = shadow.querySelector('#target'); var result = axe.commons.dom.visuallyContains(target, container); assert.isFalse(result); @@ -121,7 +189,7 @@ describe('dom.visuallyContains', function() { var shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = '
Text
'; - + axe.testUtils.flatTreeSetup(fixture); var target = shadow.querySelector('#shadowTarget'); var result = axe.commons.dom.visuallyContains(target, container); assert.isFalse(result); @@ -137,7 +205,7 @@ describe('dom.visuallyContains', function() { var shadow = container.attachShadow({ mode: 'open' }); shadow.innerHTML = '
Text
'; - + axe.testUtils.flatTreeSetup(fixture); var target = shadow.querySelector('#shadowTarget'); var result = axe.commons.dom.visuallyContains(target, container); assert.isFalse(result);