From 279ec828372bc7bd083a335a6fc45fb50a991d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 8 Apr 2024 12:17:13 +0200 Subject: [PATCH 1/9] Update applicability to reject paragraph subtrees --- packages/alfa-rules/src/sia-r113/rule.ts | 52 +++++++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/alfa-rules/src/sia-r113/rule.ts b/packages/alfa-rules/src/sia-r113/rule.ts index 6f0d9d33e7..be99715165 100644 --- a/packages/alfa-rules/src/sia-r113/rule.ts +++ b/packages/alfa-rules/src/sia-r113/rule.ts @@ -1,7 +1,8 @@ import { Rule } from "@siteimprove/alfa-act"; +import { DOM } from "@siteimprove/alfa-aria"; import { Cache } from "@siteimprove/alfa-cache"; import { Device } from "@siteimprove/alfa-device"; -import { Document, Element } from "@siteimprove/alfa-dom"; +import { Document, Element, Node } from "@siteimprove/alfa-dom"; import { Either } from "@siteimprove/alfa-either"; import { Iterable } from "@siteimprove/alfa-iterable"; import { Rectangle } from "@siteimprove/alfa-rectangle"; @@ -9,6 +10,8 @@ import { Err, Ok } from "@siteimprove/alfa-result"; import { Sequence } from "@siteimprove/alfa-sequence"; import { Criterion } from "@siteimprove/alfa-wcag"; import { Page } from "@siteimprove/alfa-web"; +import { Predicate } from "@siteimprove/alfa-predicate"; +import { Style } from "@siteimprove/alfa-style"; import { expectation } from "../common/act/expectation"; @@ -18,6 +21,11 @@ import { WithBoundingBox, WithName } from "../common/diagnostic"; import { hasSufficientSize } from "../common/predicate/has-sufficient-size"; import { isUserAgentControlled } from "../common/predicate/is-user-agent-controlled"; +import { hasName } from "@siteimprove/alfa-aria/src/role/predicate"; + +const { and } = Predicate; +const { hasComputedStyle, isFocusable, isVisible } = Style; +const { hasRole } = DOM; export default Rule.Atomic.of({ uri: "https://alfa.siteimprove.com/rules/sia-r113", @@ -25,7 +33,47 @@ export default Rule.Atomic.of({ evaluate({ device, document }) { return { applicability() { - return targetsOfPointerEvents(document, device); + // Strategy: Traverse tree and + // 1) reject subtrees that are text blocks, see sia-r62 + // 2) collect targets of pointer events + + const isParagraph = hasRole(device, "paragraph"); + const targetOfPointerEvent = and( + hasComputedStyle( + "pointer-events", + (keyword) => keyword.value !== "none", + device, + ), + isFocusable(device), + isVisible(device), + hasRole(device, (role) => role.isWidget()), + (target) => target.getBoundingBox(device).isSome(), + ); + + let targets: Array = []; + + function visit(node: Node): void { + if (Element.isElement(node)) { + if (isParagraph(node)) { + // If we encounter a paragraph, we can skip the entire subtree + return; + } + + if (targetOfPointerEvent(node)) { + targets.push(node); + } + } + + for (const child of node.children(Node.fullTree)) { + visit(child); + } + } + + visit(document); + + return Sequence.from(targets); + + // return targetsOfPointerEvents(document, device); }, expectations(target) { From b7fd020be36b9ef74d699209d72b5a9d3461c593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Mon, 8 Apr 2024 14:17:13 +0200 Subject: [PATCH 2/9] Add function for getting all targets needed for spacing calculation --- .../targets-of-pointer-events.ts | 80 +++++++++++++++++-- packages/alfa-rules/src/sia-r111/rule.ts | 4 +- packages/alfa-rules/src/sia-r113/rule.ts | 76 +++--------------- 3 files changed, 84 insertions(+), 76 deletions(-) diff --git a/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts b/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts index bbced103ed..9925732cd1 100644 --- a/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts +++ b/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts @@ -1,27 +1,90 @@ import { DOM } from "@siteimprove/alfa-aria"; import { Cache } from "@siteimprove/alfa-cache"; import { Device } from "@siteimprove/alfa-device"; -import { Document, Element, Node, Query } from "@siteimprove/alfa-dom"; +import { Document, Element, Node } from "@siteimprove/alfa-dom"; import { Predicate } from "@siteimprove/alfa-predicate"; import { Sequence } from "@siteimprove/alfa-sequence"; import { Style } from "@siteimprove/alfa-style"; +import { Query } from "@siteimprove/alfa-dom"; const { hasRole } = DOM; -const { hasComputedStyle, isFocusable } = Style; +const { hasComputedStyle, isFocusable, isVisible } = Style; const { and } = Predicate; + const { getElementDescendants } = Query; -const cache = Cache.empty>>(); +const applicabilityCache = Cache.empty< + Document, + Cache> +>(); /** * @internal */ -export function targetsOfPointerEvents( +export function applicableTargetsOfPointerEvents( document: Document, device: Device, ): Sequence { - return cache.get(document, Cache.empty).get(device, () => + return applicabilityCache.get(document, Cache.empty).get(device, () => { + const isParagraph = hasRole(device, "paragraph"); + const targetOfPointerEvent = and( + hasComputedStyle( + "pointer-events", + (keyword) => keyword.value !== "none", + device, + ), + isFocusable(device), + isVisible(device), + // TODO: Exclude elements + hasRole(device, (role) => role.isWidget()), + hasBoundingBox(device), + ); + + function visit(node: Node, result: Array = []): Iterable { + if (Element.isElement(node)) { + if (isParagraph(node)) { + // If we encounter a paragraph, we can skip the entire subtree + return result; + } + + // TODO: It's not enough to reject paragraphs, we need to reject all text blocks in order to avoid false positives + + if (targetOfPointerEvent(node)) { + result.push(node); + } + } + + for (const child of node.children(Node.fullTree)) { + visit(child, result); + } + + return result; + } + + return Sequence.from(visit(document)); + }); +} + +const allTargetsCache = Cache.empty< + Document, + Cache> +>(); + +/** + * @internal + * + * @privateRemarks + * This function is not used in the applicability of R111 or R113, + * but in the expectation of R113 since all other targets are needed + * to determine if an applicable target is underspaced. + * It's kept here since it's closely related to the applicability. + */ +export function allTargetsOfPointerEvents( + document: Document, + device: Device, +): Sequence { + return allTargetsCache.get(document, Cache.empty).get(device, () => getElementDescendants(document, Node.fullTree).filter( and( hasComputedStyle( @@ -29,10 +92,13 @@ export function targetsOfPointerEvents( (keyword) => keyword.value !== "none", device, ), - isFocusable(device), hasRole(device, (role) => role.isWidget()), - (target) => target.getBoundingBox(device).isSome(), + hasBoundingBox(device), ), ), ); } + +function hasBoundingBox(device: Device): Predicate { + return (element) => element.getBoundingBox(device).isSome(); +} diff --git a/packages/alfa-rules/src/sia-r111/rule.ts b/packages/alfa-rules/src/sia-r111/rule.ts index 4808d305ed..ac79a8a574 100644 --- a/packages/alfa-rules/src/sia-r111/rule.ts +++ b/packages/alfa-rules/src/sia-r111/rule.ts @@ -8,7 +8,7 @@ import { Page } from "@siteimprove/alfa-web"; import { expectation } from "../common/act/expectation"; -import { targetsOfPointerEvents } from "../common/applicability/targets-of-pointer-events"; +import { applicableTargetsOfPointerEvents } from "../common/applicability/targets-of-pointer-events"; import { WithBoundingBox, WithName } from "../common/diagnostic"; @@ -21,7 +21,7 @@ export default Rule.Atomic.of({ evaluate({ device, document }) { return { applicability() { - return targetsOfPointerEvents(document, device); + return applicableTargetsOfPointerEvents(document, device); }, expectations(target) { diff --git a/packages/alfa-rules/src/sia-r113/rule.ts b/packages/alfa-rules/src/sia-r113/rule.ts index be99715165..b85648a858 100644 --- a/packages/alfa-rules/src/sia-r113/rule.ts +++ b/packages/alfa-rules/src/sia-r113/rule.ts @@ -1,8 +1,7 @@ import { Rule } from "@siteimprove/alfa-act"; -import { DOM } from "@siteimprove/alfa-aria"; import { Cache } from "@siteimprove/alfa-cache"; import { Device } from "@siteimprove/alfa-device"; -import { Document, Element, Node } from "@siteimprove/alfa-dom"; +import { Document, Element } from "@siteimprove/alfa-dom"; import { Either } from "@siteimprove/alfa-either"; import { Iterable } from "@siteimprove/alfa-iterable"; import { Rectangle } from "@siteimprove/alfa-rectangle"; @@ -10,22 +9,18 @@ import { Err, Ok } from "@siteimprove/alfa-result"; import { Sequence } from "@siteimprove/alfa-sequence"; import { Criterion } from "@siteimprove/alfa-wcag"; import { Page } from "@siteimprove/alfa-web"; -import { Predicate } from "@siteimprove/alfa-predicate"; -import { Style } from "@siteimprove/alfa-style"; import { expectation } from "../common/act/expectation"; -import { targetsOfPointerEvents } from "../common/applicability/targets-of-pointer-events"; +import { + applicableTargetsOfPointerEvents, + allTargetsOfPointerEvents, +} from "../common/applicability/targets-of-pointer-events"; import { WithBoundingBox, WithName } from "../common/diagnostic"; import { hasSufficientSize } from "../common/predicate/has-sufficient-size"; import { isUserAgentControlled } from "../common/predicate/is-user-agent-controlled"; -import { hasName } from "@siteimprove/alfa-aria/src/role/predicate"; - -const { and } = Predicate; -const { hasComputedStyle, isFocusable, isVisible } = Style; -const { hasRole } = DOM; export default Rule.Atomic.of({ uri: "https://alfa.siteimprove.com/rules/sia-r113", @@ -33,47 +28,7 @@ export default Rule.Atomic.of({ evaluate({ device, document }) { return { applicability() { - // Strategy: Traverse tree and - // 1) reject subtrees that are text blocks, see sia-r62 - // 2) collect targets of pointer events - - const isParagraph = hasRole(device, "paragraph"); - const targetOfPointerEvent = and( - hasComputedStyle( - "pointer-events", - (keyword) => keyword.value !== "none", - device, - ), - isFocusable(device), - isVisible(device), - hasRole(device, (role) => role.isWidget()), - (target) => target.getBoundingBox(device).isSome(), - ); - - let targets: Array = []; - - function visit(node: Node): void { - if (Element.isElement(node)) { - if (isParagraph(node)) { - // If we encounter a paragraph, we can skip the entire subtree - return; - } - - if (targetOfPointerEvent(node)) { - targets.push(node); - } - } - - for (const child of node.children(Node.fullTree)) { - visit(child); - } - } - - visit(document); - - return Sequence.from(targets); - - // return targetsOfPointerEvents(document, device); + return applicableTargetsOfPointerEvents(document, device); }, expectations(target) { @@ -170,11 +125,6 @@ export namespace Outcomes { ); } -const undersizedCache = Cache.empty< - Document, - Cache> ->(); - /** * Yields all elements that have insufficient spacing to the target. * @@ -193,18 +143,10 @@ function* findElementsWithInsufficientSpacingToTarget( // Existence of a bounding box is guaranteed by applicability const targetRect = target.getBoundingBox(device).getUnsafe(); - const undersizedTargets = undersizedCache - .get(document, Cache.empty) - .get(device, () => - targetsOfPointerEvents(document, device).reject( - hasSufficientSize(24, device), - ), - ); - // TODO: This needs to be optimized, we should be able to use some spatial data structure like a quadtree to reduce the number of comparisons - for (const candidate of targetsOfPointerEvents(document, device)) { + for (const candidate of allTargetsOfPointerEvents(document, device)) { if (target !== candidate) { - // Existence of a bounding box is guaranteed by applicability + // Existence of a bounding box should be guaranteed by implementation of allTargetsOfPointerEvents const candidateRect = candidate.getBoundingBox(device).getUnsafe(); if ( @@ -213,7 +155,7 @@ function* findElementsWithInsufficientSpacingToTarget( targetRect.center.y, 12, ) || - (undersizedTargets.includes(candidate) && + (!hasSufficientSize(24, device)(candidate) && targetRect.distanceSquared(candidateRect) < 24 ** 2) ) { // The 24px diameter circle of the target must not intersect with the bounding box of any other target, or From ade36f9d57df427e0f0989c7ffae2a708d5725fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:47:17 +0200 Subject: [PATCH 3/9] Use generator function --- .../common/applicability/targets-of-pointer-events.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts b/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts index 9925732cd1..66da8efd7e 100644 --- a/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts +++ b/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts @@ -41,25 +41,23 @@ export function applicableTargetsOfPointerEvents( hasBoundingBox(device), ); - function visit(node: Node, result: Array = []): Iterable { + function* visit(node: Node): Iterable { if (Element.isElement(node)) { if (isParagraph(node)) { // If we encounter a paragraph, we can skip the entire subtree - return result; + return; } // TODO: It's not enough to reject paragraphs, we need to reject all text blocks in order to avoid false positives if (targetOfPointerEvent(node)) { - result.push(node); + yield node; } } for (const child of node.children(Node.fullTree)) { - visit(child, result); + yield* visit(child); } - - return result; } return Sequence.from(visit(document)); From f599537166f4c3005231bd7e0c570da58a71dfb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:56:07 +0200 Subject: [PATCH 4/9] Add cache again --- packages/alfa-rules/src/sia-r113/rule.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/alfa-rules/src/sia-r113/rule.ts b/packages/alfa-rules/src/sia-r113/rule.ts index b85648a858..b430dabaec 100644 --- a/packages/alfa-rules/src/sia-r113/rule.ts +++ b/packages/alfa-rules/src/sia-r113/rule.ts @@ -125,6 +125,10 @@ export namespace Outcomes { ); } +const undersizedCache = Cache.empty< + Document, + Cache> +>(); /** * Yields all elements that have insufficient spacing to the target. * @@ -143,6 +147,14 @@ function* findElementsWithInsufficientSpacingToTarget( // Existence of a bounding box is guaranteed by applicability const targetRect = target.getBoundingBox(device).getUnsafe(); + const undersizedTargets = undersizedCache + .get(document, Cache.empty) + .get(device, () => + allTargetsOfPointerEvents(document, device).reject( + hasSufficientSize(24, device), + ), + ); + // TODO: This needs to be optimized, we should be able to use some spatial data structure like a quadtree to reduce the number of comparisons for (const candidate of allTargetsOfPointerEvents(document, device)) { if (target !== candidate) { @@ -155,7 +167,7 @@ function* findElementsWithInsufficientSpacingToTarget( targetRect.center.y, 12, ) || - (!hasSufficientSize(24, device)(candidate) && + (undersizedTargets.includes(candidate) && targetRect.distanceSquared(candidateRect) < 24 ** 2) ) { // The 24px diameter circle of the target must not intersect with the bounding box of any other target, or From 9c7b2054c70fcb60fbde32a7b7ee310992ccf30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:59:02 +0200 Subject: [PATCH 5/9] Add changeset --- .changeset/slimy-socks-cry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slimy-socks-cry.md diff --git a/.changeset/slimy-socks-cry.md b/.changeset/slimy-socks-cry.md new file mode 100644 index 0000000000..d52abd0398 --- /dev/null +++ b/.changeset/slimy-socks-cry.md @@ -0,0 +1,5 @@ +--- +"@siteimprove/alfa-rules": patch +--- + +**Fixed:** R113 now is no longer applicable to invisible targets and targets inside a paragraph From 1449477fccae2d569827b52faa1a6f6e46f9674a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:59:49 +0200 Subject: [PATCH 6/9] Update slimy-socks-cry.md --- .changeset/slimy-socks-cry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/slimy-socks-cry.md b/.changeset/slimy-socks-cry.md index d52abd0398..55ccfd7545 100644 --- a/.changeset/slimy-socks-cry.md +++ b/.changeset/slimy-socks-cry.md @@ -2,4 +2,4 @@ "@siteimprove/alfa-rules": patch --- -**Fixed:** R113 now is no longer applicable to invisible targets and targets inside a paragraph +**Fixed:** R113 is no longer applicable to invisible targets and targets inside a paragraph From 873fbed3598c2d711a81bf1bbf809ae0637f7e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 9 Apr 2024 12:47:35 +0200 Subject: [PATCH 7/9] Update .changeset/slimy-socks-cry.md Co-authored-by: Jean-Yves Moyen --- .changeset/slimy-socks-cry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/slimy-socks-cry.md b/.changeset/slimy-socks-cry.md index 55ccfd7545..f61a0d1274 100644 --- a/.changeset/slimy-socks-cry.md +++ b/.changeset/slimy-socks-cry.md @@ -2,4 +2,4 @@ "@siteimprove/alfa-rules": patch --- -**Fixed:** R113 is no longer applicable to invisible targets and targets inside a paragraph +**Fixed:** R111 and R113 are no longer applicable to invisible targets and targets inside a paragraph From cc057909f8f4c59512dee72a9ae09704a578a881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:00:02 +0200 Subject: [PATCH 8/9] Factor target predicate --- .../targets-of-pointer-events.ts | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts b/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts index 66da8efd7e..84e3b7b535 100644 --- a/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts +++ b/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts @@ -10,7 +10,7 @@ import { Query } from "@siteimprove/alfa-dom"; const { hasRole } = DOM; const { hasComputedStyle, isFocusable, isVisible } = Style; -const { and } = Predicate; +const { and, not } = Predicate; const { getElementDescendants } = Query; @@ -28,18 +28,7 @@ export function applicableTargetsOfPointerEvents( ): Sequence { return applicabilityCache.get(document, Cache.empty).get(device, () => { const isParagraph = hasRole(device, "paragraph"); - const targetOfPointerEvent = and( - hasComputedStyle( - "pointer-events", - (keyword) => keyword.value !== "none", - device, - ), - isFocusable(device), - isVisible(device), - // TODO: Exclude elements - hasRole(device, (role) => role.isWidget()), - hasBoundingBox(device), - ); + const isArea = (element: Element) => element.name === "area"; function* visit(node: Node): Iterable { if (Element.isElement(node)) { @@ -50,7 +39,7 @@ export function applicableTargetsOfPointerEvents( // TODO: It's not enough to reject paragraphs, we need to reject all text blocks in order to avoid false positives - if (targetOfPointerEvent(node)) { + if (and(isTarget(device), isVisible(device), not(isArea))(node)) { yield node; } } @@ -82,18 +71,23 @@ export function allTargetsOfPointerEvents( document: Document, device: Device, ): Sequence { - return allTargetsCache.get(document, Cache.empty).get(device, () => - getElementDescendants(document, Node.fullTree).filter( - and( - hasComputedStyle( - "pointer-events", - (keyword) => keyword.value !== "none", - device, - ), - hasRole(device, (role) => role.isWidget()), - hasBoundingBox(device), - ), + return allTargetsCache + .get(document, Cache.empty) + .get(device, () => + getElementDescendants(document, Node.fullTree).filter(isTarget(device)), + ); +} + +function isTarget(device: Device): Predicate { + return and( + hasComputedStyle( + "pointer-events", + (keyword) => keyword.value !== "none", + device, ), + isFocusable(device), + hasRole(device, (role) => role.isWidget()), + hasBoundingBox(device), ); } From d2a8422663280588dd7ca0fa9f2acf5c171d917a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rolf=20Christian=20J=C3=B8rgensen?= <114920418+rcj-siteimprove@users.noreply.github.com> Date: Tue, 9 Apr 2024 15:31:08 +0200 Subject: [PATCH 9/9] Add tests --- .../targets-of-pointer-events.ts | 3 +- .../alfa-rules/test/sia-r113/rule.spec.tsx | 115 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts b/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts index 84e3b7b535..01a7fb493a 100644 --- a/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts +++ b/packages/alfa-rules/src/common/applicability/targets-of-pointer-events.ts @@ -39,7 +39,7 @@ export function applicableTargetsOfPointerEvents( // TODO: It's not enough to reject paragraphs, we need to reject all text blocks in order to avoid false positives - if (and(isTarget(device), isVisible(device), not(isArea))(node)) { + if (and(isTarget(device), not(isArea))(node)) { yield node; } } @@ -86,6 +86,7 @@ function isTarget(device: Device): Predicate { device, ), isFocusable(device), + isVisible(device), hasRole(device, (role) => role.isWidget()), hasBoundingBox(device), ); diff --git a/packages/alfa-rules/test/sia-r113/rule.spec.tsx b/packages/alfa-rules/test/sia-r113/rule.spec.tsx index 344c1baf9b..22628a0904 100644 --- a/packages/alfa-rules/test/sia-r113/rule.spec.tsx +++ b/packages/alfa-rules/test/sia-r113/rule.spec.tsx @@ -114,6 +114,74 @@ test("evaluate() passes button with clickable area of less than 24x24 pixels and ]); }); +test("evaluate() passes undersized button with vertically adjacent undersized button that is not displayed", async (t) => { + const device = Device.standard(); + + // The 24px diameter circles of the targets does not intersect with the bounding box of the other target, but the circles do intersect + const target1 = ( + + ); + + const target2 = ( + + ); + + const document = h.document([target1, target2]); + + t.deepEqual(await evaluate(R113, { document, device }), [ + passed(R113, target1, { + 1: Outcomes.HasSufficientSpacing( + "Hello", + target1.getBoundingBox(device).getUnsafe(), + ), + }), + ]); +}); + +test("evaluate() passes undersized button with vertically adjacent undersized button that is hidden", async (t) => { + const device = Device.standard(); + + // The 24px diameter circles of the targets does not intersect with the bounding box of the other target, but the circles do intersect + const target1 = ( + + ); + + const target2 = ( + + ); + + const document = h.document([target1, target2]); + + t.deepEqual(await evaluate(R113, { document, device }), [ + passed(R113, target1, { + 1: Outcomes.HasSufficientSpacing( + "Hello", + target1.getBoundingBox(device).getUnsafe(), + ), + }), + ]); +}); + test("evaluate() fails undersized button with vertically adjacent undersized button", async (t) => { const device = Device.standard(); @@ -250,4 +318,51 @@ test("evaluate() is inapplicable when there is no layout information", async (t) t.deepEqual(await evaluate(R113, { document, device }), [inapplicable(R113)]); }); +test("evaluate() is inapplicable to elements", async (t) => { + const device = Device.standard(); + + const img = foo; + const map = ; + + const document = h.document([img, map]); + + t.deepEqual(await evaluate(R113, { document, device }), [inapplicable(R113)]); +}); + +test("evaluate() is inapplicable to button with display: none", async (t) => { + const device = Device.standard(); + + const target = ( + + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R113, { document, device }), [ + inapplicable(R113), + ]); +}); + +test("evaluate() is inapplicable to button with visibility: hidden", async (t) => { + const device = Device.standard(); + const target = ( + + ); + + const document = h.document([target]); + + t.deepEqual(await evaluate(R113, { document, device }), [ + inapplicable(R113), + ]); +});