diff --git a/lib/commons/dom/get-element-stack.js b/lib/commons/dom/get-element-stack.js new file mode 100644 index 0000000000..2ce44bc9ed --- /dev/null +++ b/lib/commons/dom/get-element-stack.js @@ -0,0 +1,465 @@ +/* global dom */ + +// 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) + +/** + * Determine if node produces a stacking context. + * References: + * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context + * https://github.com/gwwar/z-context/blob/master/devtools/index.js + * @param {VirtualNode} vNode + * @return {Boolean} + */ +function isStackingContext(vNode) { + const node = vNode.actualNode; + + //the root element (HTML) + if ( + !node || + node.nodeName === 'HTML' || + node.nodeName === '#document-fragment' + ) { + return true; + } + + // position: fixed or sticky + if ( + vNode.getComputedStylePropertyValue('position') === 'fixed' || + vNode.getComputedStylePropertyValue('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' + ) { + return true; + } + + // elements with an opacity value less than 1. + if (vNode.getComputedStylePropertyValue('opacity') !== '1') { + return true; + } + + // elements with a transform value other than "none" + const transform = + vNode.getComputedStylePropertyValue('-webkit-transform') || + vNode.getComputedStylePropertyValue('-ms-transform') || + vNode.getComputedStylePropertyValue('transform') || + 'none'; + + if (transform !== 'none') { + return true; + } + + // elements with a mix-blend-mode value other than "normal" + if ( + vNode.getComputedStylePropertyValue('mix-blend-mode') && + vNode.getComputedStylePropertyValue('mix-blend-mode') !== 'normal' + ) { + return true; + } + + // elements with a filter value other than "none" + if ( + vNode.getComputedStylePropertyValue('filter') && + vNode.getComputedStylePropertyValue('filter') !== 'none' + ) { + return true; + } + + // elements with a perspective value other than "none" + if ( + vNode.getComputedStylePropertyValue('perspective') && + vNode.getComputedStylePropertyValue('perspective') !== 'none' + ) { + return true; + } + + // element with a clip-path value other than "none" + if ( + vNode.getComputedStylePropertyValue('clip-path') && + vNode.getComputedStylePropertyValue('clip-path') !== 'none' + ) { + return true; + } + + // element with a mask value other than "none" + const mask = + vNode.getComputedStylePropertyValue('-webkit-mask') || + vNode.getComputedStylePropertyValue('mask') || + 'none'; + + if (mask !== 'none') { + return true; + } + + // element with a mask-image value other than "none" + const maskImage = + vNode.getComputedStylePropertyValue('-webkit-mask-image') || + vNode.getComputedStylePropertyValue('mask-image') || + 'none'; + + if (maskImage !== 'none') { + return true; + } + + // element with a mask-border value other than "none" + const maskBorder = + vNode.getComputedStylePropertyValue('-webkit-mask-border') || + vNode.getComputedStylePropertyValue('mask-border') || + 'none'; + + if (maskBorder !== 'none') { + return true; + } + + // elements with isolation set to "isolate" + if (vNode.getComputedStylePropertyValue('isolation') === 'isolate') { + return true; + } + + // 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' + ) { + return true; + } + + // elements with -webkit-overflow-scrolling set to "touch" + if ( + vNode.getComputedStylePropertyValue('-webkit-overflow-scrolling') === + 'touch' + ) { + return true; + } + + // element with a contain value of "layout" or "paint" or a composite value + // that includes either of them (i.e. contain: strict, contain: content). + const contain = vNode.getComputedStylePropertyValue('contain'); + if (['layout', 'paint', 'strict', 'content'].includes(contain)) { + return true; + } + + // 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 ( + [ + 'flex', + 'inline-flex', + 'inline flex', + 'grid', + 'inline-grid', + 'inline grid' + ].includes(parentDsiplay) + ) { + return true; + } + } + + return false; +} + +/** + * Return the index order of how to position this element. return nodes in non-positioned, floating, positioned order + * References: + * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index + * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float + * https://drafts.csswg.org/css2/visuren.html#layers + * @param {VirtualNode} vNode + * @return {Number} + */ +function getPositionOrder(vNode) { + if (vNode.getComputedStylePropertyValue('position') === 'static') { + // 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks. + if ( + vNode.getComputedStylePropertyValue('display').indexOf('inline') !== -1 + ) { + return 2; + } + + // 4. the non-positioned floats. + if (vNode.getComputedStylePropertyValue('float') !== 'none') { + return 1; + } + + // 3. the in-flow, non-inline-level, non-positioned descendants. + if (vNode.getComputedStylePropertyValue('position') === 'static') { + return 0; + } + } + + // 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0. + return 3; +} + +/** + * Visually sort nodes based on their stack order + * References: + * https://drafts.csswg.org/css2/visuren.html#layers + * @param {VirtualNode} + * @param {VirtualNode} + */ +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; + } + + // 7. the child stacking contexts with positive stack levels (least positive first). + if (b._stackingOrder[i] > a._stackingOrder[i]) { + return 1; + } + + // 2. the child stacking contexts with negative stack levels (most negative first). + if (b._stackingOrder[i] < a._stackingOrder[i]) { + return -1; + } + } + + // 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; + const aPosition = getPositionOrder(a); + const bPosition = getPositionOrder(b); + + // a child of a positioned element should also be on top of the parent + if (aPosition === bPosition || isDescendant) { + return DOMOrder; + } + + return bPosition - aPosition; +} + +/** + * Determine the stacking order of an element. The stacking order is an array of + * zIndex values for each stacking context parent. + * @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') + ); + } + if (isStackingContext(vNode)) { + stackingOrder.push(0); + } + + return stackingOrder; +} + +/** + * Return the parent node that is a scroll region. + * @param {VirtualNode} + * @return {VirtualNode|null} + */ +function findScrollRegionParent(vNode) { + let scrollRegionParent = null; + let vNodeParent = vNode.parent; + let checkedNodes = [vNode]; + + while (vNodeParent) { + if (vNodeParent._scrollRegionParent) { + scrollRegionParent = vNodeParent._scrollRegionParent; + break; + } + + if (axe.utils.getScroll(vNodeParent.actualNode)) { + scrollRegionParent = vNodeParent; + break; + } + + checkedNodes.push(vNodeParent); + vNodeParent = vNodeParent.parent; + } + + // cache result of parent scroll region so we don't have to look up the entire + // tree again for a child node + checkedNodes.forEach( + vNode => (vNode._scrollRegionParent = scrollRegionParent) + ); + 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} + * @param {VirtualNode} + */ +function addNodeToGrid(grid, vNode) { + // save a reference to where this element is in the grid so we + // can find it even if it's in a subgrid + vNode._grid = grid; + + vNode.clientRects.forEach(rect => { + const startRow = Math.floor(getDomPosition(rect, 'y') / gridSize); + const startCol = Math.floor(getDomPosition(rect, 'x') / gridSize); + + const endRow = Math.floor( + (getDomPosition(rect, 'y') + rect.height) / gridSize + ); + const endCol = Math.floor( + (getDomPosition(rect, 'x') + rect.width) / gridSize + ); + + 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); + } + } + } + }); +} + +/** + * Setup the 2d grid and add every element to it. + */ +function createGrid() { + const 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; + } + + vNode._stackingOrder = getStackingOrder(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; + } + + const scrollRegionParent = findScrollRegionParent(vNode); + const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid; + + if (axe.utils.getScroll(vNode.actualNode)) { + const subGrid = { + container: vNode, + cells: [] + }; + vNode._subGrid = subGrid; + } + + addNodeToGrid(grid, vNode); + }); +} + +/** + * 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 grid = vNode._grid; + + if (!grid) { + return []; + } + + const boundingRect = vNode.boundingClientRect; + + // use center point of rect + let x = getDomPosition(boundingRect, 'x') + boundingRect.width / 2; + let y = getDomPosition(boundingRect, 'y') + boundingRect.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 + // point of the element is <1px outside of another elements bounding rect, + // 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); + 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'); + + // 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 + ); + }); + }); + + if (grid.container) { + stack = dom.getElementStack(grid.container, true).concat(stack); + } + + if (!recursed) { + stack.sort(visuallySort); + } + + return stack; +}; diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js index f4aeb57f3a..e4b6e47f45 100644 --- a/lib/core/base/virtual-node/virtual-node.js +++ b/lib/core/base/virtual-node/virtual-node.js @@ -61,6 +61,23 @@ class VirtualNode extends axe.AbstractVirtualNode { return this.actualNode.hasAttribute(attrName); } + /** + * Return a property of the computed style for this element and cache the result. This is much faster than called `getPropteryValue` every time. + * @see https://jsperf.com/get-property-value + * @return {String} + */ + getComputedStylePropertyValue(property) { + const key = 'computedStyle_' + property; + if (!this._cache.hasOwnProperty(key)) { + if (!this._cache.hasOwnProperty('computedStyle')) { + this._cache.computedStyle = window.getComputedStyle(this.actualNode); + } + + this._cache[key] = this._cache.computedStyle.getPropertyValue(property); + } + return this._cache[key]; + } + /** * Determine if the element is focusable and cache the result. * @return {Boolean} True if the element is focusable, false otherwise. @@ -82,4 +99,28 @@ class VirtualNode extends axe.AbstractVirtualNode { } return this._cache.tabbableElements; } + + /** + * Return the client rects for this element and cache the result. + * @return {DOMRect[]} + */ + get clientRects() { + if (!this._cache.hasOwnProperty('clientRects')) { + this._cache.clientRects = Array.from( + this.actualNode.getClientRects() + ).filter(rect => rect.width > 0); + } + return this._cache.clientRects; + } + + /** + * Return the bounding rect for this element and cache the result. + * @return {DOMRect} + */ + get boundingClientRect() { + if (!this._cache.hasOwnProperty('boundingClientRect')) { + this._cache.boundingClientRect = this.actualNode.getBoundingClientRect(); + } + return this._cache.boundingClientRect; + } } diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js new file mode 100644 index 0000000000..52adc87cce --- /dev/null +++ b/test/commons/dom/get-element-stack.js @@ -0,0 +1,353 @@ +describe('dom.getElementStack', function() { + 'use strict'; + + var fixture = document.getElementById('fixture'); + var getElementStack = axe.commons.dom.getElementStack; + var queryFixture = axe.testUtils.queryFixture; + + afterEach(function() { + fixture.innerHTML = ''; + }); + + describe('stack order', function() { + it('should return stack in DOM order of non-positioned elements', function() { + var vNode = queryFixture( + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '2', '1', 'fixture']); + }); + + it('should not return elements outside of the stack', function() { + var vNode = queryFixture( + '
' + + '
' + + 'Foo' + + '

