diff --git a/lib/commons/dom/get-rect-stack.js b/lib/commons/dom/get-rect-stack.js index 06172262f4..2f9c5183e3 100644 --- a/lib/commons/dom/get-rect-stack.js +++ b/lib/commons/dom/get-rect-stack.js @@ -360,6 +360,8 @@ function addNodeToGrid(grid, vNode) { const endRow = ((y + rect.height) / gridSize) | 0; const endCol = ((x + rect.width) / gridSize) | 0; + grid.numCols = Math.max(grid.numCols ?? 0, endCol); + for (let row = startRow; row <= endRow; row++) { grid.cells[row] = grid.cells[row] || []; @@ -475,21 +477,34 @@ export function getRectStack(grid, rect, recursed = false) { // went with pixel perfect collision rather than rounding const row = (y / gridSize) | 0; const col = (x / gridSize) | 0; - let stack = grid.cells[row][col].filter(gridCellNode => { - return gridCellNode.clientRects.find(clientRect => { - const rectX = clientRect.left; - const rectY = clientRect.top; - - // perform an AABB (axis-aligned bounding box) collision check for the - // point inside the rect - return ( - x <= rectX + clientRect.width && - x >= rectX && - y <= rectY + clientRect.height && - y >= rectY - ); - }); - }); + + // we're making an assumption that there cannot be an element in the + // grid which escapes the grid bounds. For example, if the grid is 4x4 there + // can't be an element whose midpoint is at column 5. If this happens this + // means there's an error in our grid logic that needs to be fixed + if (row > grid.cells.length || col > grid.numCols) { + throw new Error('Element midpoint exceeds the grid bounds'); + } + + // it is acceptable if a row has empty cells due to client rects not filling + // the entire bounding rect of an element + // @see https://github.com/dequelabs/axe-core/issues/3166 + let stack = + grid.cells[row][col]?.filter(gridCellNode => { + return gridCellNode.clientRects.find(clientRect => { + const rectX = clientRect.left; + const rectY = clientRect.top; + + // perform an AABB (axis-aligned bounding box) collision check for the + // point inside the rect + return ( + x <= rectX + clientRect.width && + x >= rectX && + y <= rectY + clientRect.height && + y >= rectY + ); + }); + }) ?? []; const gridContainer = grid.container; if (gridContainer) { diff --git a/test/checks/color/color-contrast.js b/test/checks/color/color-contrast.js index f2f322eb4a..339e5d1a3b 100644 --- a/test/checks/color/color-contrast.js +++ b/test/checks/color/color-contrast.js @@ -358,13 +358,55 @@ describe('color-contrast', function() { ); }); - describe('with pseudo elements', function () { + it('should not error if client rects do not fill entire bounding rect', function() { + var params = checkSetup( + '
' +
+        '\nx x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x ' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\nx' +
+        '\n
' + ); + assert.doesNotThrow(function() { + contrastEvaluate.apply(checkContext, params); + }); + }); + + describe('with pseudo elements', function() { it('should return undefined if :before pseudo element has a background color', function() { var params = checkSetup( '' + '

Content

' ); - + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { messageKey: 'pseudoContent' @@ -374,13 +416,13 @@ describe('color-contrast', function() { document.querySelector('#background') ); }); - + it('should return undefined if :after pseudo element has a background color', function() { var params = checkSetup( '' + '

Content

' ); - + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { messageKey: 'pseudoContent' @@ -390,21 +432,21 @@ describe('color-contrast', function() { document.querySelector('#background') ); }); - + it('should return undefined if pseudo element has a background image', function() { var dataURI = 'data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/' + 'XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkA' + 'ABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKU' + 'E1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7'; - + var params = checkSetup( '' + '

Content

' ); - + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { messageKey: 'pseudoContent' @@ -414,62 +456,62 @@ describe('color-contrast', function() { document.querySelector('#background') ); }); - + it('should not return undefined if pseudo element has no content', function() { var params = checkSetup( '' + '

Content

' ); - + assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - + it('should not return undefined if pseudo element is not absolutely positioned no content', function() { var params = checkSetup( '' + '

Content

' ); - + assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - + it('should not return undefined if pseudo element is has zero dimension', function() { var params = checkSetup( '' + '

Content

' ); - + assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - + it("should not return undefined if pseudo element doesn't have a background", function() { var params = checkSetup( '' + '

Content

' ); - + assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - + it('should not return undefined if pseudo element has visibility: hidden', function() { var params = checkSetup( '' + '

Content

' ); - + assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - + it('should not return undefined if pseudo element has display: none', function() { var params = checkSetup( '' + '

Content

' ); - + assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - it('should return undefined if pseudo element is more than 25% of the element', function () { + it('should return undefined if pseudo element is more than 25% of the element', function() { var params = checkSetup( '' + @@ -478,7 +520,7 @@ describe('color-contrast', function() { assert.isUndefined(contrastEvaluate.apply(checkContext, params)); }); - it('should not return undefined if pseudo element is 25% of the element', function () { + it('should not return undefined if pseudo element is 25% of the element', function() { var params = checkSetup( '' + @@ -487,17 +529,20 @@ describe('color-contrast', function() { assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - (isIE11 ? it : xit)('should return undefined if the unit is not in px', function () { - var params = checkSetup( - '' + - '

Content

' - ); - assert.isUndefined(contrastEvaluate.apply(checkContext, params)); - }); + (isIE11 ? it : xit)( + 'should return undefined if the unit is not in px', + function() { + var params = checkSetup( + '' + + '

Content

' + ); + assert.isUndefined(contrastEvaluate.apply(checkContext, params)); + } + ); }); - describe('with special texts', function () { + describe('with special texts', function() { it('should return undefined for a single character text with insufficient contrast', function() { var params = checkSetup( '
' + @@ -582,7 +627,7 @@ describe('color-contrast', function() { }); }); - describe('options', function () { + describe('options', function() { it('should support options.boldValue', function() { var params = checkSetup( '
' + @@ -732,24 +777,24 @@ describe('color-contrast', function() { ignorePseudo: true } ); - + assert.isTrue(contrastEvaluate.apply(checkContext, params)); }); - - it('should adjust the pseudo element minimum size with the options.pseudoSizeThreshold', function () { + + it('should adjust the pseudo element minimum size with the options.pseudoSizeThreshold', function() { var params = checkSetup( - '' + + '' + '

Content

', { - pseudoSizeThreshold: 0.20 + pseudoSizeThreshold: 0.2 } ); assert.isUndefined(contrastEvaluate.apply(checkContext, params)); }); }); - describe('with shadowDOM', function () { + describe('with shadowDOM', function() { (shadowSupported ? it : xit)( 'returns colors across Shadow DOM boundaries', function() { diff --git a/test/commons/dom/get-element-stack.js b/test/commons/dom/get-element-stack.js index ed31136946..290213430e 100644 --- a/test/commons/dom/get-element-stack.js +++ b/test/commons/dom/get-element-stack.js @@ -348,6 +348,46 @@ describe('dom.getElementStack', function() { assert.deepEqual(stack, []); }); + it('should throw error if element midpoint-x exceeds the grid', function() { + fixture.innerHTML = '
Hello World
'; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var vNode = axe.utils.getNodeFromTree(target); + Object.defineProperty(vNode, 'boundingClientRect', { + get: function() { + return { + left: 0, + top: 10, + width: 10000, + height: 10 + }; + } + }); + assert.throws(function() { + getElementStack(target); + }, 'Element midpoint exceeds the grid bounds'); + }); + + it('should throw error if element midpoint-y exceeds the grid', function() { + fixture.innerHTML = '
Hello World
'; + axe.testUtils.flatTreeSetup(fixture); + var target = fixture.querySelector('#target'); + var vNode = axe.utils.getNodeFromTree(target); + Object.defineProperty(vNode, 'boundingClientRect', { + get: function() { + return { + left: 0, + top: 10, + width: 10, + height: 10000 + }; + } + }); + assert.throws(function() { + getElementStack(target); + }, 'Element midpoint exceeds the grid bounds'); + }); + // IE11 either only supports clip paths defined by url() or not at all, // MDN and caniuse.com give different results... (isIE11 ? it.skip : it)(