diff --git a/lib/checks/mobile/target-offset-evaluate.js b/lib/checks/mobile/target-offset-evaluate.js index d9314b3fc4..e799cb68f1 100644 --- a/lib/checks/mobile/target-offset-evaluate.js +++ b/lib/checks/mobile/target-offset-evaluate.js @@ -12,7 +12,9 @@ export default function targetOffsetEvaluate(node, options, vNode) { if (getRoleType(vNeighbor) !== 'widget' || !isFocusable(vNeighbor)) { continue; } - const offset = roundToSingleDecimal(getOffset(vNode, vNeighbor)); + // the offset code works off radius but we want our messaging to reflect diameter + const offset = + roundToSingleDecimal(getOffset(vNode, vNeighbor, minOffset / 2)) * 2; if (offset + roundingMargin >= minOffset) { continue; } diff --git a/lib/checks/mobile/target-offset.json b/lib/checks/mobile/target-offset.json index 1954d1e970..45513ac9a8 100644 --- a/lib/checks/mobile/target-offset.json +++ b/lib/checks/mobile/target-offset.json @@ -7,11 +7,11 @@ "metadata": { "impact": "serious", "messages": { - "pass": "Target has sufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px)", - "fail": "Target has insufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px)", + "pass": "Target has sufficient space from its closest neighbors (${data.closestOffset}px should be at least ${data.minOffset}px)", + "fail": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px)", "incomplete": { - "default": "Element with negative tabindex has insufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px). Is this a target?", - "nonTabbableNeighbor": "Target has insufficient offset from a neighbor with negative tabindex (${data.closestOffset}px should be at least ${data.minOffset}px). Is the neighbor a target?" + "default": "Element with negative tabindex has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px). Is this a target?", + "nonTabbableNeighbor": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px). Is the neighbor a target?" } } } diff --git a/lib/commons/dom/get-target-size.js b/lib/commons/dom/get-target-size.js new file mode 100644 index 0000000000..f16c245227 --- /dev/null +++ b/lib/commons/dom/get-target-size.js @@ -0,0 +1,66 @@ +import findNearbyElms from './find-nearby-elms'; +import { splitRects, hasVisualOverlap } from '../math'; +import memoize from '../../core/utils/memoize'; + +const roundingMargin = 0.05; + +export default memoize(getTargetSize); + +/** + * Compute the target size of an element. + * @see https://www.w3.org/TR/WCAG22/#dfn-targets + */ +function getTargetSize(vNode, minSize) { + const nodeRect = vNode.boundingClientRect; + const overlappingVNodes = findNearbyElms(vNode).filter(vNeighbor => { + return ( + vNeighbor.getComputedStylePropertyValue('pointer-events') !== 'none' && + hasVisualOverlap(vNode, vNeighbor) + ); + }); + + if (!overlappingVNodes.length) { + return nodeRect; + } + + return getLargestUnobscuredArea(vNode, overlappingVNodes, minSize); +} + +// Find areas of the target that are not obscured +function getLargestUnobscuredArea(vNode, obscuredNodes, minSize) { + const nodeRect = vNode.boundingClientRect; + if (obscuredNodes.length === 0) { + return null; + } + const obscuringRects = obscuredNodes.map( + ({ boundingClientRect: rect }) => rect + ); + const unobscuredRects = splitRects(nodeRect, obscuringRects); + if (!unobscuredRects.length) { + return null; + } + + // Of the unobscured inner rects, work out the largest + return getLargestRect(unobscuredRects, minSize); +} + +// Find the largest rectangle in the array, prioritize ones that meet a minimum size +function getLargestRect(rects, minSize) { + return rects.reduce((rectA, rectB) => { + const rectAisMinimum = rectHasMinimumSize(minSize, rectA); + const rectBisMinimum = rectHasMinimumSize(minSize, rectB); + // Prioritize rects that pass the minimum + if (rectAisMinimum !== rectBisMinimum) { + return rectAisMinimum ? rectA : rectB; + } + const areaA = rectA.width * rectA.height; + const areaB = rectB.width * rectB.height; + return areaA > areaB ? rectA : rectB; + }); +} + +function rectHasMinimumSize(minSize, { width, height }) { + return ( + width + roundingMargin >= minSize && height + roundingMargin >= minSize + ); +} diff --git a/lib/commons/dom/index.js b/lib/commons/dom/index.js index 9e5f16a741..fbac8213f0 100644 --- a/lib/commons/dom/index.js +++ b/lib/commons/dom/index.js @@ -17,6 +17,7 @@ export { default as getOverflowHiddenAncestors } from './get-overflow-hidden-anc export { default as getRootNode } from './get-root-node'; export { default as getScrollOffset } from './get-scroll-offset'; export { default as getTabbableElements } from './get-tabbable-elements'; +export { default as getTargetSize } from './get-target-size'; export { default as getTextElementStack } from './get-text-element-stack'; export { default as getViewportSize } from './get-viewport-size'; export { default as getVisibleChildTextRects } from './get-visible-child-text-rects'; diff --git a/lib/commons/math/get-offset.js b/lib/commons/math/get-offset.js index 408a3e7cef..65cba9b0e3 100644 --- a/lib/commons/math/get-offset.js +++ b/lib/commons/math/get-offset.js @@ -1,91 +1,61 @@ +import { getTargetSize } from '../dom'; + /** * Get the offset between node A and node B * @method getOffset * @memberof axe.commons.math * @param {VirtualNode} vNodeA * @param {VirtualNode} vNodeB + * @param {Number} radius * @returns {number} */ -export default function getOffset(vNodeA, vNodeB) { - const rectA = vNodeA.boundingClientRect; - const rectB = vNodeB.boundingClientRect; - const pointA = getFarthestPoint(rectA, rectB); - const pointB = getClosestPoint(pointA, rectA, rectB); - return pointDistance(pointA, pointB); -} - -/** - * Get a point on rectA that is farthest away from rectB - * @param {Rect} rectA - * @param {Rect} rectB - * @returns {Point} - */ -function getFarthestPoint(rectA, rectB) { - const dimensionProps = [ - ['x', 'left', 'right', 'width'], - ['y', 'top', 'bottom', 'height'] - ]; - const farthestPoint = {}; - dimensionProps.forEach(([axis, start, end, diameter]) => { - if (rectB[start] < rectA[start] && rectB[end] > rectA[end]) { - farthestPoint[axis] = rectA[start] + rectA[diameter] / 2; // center | middle - return; - } - // Work out which edge of A is farthest away from the center of B - const centerB = rectB[start] + rectB[diameter] / 2; - const startDistance = Math.abs(centerB - rectA[start]); - const endDistance = Math.abs(centerB - rectA[end]); - if (startDistance >= endDistance) { - farthestPoint[axis] = rectA[start]; // left | top - } else { - farthestPoint[axis] = rectA[end]; // right | bottom - } - }); - return farthestPoint; -} +export default function getOffset(vNodeA, vNodeB, minRadiusNeighbour = 12) { + const rectA = getTargetSize(vNodeA); + const rectB = getTargetSize(vNodeB); -/** - * Get a point on the adjacentRect, that is as close the point given from ownRect - * @param {Point} ownRectPoint - * @param {Rect} ownRect - * @param {Rect} adjacentRect - * @returns {Point} - */ -function getClosestPoint({ x, y }, ownRect, adjacentRect) { - if (pointInRect({ x, y }, adjacentRect)) { - // Check if there is an opposite corner inside the adjacent rectangle - const closestPoint = getCornerInAdjacentRect( - { x, y }, - ownRect, - adjacentRect - ); - if (closestPoint !== null) { - return closestPoint; - } - adjacentRect = ownRect; + // one of the rects is fully obscured + if (rectA === null || rectB === null) { + return 0; } - const { top, right, bottom, left } = adjacentRect; - // Is the adjacent rect horizontally or vertically aligned - const xAligned = x >= left && x <= right; - const yAligned = y >= top && y <= bottom; - // Find the closest edge of the adjacent rect - const closestX = Math.abs(left - x) < Math.abs(right - x) ? left : right; - const closestY = Math.abs(top - y) < Math.abs(bottom - y) ? top : bottom; + const centerA = { + x: rectA.x + rectA.width / 2, + y: rectA.y + rectA.height / 2 + }; + const centerB = { + x: rectB.x + rectB.width / 2, + y: rectB.y + rectB.height / 2 + }; + const sideB = getClosestPoint(centerA, rectB); - if (!xAligned && yAligned) { - return { x: closestX, y }; // Closest horizontal point - } else if (xAligned && !yAligned) { - return { x, y: closestY }; // Closest vertical point - } else if (!xAligned && !yAligned) { - return { x: closestX, y: closestY }; // Closest diagonal corner + return Math.min( + // subtract the radius of the circle from the distance + pointDistance(centerA, centerB) - minRadiusNeighbour, + pointDistance(centerA, sideB) + ); +} + +function getClosestPoint(point, rect) { + let x; + let y; + + if (point.x < rect.left) { + x = rect.left; + } else if (point.x > rect.right) { + x = rect.right; + } else { + x = point.x; } - // ownRect (partially) obscures adjacentRect - if (Math.abs(x - closestX) < Math.abs(y - closestY)) { - return { x: closestX, y }; // Inside, closest edge is horizontal + + if (point.y < rect.top) { + y = rect.top; + } else if (point.y > rect.bottom) { + y = rect.bottom; } else { - return { x, y: closestY }; // Inside, closest edge is vertical + y = point.y; } + + return { x, y }; } /** @@ -95,55 +65,5 @@ function getClosestPoint({ x, y }, ownRect, adjacentRect) { * @returns {number} */ function pointDistance(pointA, pointB) { - const xDistance = Math.abs(pointA.x - pointB.x); - const yDistance = Math.abs(pointA.y - pointB.y); - if (!xDistance || !yDistance) { - return xDistance || yDistance; // If either is 0, return the other - } - return Math.sqrt(Math.pow(xDistance, 2) + Math.pow(yDistance, 2)); -} - -/** - * Return if a point is within a rect - * @param {Point} point - * @param {Rect} rect - * @returns {boolean} - */ -function pointInRect({ x, y }, rect) { - return y >= rect.top && x <= rect.right && y <= rect.bottom && x >= rect.left; -} - -/** - * - * @param {Point} ownRectPoint - * @param {Rect} ownRect - * @param {Rect} adjacentRect - * @returns {Point | null} With x and y - */ -function getCornerInAdjacentRect({ x, y }, ownRect, adjacentRect) { - let closestX, closestY; - // Find the opposite corner, if it is inside the adjacent rect; - if (x === ownRect.left && ownRect.right < adjacentRect.right) { - closestX = ownRect.right; - } else if (x === ownRect.right && ownRect.left > adjacentRect.left) { - closestX = ownRect.left; - } - if (y === ownRect.top && ownRect.bottom < adjacentRect.bottom) { - closestY = ownRect.bottom; - } else if (y === ownRect.bottom && ownRect.top > adjacentRect.top) { - closestY = ownRect.top; - } - - if (!closestX && !closestY) { - return null; // opposite corners are outside the rect, or {x,y} was a center point - } else if (!closestY) { - return { x: closestX, y }; - } else if (!closestX) { - return { x, y: closestY }; - } - if (Math.abs(x - closestX) < Math.abs(y - closestY)) { - return { x: closestX, y }; - } else { - return { x, y: closestY }; - } + return Math.hypot(pointA.x - pointB.x, pointA.y - pointB.y); } diff --git a/lib/commons/math/split-rects.js b/lib/commons/math/split-rects.js index 0273445f3a..c7d45bafda 100644 --- a/lib/commons/math/split-rects.js +++ b/lib/commons/math/split-rects.js @@ -5,7 +5,7 @@ * @memberof axe.commons.math * @param {DOMRect} outerRect * @param {DOMRect[]} overlapRects - * @returns {Rect[]} Unique array of rects + * @returns {DOMRect[]} Unique array of rects */ export default function splitRects(outerRect, overlapRects) { let uniqueRects = [outerRect]; @@ -37,19 +37,33 @@ function splitRect(inputRect, clipRect) { rects.push({ top, left, bottom, right: clipRect.left }); } if (rects.length === 0) { + // Fully overlapping + if (isEnclosedRect(inputRect, clipRect)) { + return []; + } + rects.push(inputRect); // No intersection } + return rects.map(computeRect); // add x / y / width / height } const between = (num, min, max) => num > min && num < max; function computeRect(baseRect) { - return { - ...baseRect, - x: baseRect.left, - y: baseRect.top, - height: baseRect.bottom - baseRect.top, - width: baseRect.right - baseRect.left - }; + return new window.DOMRect( + baseRect.left, + baseRect.top, + baseRect.right - baseRect.left, + baseRect.bottom - baseRect.top + ); +} + +function isEnclosedRect(rectA, rectB) { + return ( + rectA.top >= rectB.top && + rectA.left >= rectB.left && + rectA.bottom <= rectB.bottom && + rectA.right <= rectB.right + ); } diff --git a/locales/_template.json b/locales/_template.json index 44cc68bef4..06f198aa04 100644 --- a/locales/_template.json +++ b/locales/_template.json @@ -862,11 +862,11 @@ "fail": "${data} on tag disables zooming on mobile devices" }, "target-offset": { - "pass": "Target has sufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px)", - "fail": "Target has insufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px)", + "pass": "Target has sufficient space from its closest neighbors (${data.closestOffset}px should be at least ${data.minOffset}px)", + "fail": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px)", "incomplete": { - "default": "Element with negative tabindex has insufficient offset from its closest neighbor (${data.closestOffset}px should be at least ${data.minOffset}px). Is this a target?", - "nonTabbableNeighbor": "Target has insufficient offset from a neighbor with negative tabindex (${data.closestOffset}px should be at least ${data.minOffset}px). Is the neighbor a target?" + "default": "Element with negative tabindex has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px). Is this a target?", + "nonTabbableNeighbor": "Target has insufficient space to its closest neighbors. Safe clickable space has a diameter of {$data.closestOffset}px instead of at least ${data.minOffset}px). Is the neighbor a target?" } }, "target-size": { diff --git a/test/checks/mobile/target-offset.js b/test/checks/mobile/target-offset.js index 0baceac82e..f160baa10f 100644 --- a/test/checks/mobile/target-offset.js +++ b/test/checks/mobile/target-offset.js @@ -1,28 +1,26 @@ -describe('target-offset tests', function () { - 'use strict'; +describe('target-offset tests', () => { + const checkContext = axe.testUtils.MockCheckContext(); + const { checkSetup, getCheckEvaluate } = axe.testUtils; + const checkEvaluate = getCheckEvaluate('target-offset'); - var checkContext = axe.testUtils.MockCheckContext(); - var checkSetup = axe.testUtils.checkSetup; - var check = checks['target-offset']; - - afterEach(function () { + afterEach(() => { checkContext.reset(); }); - it('returns true when there are no other nearby targets', function () { - var checkArgs = checkSetup( + it('returns true when there are no other nearby targets', () => { + const checkArgs = checkSetup( 'x' ); - assert.isTrue(check.evaluate.apply(checkContext, checkArgs)); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.minOffset, 24); assert.closeTo(checkContext._data.closestOffset, 24, 0.2); }); - it('returns true when the offset is 24px', function () { - var checkArgs = checkSetup( + it('returns true when the offset is 24px', () => { + const checkArgs = checkSetup( 'x' + @@ -31,14 +29,14 @@ describe('target-offset tests', function () { '">x' ); - assert.isTrue(check.evaluate.apply(checkContext, checkArgs)); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.minOffset, 24); assert.closeTo(checkContext._data.closestOffset, 24, 0.2); }); describe('when the offset is insufficient', () => { - it('returns false for targets in the tab order', function () { - var checkArgs = checkSetup( + it('returns false for targets in the tab order', () => { + const checkArgs = checkSetup( 'x' + @@ -47,14 +45,14 @@ describe('target-offset tests', function () { '">x' ); - assert.isFalse(check.evaluate.apply(checkContext, checkArgs)); + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); assert.isUndefined(checkContext._data.messageKey); assert.equal(checkContext._data.minOffset, 24); - assert.closeTo(checkContext._data.closestOffset, 23, 0.2); + assert.closeTo(checkContext._data.closestOffset, 22, 0.2); }); - it('returns undefined for targets not in the tab order', function () { - var checkArgs = checkSetup( + it('returns undefined for targets not in the tab order', () => { + const checkArgs = checkSetup( 'x' + @@ -63,15 +61,15 @@ describe('target-offset tests', function () { '">x' ); - assert.isUndefined(check.evaluate.apply(checkContext, checkArgs)); + assert.isUndefined(checkEvaluate.apply(checkContext, checkArgs)); assert.isUndefined(checkContext._data.messageKey); assert.equal(checkContext._data.minOffset, 24); - assert.closeTo(checkContext._data.closestOffset, 23, 0.2); + assert.closeTo(checkContext._data.closestOffset, 22, 0.2); }); }); - it('ignores non-widget elements as neighbors', function () { - var checkArgs = checkSetup( + it('ignores non-widget elements as neighbors', () => { + const checkArgs = checkSetup( 'x' + @@ -80,13 +78,13 @@ describe('target-offset tests', function () { '">x' ); - assert.isTrue(check.evaluate.apply(checkContext, checkArgs)); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.minOffset, 24); assert.closeTo(checkContext._data.closestOffset, 24, 0.2); }); - it('ignores non-focusable widget elements as neighbors', function () { - var checkArgs = checkSetup( + it('ignores non-focusable widget elements as neighbors', () => { + const checkArgs = checkSetup( 'x' + @@ -95,13 +93,13 @@ describe('target-offset tests', function () { '">x' ); - assert.isTrue(check.evaluate.apply(checkContext, checkArgs)); + assert.isTrue(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.minOffset, 24); assert.closeTo(checkContext._data.closestOffset, 24, 0.2); }); - it('sets all elements that are too close as related nodes', function () { - var checkArgs = checkSetup( + it('sets all elements that are too close as related nodes', () => { + const checkArgs = checkSetup( 'x' + @@ -112,11 +110,11 @@ describe('target-offset tests', function () { 'display: inline-block; width:16px; height:16px;' + '">x' ); - assert.isFalse(check.evaluate.apply(checkContext, checkArgs)); + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.minOffset, 24); - assert.closeTo(checkContext._data.closestOffset, 16, 0.2); + assert.closeTo(checkContext._data.closestOffset, 8, 0.2); - var relatedIds = checkContext._relatedNodes.map(function (node) { + const relatedIds = checkContext._relatedNodes.map(function (node) { return '#' + node.id; }); assert.deepEqual(relatedIds, ['#left', '#right']); @@ -124,7 +122,7 @@ describe('target-offset tests', function () { describe('when neighbors are focusable but not tabbable', () => { it('returns undefined if all neighbors are not tabbable', () => { - var checkArgs = checkSetup( + const checkArgs = checkSetup( 'x' + @@ -135,19 +133,19 @@ describe('target-offset tests', function () { 'display: inline-block; width:16px; height:16px;' + '">x' ); - assert.isUndefined(check.evaluate.apply(checkContext, checkArgs)); + assert.isUndefined(checkEvaluate.apply(checkContext, checkArgs)); assert.equal(checkContext._data.messageKey, 'nonTabbableNeighbor'); assert.equal(checkContext._data.minOffset, 24); - assert.closeTo(checkContext._data.closestOffset, 16, 0.2); + assert.closeTo(checkContext._data.closestOffset, 8, 0.2); - var relatedIds = checkContext._relatedNodes.map(function (node) { + const relatedIds = checkContext._relatedNodes.map(function (node) { return '#' + node.id; }); assert.deepEqual(relatedIds, ['#left', '#right']); }); it('returns false if some but not all neighbors are not tabbable', () => { - var checkArgs = checkSetup( + const checkArgs = checkSetup( 'x' + @@ -158,12 +156,12 @@ describe('target-offset tests', function () { 'display: inline-block; width:16px; height:16px;' + '">x' ); - assert.isFalse(check.evaluate.apply(checkContext, checkArgs)); + assert.isFalse(checkEvaluate.apply(checkContext, checkArgs)); assert.isUndefined(checkContext._data.messageKey); assert.equal(checkContext._data.minOffset, 24); - assert.closeTo(checkContext._data.closestOffset, 16, 0.2); + assert.closeTo(checkContext._data.closestOffset, 8, 0.2); - var relatedIds = checkContext._relatedNodes.map(function (node) { + const relatedIds = checkContext._relatedNodes.map(function (node) { return '#' + node.id; }); assert.deepEqual(relatedIds, ['#left', '#right']); diff --git a/test/commons/dom/get-target-size.js b/test/commons/dom/get-target-size.js new file mode 100644 index 0000000000..ddb876f7aa --- /dev/null +++ b/test/commons/dom/get-target-size.js @@ -0,0 +1,47 @@ +describe('get-target-size', () => { + const getTargetSize = axe.commons.dom.getTargetSize; + const { queryFixture } = axe.testUtils; + + it('returns the bounding rect when unobscured', () => { + const vNode = queryFixture(''); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, vNode.actualNode.getBoundingClientRect()); + }); + + it('returns target size when obscured', () => { + const vNode = queryFixture(` + +
+ `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, new DOMRect(10, 5, 20, 40)); + }); + + it('ignores elements with "pointer-events: none"', () => { + const vNode = queryFixture(` + + + `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, new DOMRect(10, 5, 30, 40)); + }); + + it("ignores elements that don't overlap the target", () => { + const vNode = queryFixture(` + + + `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, new DOMRect(10, 5, 30, 40)); + }); + + it('returns the largest unobscured area', () => { + const vNode = queryFixture(` + + + + `); + const rect = getTargetSize(vNode); + assert.deepEqual(rect, new DOMRect(10, 10, 20, 35)); + }); +}); diff --git a/test/commons/math/get-offset.js b/test/commons/math/get-offset.js index 9214d6a861..cdbd1a5682 100644 --- a/test/commons/math/get-offset.js +++ b/test/commons/math/get-offset.js @@ -1,104 +1,65 @@ -describe('getOffset', function () { - 'use strict'; - var fixtureSetup = axe.testUtils.fixtureSetup; - var getOffset = axe.commons.math.getOffset; - var round = 0.2; +describe('getOffset', () => { + const fixtureSetup = axe.testUtils.fixtureSetup; + const getOffset = axe.commons.math.getOffset; + const round = 0.2; - // Return the diagonal of a square of size X, or rectangle of size X * Y - function getDiagonal(x, y) { - y = typeof y === 'number' ? y : x; - return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); - } - - it('returns with + spacing for horizontally adjacent elms', function () { - var fixture = fixtureSetup( - ' ' + - ' ' - ); - var nodeA = fixture.children[0]; - var nodeB = fixture.children[1]; - assert.closeTo(getOffset(nodeA, nodeB), 40, round); - assert.closeTo(getOffset(nodeB, nodeA), 30, round); - }); - - it('returns closest horizontal distance for elements horizontally aligned', function () { - var fixture = fixtureSetup( - ' ' + - ' ' - ); - var nodeA = fixture.children[0]; - var nodeB = fixture.children[1]; - assert.closeTo(getOffset(nodeA, nodeB), getDiagonal(40, 5), round); - assert.closeTo(getOffset(nodeB, nodeA), 30, round); + it('returns center to edge of circle when both are undersized', () => { + const fixture = fixtureSetup(` + + + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.closeTo(getOffset(nodeA, nodeB), 38, round); }); - it('returns height + spacing for vertically adjacent elms', function () { - var fixture = fixtureSetup( - ' ' + - ' ' - ); - var nodeA = fixture.children[0]; - var nodeB = fixture.children[1]; - assert.closeTo(getOffset(nodeA, nodeB), 40, round); - assert.closeTo(getOffset(nodeB, nodeA), 30, round); - }); - - it('returns closest vertical distance for elements horizontally aligned', function () { - var fixture = fixtureSetup( - ' ' + - ' ' - ); - var nodeA = fixture.children[0]; - var nodeB = fixture.children[1]; - - assert.closeTo(getOffset(nodeA, nodeB), getDiagonal(40, 10), round); - assert.closeTo(getOffset(nodeB, nodeA), 30, round); + it('returns center to edge of square when one is undersized', () => { + const fixture = fixtureSetup(` + + + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.closeTo(getOffset(nodeA, nodeB), 45, round); }); - it('returns corner to corner distance for diagonal elms', function () { - var fixture = fixtureSetup( - ' ' + - ' ' - ); - var nodeA = fixture.children[0]; - var nodeB = fixture.children[1]; - assert.closeTo(getOffset(nodeA, nodeB), getDiagonal(40), round); - assert.closeTo(getOffset(nodeB, nodeA), getDiagonal(30), round); + it('returns center to corner of square when at a diagonal', () => { + const fixture = fixtureSetup(` + + + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.closeTo(getOffset(nodeA, nodeB), 63.6, round); }); - it('returns the distance to the edge when elements overlap on an edge', function () { - var fixture = fixtureSetup( - '' + - ' ' + - '' - ); - var nodeA = fixture.children[0]; - var nodeB = nodeA.children[0]; - assert.closeTo(getOffset(nodeA, nodeB), 30, round); - assert.closeTo(getOffset(nodeB, nodeA), 30, round); + it('returns 0 if nodeA is overlapped by nodeB', () => { + const fixture = fixtureSetup(` + + + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.equal(getOffset(nodeA, nodeB), 0); }); - it('returns the shortest side of the element when an element overlaps on a corner', function () { - var fixture = fixtureSetup( - '' + - ' ' + - '' - ); - var nodeA = fixture.children[0]; - var nodeB = nodeA.children[0]; - assert.closeTo(getOffset(nodeA, nodeB), getDiagonal(30), round); - assert.closeTo(getOffset(nodeB, nodeA), 20, round); + it('returns 0 if nodeB is overlapped by nodeA', () => { + const fixture = fixtureSetup(` + + + `); + const nodeA = fixture.children[3]; + const nodeB = fixture.children[1]; + assert.equal(getOffset(nodeA, nodeB), 0); }); - it('returns smallest diagonal if elmA fully covers elmB', function () { - var fixture = fixtureSetup( - '' + - ' ' + - '' - ); - var nodeA = fixture.children[0]; - var nodeB = nodeA.children[0]; - assert.closeTo(getOffset(nodeA, nodeB), getDiagonal(10), round); - assert.closeTo(getOffset(nodeB, nodeA), 10, round); + it('subtracts minNeighbourRadius from center-to-center calculations', () => { + const fixture = fixtureSetup(` + + + `); + const nodeA = fixture.children[1]; + const nodeB = fixture.children[3]; + assert.closeTo(getOffset(nodeA, nodeB, 30), 20, round); }); }); diff --git a/test/commons/math/split-rects.js b/test/commons/math/split-rects.js index 714e03f24d..0680b44822 100644 --- a/test/commons/math/split-rects.js +++ b/test/commons/math/split-rects.js @@ -1,92 +1,95 @@ -describe('splitRects', function () { - var splitRects = axe.commons.math.splitRects; - function createRect(x, y, width, height) { - return { - x: x, - y: y, - width: width, - height: height, - top: y, - left: x, - bottom: y + height, - right: x + width - }; - } +describe('splitRects', () => { + const splitRects = axe.commons.math.splitRects; - it('returns the original rect if there is no clipping rect', function () { - var rectA = createRect(0, 0, 100, 50); - var rects = splitRects(rectA, []); + it('returns the original rect if there is no clipping rect', () => { + const rectA = new DOMRect(0, 0, 100, 50); + const rects = splitRects(rectA, []); assert.lengthOf(rects, 1); assert.deepEqual(rects[0], rectA); }); - it('returns the original rect if there is no overlap', function () { - var rectA = createRect(0, 0, 100, 50); - var rectB = createRect(0, 50, 50, 50); - var rects = splitRects(rectA, [rectB]); + it('returns the original rect if there is no overlap', () => { + const rectA = new DOMRect(0, 0, 100, 50); + const rectB = new DOMRect(0, 50, 50, 50); + const rects = splitRects(rectA, [rectB]); assert.lengthOf(rects, 1); assert.deepEqual(rects[0], rectA); }); - describe('with one overlapping rect', function () { - it('returns one rect if overlaps covers two corners', function () { - var rectA = createRect(0, 0, 100, 50); - var rectB = createRect(40, 0, 100, 50); - var rects = splitRects(rectA, [rectB]); + describe('with one overlapping rect', () => { + it('returns one rect if overlaps covers two corners', () => { + const rectA = new DOMRect(0, 0, 100, 50); + const rectB = new DOMRect(40, 0, 100, 50); + const rects = splitRects(rectA, [rectB]); assert.lengthOf(rects, 1); - assert.deepEqual(rects[0], createRect(0, 0, 40, 50)); + assert.deepEqual(rects[0], new DOMRect(0, 0, 40, 50)); }); - it('returns two rects if overlap covers one corner', function () { - var rectA = createRect(0, 0, 100, 100); - var rectB = createRect(50, 50, 50, 50); - var rects = splitRects(rectA, [rectB]); + it('returns two rects if overlap covers one corner', () => { + const rectA = new DOMRect(0, 0, 100, 100); + const rectB = new DOMRect(50, 50, 50, 50); + const rects = splitRects(rectA, [rectB]); assert.lengthOf(rects, 2); - assert.deepEqual(rects[0], createRect(0, 0, 100, 50)); - assert.deepEqual(rects[1], createRect(0, 0, 50, 100)); + assert.deepEqual(rects[0], new DOMRect(0, 0, 100, 50)); + assert.deepEqual(rects[1], new DOMRect(0, 0, 50, 100)); }); - it('returns three rects if overlap covers an edge, but no corner', function () { - var rectA = createRect(0, 0, 100, 150); - var rectB = createRect(50, 50, 50, 50); - var rects = splitRects(rectA, [rectB]); + it('returns three rects if overlap covers an edge, but no corner', () => { + const rectA = new DOMRect(0, 0, 100, 150); + const rectB = new DOMRect(50, 50, 50, 50); + const rects = splitRects(rectA, [rectB]); assert.lengthOf(rects, 3); - assert.deepEqual(rects[0], createRect(0, 0, 100, 50)); - assert.deepEqual(rects[1], createRect(0, 100, 100, 50)); - assert.deepEqual(rects[2], createRect(0, 0, 50, 150)); + assert.deepEqual(rects[0], new DOMRect(0, 0, 100, 50)); + assert.deepEqual(rects[1], new DOMRect(0, 100, 100, 50)); + assert.deepEqual(rects[2], new DOMRect(0, 0, 50, 150)); }); - it('returns four rects if overlap sits in the middle, touching no corner', function () { - var rectA = createRect(0, 0, 150, 150); - var rectB = createRect(50, 50, 50, 50); - var rects = splitRects(rectA, [rectB]); + it('returns four rects if overlap sits in the middle, touching no corner', () => { + const rectA = new DOMRect(0, 0, 150, 150); + const rectB = new DOMRect(50, 50, 50, 50); + const rects = splitRects(rectA, [rectB]); assert.lengthOf(rects, 4); - assert.deepEqual(rects[0], createRect(0, 0, 150, 50)); - assert.deepEqual(rects[1], createRect(100, 0, 50, 150)); - assert.deepEqual(rects[2], createRect(0, 100, 150, 50)); - assert.deepEqual(rects[3], createRect(0, 0, 50, 150)); + assert.deepEqual(rects[0], new DOMRect(0, 0, 150, 50)); + assert.deepEqual(rects[1], new DOMRect(100, 0, 50, 150)); + assert.deepEqual(rects[2], new DOMRect(0, 100, 150, 50)); + assert.deepEqual(rects[3], new DOMRect(0, 0, 50, 150)); + }); + + it('returns no rects if overlap covers the entire input rect', () => { + const rectA = new DOMRect(0, 0, 100, 50); + const rectB = new DOMRect(-50, -50, 400, 400); + const rects = splitRects(rectA, [rectB]); + assert.lengthOf(rects, 0); }); }); - describe('with multiple overlaps', function () { - it('can return a single rect two overlaps each cover an edge', function () { - var rectA = createRect(0, 0, 150, 50); - var rectB = createRect(0, 0, 50, 50); - var rectC = createRect(100, 0, 50, 50); - var rects = splitRects(rectA, [rectB, rectC]); + describe('with multiple overlaps', () => { + it('can return a single rect two overlaps each cover an edge', () => { + const rectA = new DOMRect(0, 0, 150, 50); + const rectB = new DOMRect(0, 0, 50, 50); + const rectC = new DOMRect(100, 0, 50, 50); + const rects = splitRects(rectA, [rectB, rectC]); assert.lengthOf(rects, 1); - assert.deepEqual(rects[0], createRect(50, 0, 50, 50)); + assert.deepEqual(rects[0], new DOMRect(50, 0, 50, 50)); }); - it('can recursively clips regions', function () { - var rectA = createRect(0, 0, 150, 100); - var rectB = createRect(0, 50, 50, 50); - var rectC = createRect(100, 50, 50, 50); - var rects = splitRects(rectA, [rectB, rectC]); + it('can recursively clips regions', () => { + const rectA = new DOMRect(0, 0, 150, 100); + const rectB = new DOMRect(0, 50, 50, 50); + const rectC = new DOMRect(100, 50, 50, 50); + const rects = splitRects(rectA, [rectB, rectC]); assert.lengthOf(rects, 3); - assert.deepEqual(rects[0], createRect(0, 0, 150, 50)); - assert.deepEqual(rects[1], createRect(50, 0, 100, 50)); - assert.deepEqual(rects[2], createRect(50, 0, 50, 100)); + assert.deepEqual(rects[0], new DOMRect(0, 0, 150, 50)); + assert.deepEqual(rects[1], new DOMRect(50, 0, 100, 50)); + assert.deepEqual(rects[2], new DOMRect(50, 0, 50, 100)); + }); + + it('returns no rects if overlap covers the entire input rect', () => { + const rectA = new DOMRect(0, 0, 100, 50); + const rectB = new DOMRect(50, 50, 200, 200); + const rectC = new DOMRect(-50, -50, 200, 200); + const rects = splitRects(rectA, [rectB, rectC]); + assert.lengthOf(rects, 0); }); }); }); diff --git a/test/integration/full/target-size/target-size.html b/test/integration/full/target-size/target-size.html index 02a36e9a25..d6a1b2f873 100644 --- a/test/integration/full/target-size/target-size.html +++ b/test/integration/full/target-size/target-size.html @@ -350,7 +350,7 @@ -Example E1 and E2 pass, the two outside elements of E3 and E4 fail.
+Example E1 - E3 pass, E4 fails.
Example F1 and F2 pass, the inside element of F3 and F4 fail.
+Example F1 - F3 pass, F4 fails.