Hello World

' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + 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( + '
' + + '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; + }); + 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( + '
' + + '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; + }); + 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( + '
' + + '
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; + }); + 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( + '
' + + '
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; + }); + 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( + '
' + + '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; + }); + assert.deepEqual(stack, ['1', '4', 'target', '5', '3', '2']); + }); + + it('should correctly order children of position elements without z-index', function() { + var vNode = queryFixture( + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '1', 'fixture']); + }); + + it('should correctly order children of position elements with z-index', function() { + var vNode = queryFixture( + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '1', 'fixture']); + }); + + it('should handle modals on top of the stack', function() { + var vNode = queryFixture( + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['3', 'target', '2', '1', 'fixture']); + }); + + it('should handle "pointer-events:none"', function() { + var vNode = queryFixture( + '
' + + '
' + + '

Hello World

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

Text oh heyyyy and here\'s
a link

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

Hello World

' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['1', 'fixture', 'target', '2']); + }); + }); + + describe('scroll regions', function() { + it('should return stack of scroll regions', function() { + var vNode = queryFixture( + '
' + + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); + }); + + it('should return stack when scroll region is larger than parent', function() { + var vNode = queryFixture( + '
' + + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '3', '2', '1', 'fixture']); + }); + + it('should return stack of recursive scroll regions', function() { + var vNode = queryFixture( + '
' + + '
' + + '
' + + '
' + + '
' + + '

Hello World

' + + '
' + + '
' + + '
' + + '
' + + '
' + ); + var stack = getElementStack(vNode).map(function(vNode) { + return vNode.actualNode.id; + }); + assert.deepEqual(stack, ['target', '5', '4', '3', '2', '1', 'fixture']); + }); + }); +}); diff --git a/test/core/base/virtual-node/virtual-node.js b/test/core/base/virtual-node/virtual-node.js index df1e352db1..47c96a78d1 100644 --- a/test/core/base/virtual-node/virtual-node.js +++ b/test/core/base/virtual-node/virtual-node.js @@ -190,5 +190,114 @@ describe('VirtualNode', function() { assert.equal(count, 1); }); }); + + describe('getComputedStylePropertyValue', function() { + var computedStyle; + + beforeEach(function() { + computedStyle = window.getComputedStyle; + }); + + afterEach(function() { + window.getComputedStyle = computedStyle; + }); + + it('should call window.getComputedStyle and return the property', function() { + var called = false; + window.getComputedStyle = function() { + called = true; + return { + getPropertyValue: function() { + return 'result'; + } + }; + }; + var vNode = new VirtualNode(node); + var result = vNode.getComputedStylePropertyValue('prop'); + + assert.isTrue(called); + assert.equal(result, 'result'); + }); + + it('should only call window.getComputedStyle and getPropertyValue once', function() { + var computedCount = 0; + var propertyCount = 0; + window.getComputedStyle = function() { + computedCount++; + return { + getPropertyValue: function() { + propertyCount++; + } + }; + }; + var vNode = new VirtualNode(node); + vNode.getComputedStylePropertyValue('prop'); + vNode.getComputedStylePropertyValue('prop'); + vNode.getComputedStylePropertyValue('prop'); + assert.equal(computedCount, 1); + assert.equal(propertyCount, 1); + }); + }); + + describe('clientRects', function() { + it('should call node.getClientRects', function() { + var called = false; + node.getClientRects = function() { + called = true; + return []; + }; + var vNode = new VirtualNode(node); + vNode.clientRects; + + assert.isTrue(called); + }); + + it('should only call node.getClientRects once', function() { + var count = 0; + node.getClientRects = function() { + count++; + return []; + }; + var vNode = new VirtualNode(node); + vNode.clientRects; + vNode.clientRects; + vNode.clientRects; + assert.equal(count, 1); + }); + + it('should filter out 0 width rects', function() { + node.getClientRects = function() { + return [{ width: 10 }, { width: 0 }, { width: 20 }]; + }; + var vNode = new VirtualNode(node); + + assert.deepEqual(vNode.clientRects, [{ width: 10 }, { width: 20 }]); + }); + }); + + describe('boundingClientRect', function() { + it('should call node.getBoundingClientRect', function() { + var called = false; + node.getBoundingClientRect = function() { + called = true; + }; + var vNode = new VirtualNode(node); + vNode.boundingClientRect; + + assert.isTrue(called); + }); + + it('should only call node.getBoundingClientRect once', function() { + var count = 0; + node.getBoundingClientRect = function() { + count++; + }; + var vNode = new VirtualNode(node); + vNode.boundingClientRect; + vNode.boundingClientRect; + vNode.boundingClientRect; + assert.equal(count, 1); + }); + }); }); });